定位API的原理:
所有的win_32程序都会加载ntdll.dll和kerner32.dll这两个最基础的动态链接库。如果想要在win_32平台下定位kernel32.dll中的API地址
1,首先通过段选择字FS在内存中找到当前的线程环境快TEB。
2,线程环境快偏移位置为0x30的地方存放着指向进程环境块PEB的指针。
3,进程环境块中偏移位置为0x0C的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装在的动态链接库的信息。
4,PEB_LDR_DATA结构体偏移位置为0x1C的地方存放着指向模块初始化链表的头指针InInitizationOrderModuleList.
5,模块初始化链表InInitizationOrderModuleList中按顺序存放着PE装入运行时初始化模块信息,第一个链表结点是ntdll.dll,第二个链表结点就是kernel32.dll。
6,找到属于kernel32.dll的结点后,在其基础上再偏移0x08就是kernel32.dll在内存中的加载基地址。
7,从kernel32.dll的加载基址算起,偏移0x3C的地方就是其PE头。
8,PE头偏移0x78的地方存放着指向函数导出表的指针。
9,导出表0x1C处的指针指向存储导出函数偏移地址(RVA)的列表
导出表偏移0x20处的指针指向存储导出函数函数名的列表
函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的RVA
获得RVA后,再加上前面已经得到的动态链接库的加载基址,就获得了所需API此刻在内存中的虚拟地址。
PE导出表结构如下:
代码实现流程
具体代码实现
编译环境VS2013+x86 测试环境server 2008 r2 x64
__asm
{
CLD
push 0x1e380a6a // 压入 MessageBoxA 字符串的hash
push 0x4fd18963 // 压入 ExitProcess 字符串的hash
push 0x0c917432; // 压入 LoadLibraryA 字符串的hash
mov esi, esp // esi = LoadLibraryA 字符串 hash 地址
lea edi, [esi - 0xc] // 用于存放后边找到的 三个函数地址
// 开辟一些栈空间
xor ebx, ebx
mov bh, 0x04
sub esp, ebx
// 压入一个指向 user32字符串的 栈指针
mov bx, 0x3233
push ebx
push 0x72657375
push esp
xor edx, edx
// 得到 kernel32.dll 的基地址
mov ebx, fs:[edx + 0x30] // FS得到当前线程环境块TEB TEB+0x30 是进程环境块 PEB
mov ecx, [ebx + 0x0c] // PEB+0x0c 是PEB_LDR_DATA结构体指针 存放这已经被进程加载的动态链接库的信息
mov ecx, [ecx + 0x1c] // PEB_LDR_DATA+0x1c 指向模块初始化链表的头指针 InInitalizationOrderModuleList
mov ecx, [ecx] // ecx指向 第二个节点 KERNELBASE.dll
mov ecx, [ecx] // ecx指向 第三个节点 kernel32.dll
mov ebp, [ecx + 0x08] // InInitializationOrderLinks+0x08 得到 DllBase 字段 即kernel32.dll基地址
find_lib_functions :
lodsd; // 将[esi]中的4字节 传到eax中
cmp eax, 0x1e380a6a; // 比较 MessageBoxA 字符串的hash值
jne find_functions
xchg eax, ebp // 记录当前hash值
call[edi - 0x8]; loadLibraryA
xchg eax, ebp // 还原当前hash值 并且把exa基地址更新为 user32.dll的基地址
find_functions :
pushad // 保存寄存器环境
mov eax, [ebp + 0x3c] // 得到 当前DLL PE头 循环依次是kernel32.dll 和 user32.dll
mov ecx, [ebp + eax + 0x78] // 得到 当前DLL 导出函数表 相对地址
add ecx, ebp // 得到 当前DLL 导出函数表 绝对地址
mov ebx, [ecx + 0x20] // 得到导出函数 名称表 相对地址
add ebx, ebp // 得到导出函数 名称表 绝对地址
xor edi, edi // 初始化计数器
next_function_loop :
inc edi // 函数计数器+1
mov esi, [ebx + edi * 4] // 得到 当前函数名的 相对偏移
add esi, ebp // 得到 当前函数名的 绝对地址
cdq; // 因为知道EAX<80000000 所以edx会被置0 相当于 xor edx,edx的功能
// 此指令只有一个字节 节约空间 算是一种优化
hash_loop: // 循环得到当前函数名的hash
movsx eax, byte ptr[esi] // 得到当前函数名称 第esi的一个字母
cmp al, ah // 比较到达函数名最后的0没有
jz compare_hash // 函数名hash 计算完毕后跳到 下一个流程
ror edx, 7 // 循环右移7位
add edx, eax // 累加得到hash
inc esi // 计数+1 得到函数名的下一个字母
jmp hash_loop // 循环跳到 hash_loop
compare_hash :
cmp edx, [esp + 0x1c] // 比较 目标函数名hash 和 当前函数名的hash
jnz next_function_loop // 如果 不等于 继续下一个函数名
mov ebx, [ecx + 0x24] // 得到 PE导出表中的 函数序号列表的 相对位置
add ebx, ebp // 得到 PE导出表中的 函数序号列表的 绝对位置
mov di, [ebx + 2 * edi] // 得到 PE导出表中的 当前函数的序号
mov ebx, [ecx + 0x1c] // 得到 PE导出表中的 函数地址列表的 相对位置
add ebx, ebp // 得到 PE导出表中的 函数地址列表的 绝对位置
add ebp, [ebx + 4 * edi] // 得到 PE导出表中的 当前函数的绝对地址
// 循环依次得到kernel32.dll中的 LoadLibraryA ExitProcess
// 和user32.dll中的 MessageBoxA
xchg eax, ebp // 把函数地址放入eax中
pop edi // pushad中最后一个压入的是edi 正好是开始预留 用于存放的三个函数地址 的栈空间
stosd // 把找到函数地址出入 edi对应的栈空间
push edi // 继续压栈 平衡栈
popad // 还原环境
cmp eax, 0x1e380a6a // 比较是否是 MessageBoxA 函数 如果是说明全部函数已经找齐 可以调用函数执行功能
jne find_lib_functions
function_call :
xor ebx, ebx
push ebx // 压入字符串结尾的0
push 0x74736577
push 0x6c696166 // 压入字符串 failwest
mov eax, esp // 得到字符串 failwest 地址
push ebx
push eax
push eax
push ebx
call[edi - 0x04] // 调用 MessageBoxA(0, "failwest", "failwest", 0);
push ebx
call[edi - 0x08] // 调用 ExitProcess(0);
nop
nop
nop
nop
}
参考《0day安全:软件漏洞分析技术》