文章概览

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

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。