缓冲区溢出分析第05课:编写通用的ShellCode

前言

我们这次的实验所要研究的是如何编写通用的ShellCode。可能大家会有疑惑,我们上次所编写的ShellCode已经能够很好地完成任务,哪里不通用了呢?其实这就是因为我们上次所编写的ShellCode,是采用“硬编址”的方式来调用相应API函数的。也就是说,我们需要首先获取所要使用函数的地址,然后将该地址写入ShellCode,从而实现调用。这种方式对于所有的函数,通用性都是相当地差,试想,如果系统的版本变了,那么很多函数的地址往往都会发生变化,那么调用肯定就会失败了。所以本次的课程主要讨论如何在ShellCode中动态地寻找相关API函数的地址,从而解决通用性的问题。

计算函数名称的hash值

这里可以首先总结一下我们将要用到的函数。

首先为了显示对话框,需要使用MessageBoxA这个函数,它位于user32.dll里面。为了使用这个动态链接库,还需要使用LoadLibraryA来读取这个DLL文件,而LoadLibraryA又位于kernel32.dll中。因为所有的Win32程序都会自动加载kernel32.dll,因此这里我们无需再使用LoadLibraryA来加载kernel32.dll。最后为了正常退出程序,还需要使用ExitProcess,它同样位于kernel32.dll里面。

由于ShellCode最终是要放进缓冲区的,为了使得ShellCode更加通用,能被大多数缓冲区容纳,我们总是希望ShellCode尽可能地短小精悍。因此我们在系统中搜索函数名的时候,一般情况下并不会使用诸如“LoadLibraryA”这么长的字符串直接进行比较查找。而是首先会对函数名进行hash运算,而在系统中搜索所要使用的函数时,也会先对系统中的函数进行hash运算,这样只需要比较二者的hash值就能够判定目标函数是不是我们想要查找的了。尽管这样会引入额外的hash算法,,但是却可以节省出存储函数名字的空间。

计算以上三个API函数的hash值的程序如下:

#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("The hash of MessageBoxA is 0x%.8x\n", hash);hash = GetHash("ExitProcess");printf("The hash of ExitProcess is 0x%.8x\n", hash);hash = GetHash("LoadLibraryA");printf("The hash of LoadLibraryA is 0x%.8x\n", hash);getchar();return 0;} 运行结果如下:

图1

可见,通过hash算法,我们能够将任意长度的函数名称变成四个字节(DWORD)的长度。

这里给大家简单分析一下上述hash值的计算方法。假设现在有一个函数,名为“AB”,然后调用GetHash函数:

hash =GetHash("AB");

进入GetHash函数,它会将函数名称中的字符一个一个地分别取出进行计算,有几个字符就循环计算几次。首先是第一次循环,取出字符“A”,然后有:

digest= ((digest << 25) | (digest >> 7 ));

这里由于digest在上面被赋值为0,且为DWORD类型,因此这里不管怎么计算,它的值都是0。然后计算:

digest+= *fun_name;

此时的digest是0,*fun_name保存的是第一个字符“A”,它们相加也就是ASCII码值的相加,结果就是digest的值为“00000000 0000000000000000 01000001”。然后执行语句:

fun_name++;

令指针指向第二个字符“B”,从而进入第二次循环。首先计算:

digest= ((digest << 25) | (digest >> 7 ));

首先将digest左移25位,即“10000010 0000000000000000 00000000”,然后将其右移7位,即“10000010 00000000 00000000 00000000”,然后江这两个值做“或”运算,则digest的值为“10000010 0000000000000000 00000000”。事实上,上述语句的目的是实现digest的循环右移7位(或循环左移25位),由于C语言没有直接实现循环移位的运算符号,因此只能通过这种方式运算。然后计算:

digest+= *fun_name;

也就是将digest的值加上“B”的ASCII码值,结果为“1000001000000000 00000000 01000010”,这也就是最终的运算结果,以十六进制显示就是0x82000042。

下面就可以编写汇编代码,首先是让函数的hash值入栈:

push 0x1e380a6a ; MessageBoxA的hash值

push 0x4fd18963 ; ExitProcess的hash值

push 0x0c917432 ; LoadLibraryA的hash值

mov esi,esp ; esi保存的是栈顶第一个函数,即LoadLibraryA的hash值

然后编写用于计算hash值的代码:

hash_loop:

movsx eax,byte ptr[esi] // 每次取出一个字符放入eax中

cmp al,ah // 验证eax是否为0x0,即结束符

jz compare_hash // 如果上述结果为零,说明hash值计算完毕,则进行hash值的比较

ror edx,7 // 如果cmp的结果不为零,则进行循环右移7位的操作

add edx,eax // 将循环右移的值不断累加

inc esi // esi自增,用于读取下一个字符

jmp hash_loop // 跳到hash_loop的位置继续计算

这样通过循环,就能够计算出函数名称的hash值,请大家注意汇编的这种写法。

获取kernel32.dll的地址等待故人的归来。山上的树,大多数是松树比较突出。

缓冲区溢出分析第05课:编写通用的ShellCode

相关文章:

你感兴趣的文章:

标签云: