堆栈中ShellCode的利用原理
文章概览:
001-关于堆栈ShellCode操作:基础理论
002-利用fs寄存器寻找当前程序dll的入口:从动态运行的程序中定位所需dll
003-寻找大兵LoadLibraryA:从定位到的dll中寻找所需函数地址
004-被截断的shellCode:加解密,解决shellCode的零字截断问题
前言
在进入正题之前,需要先了解一下函数调用的堆栈变化。
//代码内容:一个简单的函数调用
//环境:vs2022 debug x86
#include <iostream>
void fun() {
}
int main() {
fun();
return 0;
}
;fun函数调用的汇编代码
;在执行该汇编语句时,会将该代码的下一个指令的地址进行压栈(0x006C1886)
;目的是为了回跳,继续执行后续代码
006C1881 call 006C10F0
006C1886 xor eax,eax
;提升堆栈
006C1800 push ebp
006C1801 mov ebp,esp
006C1803 sub esp,0C0h ;堆栈默认提升0xc0大小
;保存函数调用前的寄存器状态
006C1809 push ebx
006C180A push esi
006C180B push edi
;初始化提升的新堆栈
006C180C mov edi,ebp
006C180E xor ecx,ecx
006C1810 mov eax,0CCCCCCCCh
006C1815 rep stos dword ptr es:[edi]
006C1817 mov ecx,6CC066h
006C181C call 006C1311
;一般功能代码在中间附近出现
;结束功能代码后,恢复现场
006C1821 pop edi
006C1822 pop esi
006C1823 pop ebx
006C1824 add esp,0C0h
006C182A cmp ebp,esp
006C182C call 006C123A
006C1831 mov esp,ebp
;恢复调用前的堆栈
006C1833 pop ebp
;跳转回地址0x006C1886处
006C1834 ret
在执行完call指令之后,正式进入函数内部,进行一系列初始化及环境保存操作。
函数调用中的堆栈情况如下:
正文
对上述两个堆栈状态有个印象,接下来继续进行代码分析。
在用vs环境进行代码编写的时候,遇见类似strcpy,strcmp等函数调用时,如果不将设置中的安全检查取消,那么将判定上述函数为危险函数。其危险的根本就是没有对拷贝、比较等操作限制字符个数,如果无法预见“\0”字符或比较出差值,那么将一直执行下去。
#报错如下
D:\Code\CPP\CPP控制台程序\CPP控制台程序\main.cpp(7,2): error C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
//代码内容:一个简单的函数调用
//环境:vs2022 debug x86
#include <iostream>
void fun(const char * c ) {
char arr[4];
strcpy(arr, c);
}
int main() {
char c[1024] = { 0 };
scanf("%s");
fun(c);
return 0;
}
/*
程序开始后输入:11111111111111111111
*/
;提升堆栈
00141EC0 | 55 | push ebp | main.cpp:5
00141EC1 | 8BEC | mov ebp,esp |
;sub esp,CC这句表示提升栈空间0xCC大小,十进制也就是204字节
00141EC3 | 81EC CC000000 | sub esp,CC |
;保存现场
00141EC9 | 53 | push ebx |
00141ECA | 56 | push esi | esi:__enc$textbss$end+23
00141ECB | 57 | push edi |
;初始化新堆栈
00141ECC | 8D7D F4 | lea edi,dword ptr ss:[ebp-C] |
00141ECF | B9 03000000 | mov ecx,3 |
00141ED4 | B8 CCCCCCCC | mov eax,CCCCCCCC | eax:"11111111111111111111"
00141ED9 | F3:AB | rep stosd |
00141EDB | B9 66C01400 | mov ecx,_A0FF1F1C_CPP | main.cpp:15732480
00141EE0 | E8 2CF4FFFF | call cpp控制台程序.141311 |
;拷贝功能代码的汇编形式
00141EE5 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] | main.cpp:7, [ebp+8]:"11111111111111111111"
00141EE8 | 50 | push eax | eax:"11111111111111111111"
00141EE9 | 8D4D F8 | lea ecx,dword ptr ss:[ebp-8] |
00141EEC | 51 | push ecx |
00141EED | E8 C9F4FFFF | call cpp控制台程序.1413BB | strcpy
;结束函数调用,进行堆栈平衡
00141EF2 | 83C4 08 | add esp,8 |
00141EF5 | 52 | push edx | main.cpp:8
00141EF6 | 8BCD | mov ecx,ebp |
00141EF8 | 50 | push eax | eax:"11111111111111111111"
00141EF9 | 8D15 1C1F1400 | lea edx,dword ptr ds:[<>] |
00141EFF | E8 D7F2FFFF | call cpp控制台程序.1411DB |
;恢复现场
00141F04 | 58 | pop eax | eax:"11111111111111111111"
00141F05 | 5A | pop edx |
00141F06 | 5F | pop edi |
00141F07 | 5E | pop esi | esi:__enc$textbss$end+23
00141F08 | 5B | pop ebx |
00141F09 | 81C4 CC000000 | add esp,CC |
00141F0F | 3BEC | cmp ebp,esp |
00141F11 | E8 24F3FFFF | call cpp控制台程序.14123A |
00141F16 | 8BE5 | mov esp,ebp |
00141F18 | 5D | pop ebp |
;结束当前函数调用
00141F19 | C3 | ret |
下图是对fun函数调用时,堆栈栈底值含义的分析
开始分析功能代码部分
;将输入的字符串的地址入栈
00141EE5 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] | main.cpp:7, [ebp+8]:"11111111111111111111"
00141EE8 | 50 | push eax | eax:"11111111111111111111"
;将目标内存地址入栈,根据ebp-8我们可以清楚地知道,char arr[4]的空间位置
00141EE9 | 8D4D F8 | lea ecx,dword ptr ss:[ebp-8] |
00141EEC | 51 | push ecx |
;调用strcpy函数
00141EED | E8 C9F4FFFF | call cpp控制台程序.1413BB | strcpy
执行strcpy函数后,堆栈图如下:
我们可以清楚地看见,一个strcpy函数的执行,直接将原来的堆栈环境打乱
旧栈底地址值被更改
函数结束的回跳地址被更改(改成shellCOde的起始地址)
更改地址的堆栈也可以被随意更改(改写成shellCode具体内容
利用
思考:如果我们输入的内容,恰好将函数的回跳地址重新覆盖,覆盖成我们shellCode地址的开始地址,那么接下来继续执行的代码就是我们的恶意代码
问题:程序每次执行或操作系统重启,地址都会发生改变,如果将回跳地址写死,那么费力编写的恶意字符串也就变成了无用的字符串
输入恶意拼接的字符串,就从左侧的正常堆栈变成右侧被shellCode填充的堆栈
根据上边的问题,我们需要一个动态定位ShellCode地址的方法。让我们详细分析下函数调用恢复的过程。
;恢复寄存器现场
00141F04 | 58 | pop eax | eax:"11111111111111111111"
00141F05 | 5A | pop edx |
00141F06 | 5F | pop edi | edi:"111111111111"
00141F07 | 5E | pop esi | esi:__enc$textbss$end+23
;将旧栈底重新赋给栈底指针,但是旧栈底会在输入恶意字符串时被破坏
00141F08 | 5B | pop ebx |
;esp栈顶寄存器向高位移动,栈提升使用了sub esp,cc,栈顶恢复与此相反
;重点1:此时的esp指向了老ebp的存储空间
00141F09 | 81C4 CC000000 | add esp,CC |
00141F0F | 3BEC | cmp ebp,esp |
00141F11 | E8 24F3FFFF | call cpp控制台程序.14123A |
00141F16 | 8BE5 | mov esp,ebp |
00141F18 | 5D | pop ebp |
;最后利用ret将回跳地址pop并跳转
;重点2:此时ebp指向了shellCode的首地址
00141F19 | C3 | ret |
在考虑更改eip寄存器的常用指令:
ret
jmp系列指令
经过两次跳转,成功的将eip指向了shellCode位置,进行继续执行shellCode
利用跳转的原因
动态定位shellCode
shellCode复用
总结
尽量寻找系统文件dll中的jmp esp。因为DLL文件有个默认的载入地址,在没有其他dll占用着位置的时候,dll就会装入默认的位置,否则就会装载到随机的位置,并重新计算函数地址。而操作系统的dll是先于其他应用加载的,因此地址更变得概率不大,成功率高。
相同的系统,shellCode才有通用的可能。因为版本不同,dll加载位置可能不同,最终寻找到的jmp命令位置也不同
全文的操作前提是,能够将栈中的数据当成了代码来进行执行。如果不限制权限,那么就可以利用ret + jmp esp来进行跳转,达到更改eip的效果,进而执行shellcode
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。