简易 ShellCode 虽然可以正常被执行,但是还存在很多的问题,因为上次所编写的 ShellCode 采用了硬编址的方式来调用相应API函数的,那么就会存在一个很大的缺陷,如果操作系统的版本不统一就会存在调用函数失败甚至是软件卡死的现象,下面我们通过编写一些定位程序,让 ShellCode 能够动态定位我们所需要的API函数地址,从而解决上节课中 ShellCode 的通用性问题。
首先我们需要通过汇编的方式来实现动态定位 Kernel32 中的基地址,你或许会有个疑问? 为什么要查找 Kernel32.dll 的地址而不是 User32.dll,这是因为我们最终的目的是调用 MessageBox 这个函数,而该函数位于 User32.dll 这个动态链接库里,默认情况下是无法直接调用的,为了能够调用这个函数,我们就需要调用 LoadLibraryA 函数来加载 User32.dll 这个模块,而 LoadLibraryA 又位于 kernel32.dll 中。
恰巧的是 Kernel32.dll 这个模块只要是 PE 文件都会默认被加载 ,因此我们只需要找到 LoadLibraryA 函数,即可加载任意的动态链接库,并调用任意的函数啦。
由于我们需要动态获取 LoadLibraryA() 以及 ExitProcess() 这两个函数的地址,而这两个函数又是存在于 kernel32.dll 中的,因此这里需要先找到 kernel32.dll 的地址,然后通过对其进行解析,从而查找那两个函数的地址。
这里有一个公式,可以动态的查找到 Kernel32.dll 的地址,如下:
既然有了固定的公式,接下我们就使用WinDBG调试器来手工完成对 Kernel32.dll 地址的定位:
1.首先我们运行 WinDBG调试器,然后按下【Ctrl + K】选择文件(File) -> 选择内核调试(Kernel Debug) -> 本地调试(Local) 点击确定。打开后会看到如下界面,直接在最底部输入两条命令,来加载一下符号文件,否则无法进行查看。
2.通过段选择字FS在内存中找到当前的线程环境块TEB。这里可以利用本地调试,输入!teb 指令:
线程环境块偏移位置为 0x30
的地方存放着指向进程环境块PEB的指针。结合上图可见,PEB的地址为0x7ffd7000
。
3.进程环境块中偏移位置为 0x0c
的地方存放着指向 PEB_LDR_DATA
结构体的指针,其中存放着已经被进程装载的动态链接库的信息。
4.接着 PEB_LDR_DATA
结构体偏移位置为 0x1c
的地方存放着指向模块初始化链表的头指针 InInitializationOrderModuleList
。
5.模块初始化链表 InInitializationOrderModuleList
中按顺序存放着PE装入运行时初始化模块的信息,第一个链表节点是 ntdll.dll
,第二个链表结点就是kernel32.dll
。比如可以先看看
InInitializationOrderModuleList
中的内容:
上图中的 0x00191f28
保存的是第一个链节点的指针,解析一下这个结点,可发现如下地址:
上图中 0x7c92000
为 ntdll.dll 的基地址,而 0x00191fd0
则是下一个模块的指针,继续跟随 0x00191fd0
。
观察发现第二个节点偏移 0x08
个字节正是 kernel32.dll的基地址,其地址为 0x7c800000
。最有我们来验证一下:
6.通过上方的调试我们可得到公式,接着通过编写一段汇编代码来实现自动的遍历出 kernel32.dl
的基址。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kerbcli.lib
assume fs:nothing
.code
main PROC
xor eax,eax
xor edx,edx
mov eax,fs:[30h] ; 得到PEB结构地址
mov eax,[eax + 0ch] ; 得到PEB_LDR_DATA结构地址
mov esi,[eax + 1ch]
lodsd ; 得到KERNEL32.DLL所在LDR_MODULE结构的
; mov eax,[eax] ; Windows 7 以上要将这里打开
mov edx,[eax + 8h] ; 得到BaseAddress,既Kernel32.dll基址
ret
main ENDP
END main
接着需要对我们所用到的字符串进行 hash 压缩处理,为啥要压缩? 原因是如果直接将函数名压栈的话,我们就需要提供更多的空间来存储 ShellCode 代码,为了能够让我们编写的 ShellCode 代码更加的短小精悍,所以我们将要对字符串进行hash处理,将字符串压缩为一个十六进制数,这样只需要比较二者hash值就能够判断目标函数,尽管这样会引入额外的hash算法,但是却可以节省出存储函数名字的空间。
如下代码是使用 Win32 汇编语言的实现过程,并在 MASM 上正常编译,汇编版字符串转换Hash值。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
include msvcrt.inc
includelib kernel32.lib
includelib msvcrt.lib
.data
data db "MessageBoxA",0h
Fomat db "0x%x",0
.code
main PROC
xor eax,eax ; 清空eax寄存器
xor edx,edx ; 清空edx寄存器
lea esi,data ; 取出字符串地址
loops:
movsx eax,byte ptr[esi] ; 每次取出一个字符放入eax中
cmp al,ah ; 验证eax是否为0x0即结束符
jz nops ; 为0则说明计算完毕跳转到nops
ror edx,7 ; 不为零,则进行循环右移7位
add edx,eax ; 将循环右移的值不断累加
inc esi ; esi自增,用于读取下一个字符
jmp loops ; 循环执行
nops:
mov eax,edx ; 结果存在eax里面
invoke crt_printf,addr Fomat,eax
ret
main ENDP
END main
当然也可以使用C语言来实现这个转换的过程,这里使用的是VS Express 2013可正常编译通过,如下代码:
#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
DWORD digest = 0;
while (*fun_name)
{
digest = ((digest << 25) | (digest >> 7));
digest += *fun_name;
fun_name++;
}
return digest;
}
int main()
{
DWORD hash;
hash = GetHash("MessageBoxA");
printf("0x%.8x\n", hash);
getchar();
return 0;
}
我们通过传入不同的函数名称,让其计算出我们所需要计算的三个函数的hash值,其计算结果如下所示:
MessageBoxA 0x1e380a6a
ExitProcess 0x4fd18963
LoadLibraryA 0x0c917432
在开头的部分我们通过 WinDBG
调试器已经找到了 Kernel32.dll
这个动态链接库的基地址,而Dll文件本质上也是PE文件,在Dll文件中存在一个导出表,其内部记录着该Dll的导出函数。接着我们需要对Dll文件的导出表进行遍历,不断地搜索,从而找到我们所需要的API函数。
同样的,这里有一个定式,可以通过该定式获取到指定的导出表。
函数的 RVA 地址和名字按照顺序存放在上述两个列表中,我们可以在列表定位任意函数的RVA地址,通过与动态链接库的基地址相加得到其真实的VA,而计算的地址就是我们最终在 ShellCode 中调用时需要的地址。
pushad ; 保护所有寄存器中的内容
mov eax,[ebp+0x3C] ; 指向PE文件头
mov ecx,[ebp+eax+0x78] ; 导出表的指针
add ecx,ebp
mov ebx,[ecx+0x20] ; 导出函数的名字列表
add ebx,ebp
xor edi,edi ; 用edi寄存器作为索引
; ------- 循环读取导出表函数
next_loop:
inc edi ; 不断自增索引
mov esi,[ebx+edi*4] ; 从列表数组中读取
add esi,ebp ; esi保存的是函数名称所在地址
cdq
完整代码如下,下方代码是一个定式,这里就只做了翻译,使用编译器编译如下代码,运行后会弹出一个提示框hello lyshark
说明我们成功了。
#include <stdio.h>
#include <windows.h>
int main()
{
__asm {
// ===将索要调用的函数hash值入栈保存
CLD // 清空标志位DF
push 0x1E380A6A // 压入MessageBoxA-->user32.dll
push 0x4FD18963 // 压入ExitProcess-->kernel32.dll
push 0x0C917432 // 压入LoadLibraryA-->kernel32.dll
mov esi,esp // 指向堆栈中存放LoadLibraryA的地址
lea edi,[esi-0xc] // 后面会利用edi的值来调用不同的函数
// ===开辟内存空间,这里是堆栈空间
xor ebx,ebx
mov bh,0x04 // ebx为0x400
sub esp,ebx // 开辟0x400大小的空间
// ===将user32.dll入栈
mov bx,0x3233
push ebx // 压入字符'32'
push 0x72657375 // 压入字符 'user'
push esp
xor edx,edx // edx=0
// ===查找kernel32.dll的基地址
mov ebx,fs:[edx+0x30] // [TEB+0x30] -> PEB
mov ecx,[ebx+0xC] // [PEB+0xC] -> PEB_LDR_DATA
mov ecx,[ecx+0x1C] // [PEB_LDR_DATA+0x1C] -> InInitializationOrderModuleList
mov ecx,[ecx] // 进入链表第一个就是ntdll.dll
mov ebp,[ecx+0x8] //ebp = kernel32.dll 的基地址
// === hash 的查找相关
find_lib_functions:
lodsd // eax=[ds*10H+esi],读出来是LoadLibraryA的Hash
cmp eax,0x1E380A6A // 与MessageBoxA的Hash进行比较
jne find_functions // 如果不相等则继续查找
xchg eax,ebp
call [edi-0x8]
xchg eax,ebp
// ===在PE文件中查找相应的API函数
find_functions:
pushad
mov eax,[ebp+0x3C] // 指向PE头
mov ecx,[ebp+eax+0x78] // 导出表的指针
add ecx,ebp // ecx=0x78C00000+0x262c
mov ebx,[ecx+0x20] // 导出函数的名字列表
add ebx,ebp // ebx=0x78C00000+0x353C
xor edi,edi // 清空edi中的内容,用作索引
// ===循环读取导出表函数
next_function_loop:
inc edi // edi作为索引,自动递增
mov esi,[ebx+edi*4] // 从列表数组中读取
add esi,ebp // esi保存的是函数名称所在的地址
cdq
// ===hash值的运算过程
hash_loop:
movsx eax,byte ptr[esi] // 每次读取一个字节放入eax
cmp al,ah // eax和0做比较,即结束符
jz compare_hash // hash计算完毕跳转
ror edx,7
add edx,eax
inc esi
jmp hash_loop
// ===hash值的比较函数
compare_hash:
cmp edx,[esp+0x1C]
jnz next_function_loop // 比较不成功则查找下一个函数
mov ebx,[ecx+0x24] // ebx=序数表的相对偏移量
add ebx,ebp // ebx=序数表的绝对地址
mov di,[ebx+2*edi] // di=匹配函数的序数
mov ebx,[ecx+0x1C] // ebx=地址表的相对偏移量
add ebx,ebp // ebx=地址表的绝对地址
add ebp,[ebx+4*edi] // 添加到EBP(模块地址库)
xchg eax,ebp // 将func addr移到eax中
pop edi // edi是pushad中最后一个堆栈
stosd
push edi
popad
cmp eax,0x1e380a6a // 与MessageBox的hash值比较
jne find_lib_functions
// ===下方的代码,就是我们的弹窗
function_call:
xor ebx,ebx // 清空eb寄存器
push ebx // 截断字符串0
push 0x2020206b
push 0x72616873
push 0x796c206f
push 0x6c6c6568 // push hello lyshark
mov eax,esp
push ebx // push 0
push eax // push "hello lyshark"
push eax // push "hello lyshark"
push ebx // push 0
call [edi-0x04] // call MessageBoxA
push ebx // push 0
call [edi-0x08] // call ExitProcess
}
return 0;
}
编译生成可执行文件以后,我们使用OD打开程序,并手工寻找到程序的 OEP 提取出 ShellCode 的机器码,打开UltraEdit 工具,粘贴机器码,然后按下【Alt + C】进入列模式,编辑只保留机器码即可。