一、序言
结合之前所讲的内容,我们将如下代码的功能转写为shellcode的实现方式:
#include <windows.h>
int main()
{
CreateFileA("D:\\1.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
MessageBox(NULL, L"Hello World", L"imbyter.com", MB_OK);
return 0;
}
上述代码执行的功能为:
- 创建文件D:\1.txt;
- 弹框提示;
我们将如上功能转用shellcode的方式来实现,大概可以分为以下几部:
二、编译链接属性设置
为达到最好shellcode执行效果,需要修改主函数入口点、关闭缓冲区安全检查、设置系统兼容性、修改运行库、关闭生成清单,以及关闭调试信息。详见Shellcode项目属性规范。
三、获取kernel32基址
按照则 原Kernel32基址的获取 的方式,实现对kernel32基址的获取。
四、函数地址动态获取
按照原则 函数地址动态获取 的方式,实现用GetProcAddress动态获取函数地址。
五、调整源码中函数位置
将CreateFileA于MessageBox以动态调用的方式实现,并且要注意,修改函数名后的入口函数EntryMain,要放在整个cpp文件中的最头部。
六、最终实现代码
#include <windows.h>
#include <Winternl.h>
#pragma optimize("", off )
#pragma comment(linker,"/entry:EntryMain")
HMODULE GetKernel32BaseAddress();
FARPROC _GetPorcAddress();
int EntryMain()
{
// 获取GetPorcAddress函数地址
typedef FARPROC(WINAPI* FN_GetProcAddress)(__in HMODULE hModule, __in LPCSTR lpProcName);
FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)_GetPorcAddress();
if (fn_GetProcAddress)
{
// 获取LoadLibraryA函数地址
char szLoadLibraryA[] = { 'L','o','a','d','L','i','b','r','a','r','y','A',0 };
typedef HMODULE(WINAPI* FN_LoadLibraryA)(__in LPCSTR lpLibFileName);
FN_LoadLibraryA fn_LoadLibraryA = (FN_LoadLibraryA)fn_GetProcAddress(GetKernel32BaseAddress(), szLoadLibraryA);
if (fn_LoadLibraryA)
{
// 获取CreateFileA函数地址
char szCreateFileA[] = { 'C','r','e','a','t','e','F','i','l','e','A',0 };
typedef HANDLE(WINAPI* FN_CreateFileA)(
_In_ LPCSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
FN_CreateFileA fn_CreateFileA = (FN_CreateFileA)fn_GetProcAddress(GetKernel32BaseAddress(), szCreateFileA);
// 执行CreateFileA
char szFilePath[] = { 'D',':','\\','1','.','t','x','t',0 };
fn_CreateFileA(szFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
// 获取MessageBoxA函数地址
char szUser32[] = { 'U','s','e','r','3','2','.','d','l','l',0 };
char szMessageBoxA[] = { 'M','e','s','s','a','g','e','B','o','x','A',0 };
typedef int (WINAPI* FN_MessageBoxA)(__in_opt HWND hWnd, __in_opt LPCSTR lpText, __in_opt LPCSTR lpCaption, __in UINT uType);
FN_MessageBoxA fn_MessageBoxA = (FN_MessageBoxA)fn_GetProcAddress(fn_LoadLibraryA(szUser32), szMessageBoxA);
// 执行MessageBoxA
char szCaption[] = { 'i','m','b','y','t','e','r','.','c','o','m',0 };
char szText[] = { 'H','e','l','l','o',' ','W','o','r','l','d', 0 };
fn_MessageBoxA(0, szText, szCaption, MB_OK | MB_ICONINFORMATION);
}
}
return 0;
}
// 获取kernel32基址
HMODULE GetKernel32BaseAddress()
{
HMODULE hKernel32 = NULL;
// 用户保存模块名
WCHAR wszModuleName[MAX_PATH];
#ifdef _WIN64 // 64位PEB偏移为0x60
PPEB lpPeb = (PPEB)__readgsqword(0x60);
#else // 32位PEB偏移为0x30
PPEB lpPeb = (PPEB)__readfsdword(0x30);
#endif
PLIST_ENTRY pListHead = &lpPeb->Ldr->InMemoryOrderModuleList;
PLIST_ENTRY pListData = pListHead->Flink;
// 遍历所有模块
while (pListData != pListHead)
{
PLDR_DATA_TABLE_ENTRY pLDRData = CONTAINING_RECORD(pListData, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
DWORD dwLen = pLDRData->FullDllName.Length / 2;
if (dwLen > 12) // 12 是"kernel32.dll"的长度,获取到的完整路径肯定要比模块名长
{
// 从获取到的模块完整路径中提取模块名
for (size_t i = 0; i < 12; i++)
{
wszModuleName[11 - i] = pLDRData->FullDllName.Buffer[dwLen - 1 - i];
}
// 最终要获取的目标模块名("kernel32.dll"),逐个字节比较,包含大小写。
if ((wszModuleName[0] == 'k' || wszModuleName[0] == 'K') &&
(wszModuleName[1] == 'e' || wszModuleName[1] == 'E') &&
(wszModuleName[2] == 'r' || wszModuleName[2] == 'R') &&
(wszModuleName[3] == 'n' || wszModuleName[3] == 'N') &&
(wszModuleName[4] == 'e' || wszModuleName[4] == 'E') &&
(wszModuleName[5] == 'l' || wszModuleName[5] == 'L') &&
(wszModuleName[6] == '3') &&
(wszModuleName[7] == '2') &&
(wszModuleName[8] == '.') &&
(wszModuleName[9] == 'd' || wszModuleName[9] == 'D') &&
(wszModuleName[10] == 'l' || wszModuleName[10] == 'L') &&
(wszModuleName[11] == 'l' || wszModuleName[11] == 'L'))
{
hKernel32 = (HMODULE)pLDRData->DllBase;
break;
}
}
pListData = pListData->Flink;
}
return hKernel32;
}
// 获取GetPorcAddress函数地址
FARPROC _GetPorcAddress()
{
// 保存最终结果
FARPROC pGetPorcAddress = NULL;
// kernel32基址
HMODULE hKernel32 = GetKernel32BaseAddress();
if (!hKernel32)
{
return NULL;
}
PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)hKernel32;
PIMAGE_NT_HEADERS lpNTHeader = (PIMAGE_NT_HEADERS)((unsigned char*)hKernel32 + lpDosHeader->e_lfanew);
// 模块有效性验证
if (!lpNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
{
return NULL;
}
if (!lpNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
return NULL;
}
// 通过导出表中的导出函数名,定位"GetProcAddress"的位置
PIMAGE_EXPORT_DIRECTORY lpExports = (PIMAGE_EXPORT_DIRECTORY)((unsigned char*)hKernel32 + lpNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD lpdwFunName = (PDWORD)((unsigned char*)hKernel32 + lpExports->AddressOfNames);
PWORD lpdwOrd = (PWORD)((unsigned char*)hKernel32 + lpExports->AddressOfNameOrdinals);
PDWORD lpdwFunAddr = (PDWORD)((unsigned char*)hKernel32 + lpExports->AddressOfFunctions);
for (DWORD dwLoop = 0; dwLoop <= lpExports->NumberOfNames - 1; dwLoop++)
{
char* pFunName = (char*)(lpdwFunName[dwLoop] + (unsigned char*)hKernel32);
// 比较函数名
if (
pFunName[0] == 'G' &&
pFunName[1] == 'e' &&
pFunName[2] == 't' &&
pFunName[3] == 'P' &&
pFunName[4] == 'r' &&
pFunName[5] == 'o' &&
pFunName[6] == 'c' &&
pFunName[7] == 'A' &&
pFunName[8] == 'd' &&
pFunName[9] == 'd' &&
pFunName[10] == 'r' &&
pFunName[11] == 'e' &&
pFunName[12] == 's' &&
pFunName[13] == 's'
)
{
pGetPorcAddress = (FARPROC)(lpdwFunAddr[lpdwOrd[dwLoop]] + (unsigned char*)hKernel32);
break;
}
}
return pGetPorcAddress;
}
注意:在我们这一节所讲的shellcode编写方法中,入口函数EntryMain一定要放到所有函数的最前面。
在VS平台对代码进行编译的时,会按照一个cpp文件从上到下的顺序对各函数进行编译,生成的exe文件中函数所在的顺序和cpp文件函数的对应顺序基本一致。
我们DIE(或其他二进制编辑器)打开上述代码生成的exe文件,点击“入口点”处“>”:
然后在新弹框显示的界面左边,发现地址为0200,表示代码入口点的文件偏移为x0200。重新使用二进制编辑器(WinHex)打开exe,从0x200处开始复制,一直到大量连续为0的开始处:
然后右键选择“编辑”->“复制选快”->“至新文件”,保存为新文件,这个新文件就是我们最终的shellcode文件。
七、测试shellcode
shellcode都是运行在其他进程的内存空间中,我们测试上面得到的shellcode功能是否正常,可以尝试将上述shellcode运行在我们自己的测试程序中。
-
确认上述shellcode最终实现代码的编译版本,要明确是x86版本,还是x64版本。此处我的编译方式为x86。
-
重新编写一个和上述shellcode编译类型相同的正常程序。比如一个如下代码的x86方式编译的exe程序:
#include <iostream> int main() { std::cout << "Hello imbyter.com!\n"; }
-
用调试工具(x64dbg)打开第2步生成的exe文件,然后选择菜单中“调试”->“运行到用户代码”,让程序运行到自己的代码空间:
-
打开我们前面得到的shellcode文件,全选内容并以“十六进制数值”的方式进行复制:
-
将上述复制的数据粘贴到x64dbg下的测试程序。如图在当前执行的语句上右键“二进制”->“粘贴时忽略大小”:
-
此时会发现当前代码已经全部粘贴为shellcode的二进制数据,然后点击菜单“调试”->“运行”(或者左上角运行的快捷图标):
-
如果发现有上述弹框提示,并且生成了D:\1.txt,表示shellcode执行成功。如果未有结果,需要检查是否是由于shellcode的版本和测试exe程序的版本不一致或其他什么问题,可在下方留言。
如果有任何问题,可以在我们的知识社群中提问和沟通交流:
一个人走得再快,不如一群人走得更远!🤜🤛