前言
好久没有更新文章了,不管期末咋样,也是进入暑假了。这几天看了一些关于编写shellcode的内容,自己也来实操一下😁😁😁
目标
编写shellcode,通过加载器,实现一个弹窗的功能,和世界打个招呼
**合法研究,严禁越界;技术向善,从我做起**
前期分析
在shellcode制作过程中,我们需要重点关注GetProcAddress以及LoadLibraryA这两个API。但这两个API已经被杀软重点标记了,所以我们尝试其它方法调用这两个API。
下面在Windbg中推演过程:
随机运行一个程序,开始调试,运行!process 0 0 <具体程序>,进入该进程上下文

.process /i ffffe688ce695080,切换到该进程的上下文

查看当前进程!process

我们找到主线程的Teb(线程环境模块)的地址,查看其具体结构

我们找到PEB(进程环境模块)的地址,查看其具体结构

找到Ldr(Loader Data(加载器数据))的地址,查看其结构

找到这里的InLoadOrderModuleList(加载模块列表)字段,这个字段是一个双向链表,其中记录了当前进程加载的模块列表。kernel32.dll一般情况下会在第三个模块,顺着这个双向链表往下查看。

找到了kernel32.dll的基址,开始解析其文件结构

通过解析其DOS头,定位到e_lfanew字段,这个字段记录的NT头的RVA,查看NT头结构

这里我们找到了NT可选头的位置,继续查看其结构

找到目录表,查看其第一个结构

我们对kernel32.dll的目录表进行分析

第一个结构体中记录的就是导出表的RVA,分析其内容

这里 存在四个重要的字段:
NumberOfFunctions:导出表中函数的数量 AddressOfFunctions:具体函数RVA的RVA AddressOfNames:函数名字的RVA AddressOfNameOrdinals:函数索引的RVA

那我们可以遍历AddressOfNames,找到关键函数的索引,在AddressOfFunctions中找到具体函数的RVA,即可调用具体函数。
复盘
我们的调用链为:teb -> peb -> Ldr -> InLoadOrderModuleList ->kernel32基址 -> 导出表 -> LoadLibraryA/GetProcAddress地址
具体代码实现
#include <windows.h>
#include <stdio.h>
#include <intrin.h>
int main() {
ULONG_PTR peb = __readgsqword(0x60);
ULONG_PTR ldr = *(ULONG_PTR*)(peb + 0x18);
ULONG_PTR head = ldr + 0x10;
ULONG_PTR entry = *(ULONG_PTR*)(head);
ULONG_PTR kernelBase = 0;
while (entry != head) {
USHORT nameLen = *(USHORT*)(entry + 0x58);
if (nameLen == 24) {
PWSTR name = *(PWSTR*)(entry + 0x60);
// 大小写不敏感比较: (c | 0x20) 将大写 ASCII 转小写
if ((name[0] | 0x20) == L'k' &&
(name[1] | 0x20) == L'e' &&
(name[2] | 0x20) == L'r' &&
(name[3] | 0x20) == L'n' &&
(name[4] | 0x20) == L'e' &&
(name[5] | 0x20) == L'l' &&
(name[6] ) == L'3' && // 数字不变
(name[7] ) == L'2' &&
(name[8] ) == L'.' && // 点号不变
(name[9] | 0x20) == L'd' &&
(name[10] | 0x20) == L'l' &&
(name[11] | 0x20) == L'l') {
kernelBase = *(ULONG_PTR*)(entry + 0x30); // DllBase
break;
}
}
entry = *(ULONG_PTR*)(entry); // Flink → 下一个节点
}
if (!kernelBase) {
printf("[-] kernel32.dll not found\n");
return1;
}
printf("[+] kernel32.dll base: 0x%p\n", (PVOID)kernelBase);
ULONG e_lfanew = *(ULONG*)(kernelBase + 0x3C);
ULONG exportRVA = *(ULONG*)(kernelBase + e_lfanew + 0x18 + 0x70);
ULONG_PTR expBase = kernelBase + exportRVA;
ULONG numNames = *(ULONG*)(expBase + 0x18); // NumberOfNames
ULONG funcsRVA = *(ULONG*)(expBase + 0x1C); // AddressOfFunctions
ULONG namesRVA = *(ULONG*)(expBase + 0x20); // AddressOfNames
ULONG ordsRVA = *(ULONG*)(expBase + 0x24); // AddressOfNameOrdinals
ULONG_PTR funcs = kernelBase + funcsRVA; // DWORD 数组: 函数 RVA
ULONG_PTR names = kernelBase + namesRVA; // DWORD 数组: 名称 RVA
ULONG_PTR ords = kernelBase + ordsRVA; // WORD 数组: 序号
ULONG_PTR pGetProcAddress = 0;
ULONG_PTR pLoadLibraryA = 0;
for (ULONG i = 0; i < numNames; i++) {
PCHAR fn = (PCHAR)(kernelBase + *(ULONG*)(names + i * 4));
USHORT ord = *(USHORT*)(ords + i * 2);
ULONG rva = *(ULONG*)(funcs + ord * 4);
if (fn[0]=='G' && fn[1]=='e' && fn[2]=='t' && fn[3]=='P' &&
fn[4]=='r' && fn[5]=='o' && fn[6]=='c' && fn[7]=='A' &&
fn[8]=='d' && fn[9]=='d' && fn[10]=='r' && fn[11]=='e' &&
fn[12]=='s' && fn[13]=='s' && fn[14]=='\0') {
pGetProcAddress = kernelBase + rva;
}
if (fn[0]=='L' && fn[1]=='o' && fn[2]=='a' && fn[3]=='d' &&
fn[4]=='L' && fn[5]=='i' && fn[6]=='b' && fn[7]=='r' &&
fn[8]=='a' && fn[9]=='r' && fn[10]=='y' && fn[11]=='A' &&
fn[12]=='\0') {
pLoadLibraryA = kernelBase + rva;
}
}
printf("[+] GetProcAddress: 0x%p\n", (PVOID)pGetProcAddress);
printf("[+] LoadLibraryA: 0x%p\n", (PVOID)pLoadLibraryA);
if (!pGetProcAddress || !pLoadLibraryA) {
printf("[-] Failed to resolve functions\n");
return1;
}
typedef HMODULE (WINAPI *fnLoadLibraryA_t)(LPCSTR);
typedef FARPROC (WINAPI *fnGetProcAddress_t)(HMODULE, LPCSTR);
typedef int (WINAPI *fnMessageBoxA_t)(HWND, LPCSTR, LPCSTR, UINT);
fnLoadLibraryA fnLoadLibraryA = (fnLoadLibraryA)pLoadLibraryA;
fnGetProcAddress fnGetProcAddress = (fnGetProcAddress)pGetProcAddress;
HMODULE hUser32 = fnLoadLibraryA("user32.dll");
if (!hUser32) {
printf("[-] Failed to load user32.dll\n");
return1;
}
printf("[+] user32.dll base: 0x%p\n", hUser32);
fnMessageBoxA_t fnMessageBoxA = (fnMessageBoxA_t)fnGetProcAddress(hUser32, "MessageBoxA");
if (!fnMessageBoxA) {
printf("[-] Failed to resolve MessageBoxA\n");
return1;
}
printf("[+] MessageBoxA: 0x%p\n", fnMessageBoxA);
fnMessageBoxA(NULL, "hello world", "Message", MB_OK);
return0;
}

提取shellcode
上述过程中制作的是一个完整的PE文件,但shellcode只是一段字节码,所以我们需要对编译过程做出更改。主要是一下两点:
该变程序入口点,正常程序启动是通过
_start函数会做CRT初始化,再调用main函数,但shellcode是没有这些初始化过程,所以需要更换程序入口点。为了方便提取shellcode,我们需要合并段
其它还包括:关Spectre缓解,关闭安全保护... 具体步骤如下:




代码也要做出如下修改:
#include <windows.h>
#include <intrin.h>
#pragma code_seg("shell")
#pragma comment(linker,"/entry:shellcode")
int shellcode() {
ULONG_PTR peb = __readgsqword(0x60);
ULONG_PTR ldr = *(ULONG_PTR*)(peb + 0x18);
ULONG_PTR head = ldr + 0x10;
ULONG_PTR entry = *(ULONG_PTR*)(head); // Flink → 第一个 LDR_DATA_TABLE_ENTRY
ULONG_PTR kernelBase = 0;
while (entry != head) {
WORD nameLen = *(WORD*)(entry + 0x58);
if (nameLen == 24) {
PWSTR name = *(PWSTR*)(entry + 0x60);
if ((name[0] | 0x20) == L'k' &&
(name[1] | 0x20) == L'e' &&
(name[2] | 0x20) == L'r' &&
(name[3] | 0x20) == L'n' &&
(name[4] | 0x20) == L'e' &&
(name[5] | 0x20) == L'l' &&
(name[6]) == L'3' && // 数字不变
(name[7]) == L'2' &&
(name[8]) == L'.' && // 点号不变
(name[9] | 0x20) == L'd' &&
(name[10] | 0x20) == L'l' &&
(name[11] | 0x20) == L'l') {
kernelBase = *(ULONG_PTR*)(entry + 0x30); // DllBase
break;
}
}
entry = *(ULONG_PTR*)(entry); // Flink → 下一个节点
}
if (!kernelBase) {
return1;
}
ULONG e_lfanew = *(ULONG*)(kernelBase + 0x3C);
ULONG exportRVA = *(ULONG*)(kernelBase + e_lfanew + 0x18 + 0x70);
ULONG_PTR expBase = kernelBase + exportRVA;
ULONG numNames = *(ULONG*)(expBase + 0x18); // NumberOfNames
ULONG funcsRVA = *(ULONG*)(expBase + 0x1C); // AddressOfFunctions
ULONG namesRVA = *(ULONG*)(expBase + 0x20); // AddressOfNames
ULONG ordsRVA = *(ULONG*)(expBase + 0x24); // AddressOfNameOrdinals
ULONG_PTR funcs = kernelBase + funcsRVA; // DWORD 数组: 函数 RVA
ULONG_PTR names = kernelBase + namesRVA; // DWORD 数组: 名称 RVA
ULONG_PTR ords = kernelBase + ordsRVA; // WORD 数组: 序号
ULONG_PTR pGetProcAddress = 0;
ULONG_PTR pLoadLibraryA = 0;
for (ULONG i = 0; i < numNames; i++) {
PCHAR fn = (PCHAR)(kernelBase + *(ULONG*)(names + i * 4));
WORD ord = *(WORD*)(ords + i * 2);
ULONG rva = *(ULONG*)(funcs + ord * 4);
if (fn[0] == 'G' && fn[1] == 'e' && fn[2] == 't' && fn[3] == 'P' &&
fn[4] == 'r' && fn[5] == 'o' && fn[6] == 'c' && fn[7] == 'A' &&
fn[8] == 'd' && fn[9] == 'd' && fn[10] == 'r' && fn[11] == 'e' &&
fn[12] == 's' && fn[13] == 's' && fn[14] == '\0') {
pGetProcAddress = kernelBase + rva;
}
if (fn[0] == 'L' && fn[1] == 'o' && fn[2] == 'a' && fn[3] == 'd' &&
fn[4] == 'L' && fn[5] == 'i' && fn[6] == 'b' && fn[7] == 'r' &&
fn[8] == 'a' && fn[9] == 'r' && fn[10] == 'y' && fn[11] == 'A' &&
fn[12] == '\0') {
pLoadLibraryA = kernelBase + rva;
}
}
if (!pGetProcAddress || !pLoadLibraryA) {
return1;
}
typedef HMODULE(WINAPI* fnLoadLibraryA_t)(LPCSTR);
typedef FARPROC(WINAPI* fnGetProcAddress_t)(HMODULE, LPCSTR);
typedef int (WINAPI* fnMessageBoxA_t)(HWND, LPCSTR, LPCSTR, UINT);
fnLoadLibraryA_t fnLoadLibraryA = (fnLoadLibraryA_t)pLoadLibraryA;
fnGetProcAddress_t fnGetProcAddress = (fnGetProcAddress_t)pGetProcAddress;
char module_user32dll[] = { 'u','s','e','r','3','2','.','d','l','l'},
func_MessageBoxA[] = { 'M','e','s','s','a','g','e','B','o','x','A' },
content[] = { 'h','e','l','l','o',' ','w','o','r','l','d' },
Tip[] = { 'M','e','s','s','a','g','e' };
HMODULE hUser32 = fnLoadLibraryA(module_user32dll);
if (!hUser32) {
return1;
}
fnMessageBoxA_t fnMessageBoxA = (fnMessageBoxA_t)fnGetProcAddress(hUser32, func_MessageBoxA);
if (!fnMessageBoxA) {
return1;
}
fnMessageBoxA(NULL, content, Tip, MB_OK);
return0;
}
编译,查看文件的段分布

这里的shell段就是我们的shellcode
注意:代码中所有的字符串都应该像Tip[] = { 'M','e','s','s','a','g','e' };这样存储,否则字符串会和代码存储到不同位置(虽然仍然在同一段)

这部分就是我们的shellcode,放入Loader中运行
#include<Windows.h>
#include<iostream>
/* length: 894 bytes */
unsignedchar buf[] = "\x48\x89\x5C\x24\x10\x48\x89\x74\x24\x18\x55\x57\x41\x56\x48\x8B\xEC\x48\x83\xEC\x50\x65\x48\x8B\x04\x25\x60\x00\x00\x00\x4C\x8B\x40\x18\x49\x83\xC0\x10\x49\x8B\x10\x49\x3B\xD0\x0F\x84\xB1\x00\x00\x00\x66\x83\x7A\x58\x18\x0F\x85\x9A\x00\x00\x00\x48\x8B\x4A\x60\x0F\xB7\x01\x66\x83\xC8\x20\x66\x83\xF8\x6B\x0F\x85\x85\x00\x00\x00\x0F\xB7\x41\x02\x66\x83\xC8\x20\x66\x83\xF8\x65\x75\x77\x0F\xB7\x41\x04\x66\x83\xC8\x20\x66\x83\xF8\x72\x75\x69\x0F\xB7\x41\x06\x66\x83\xC8\x20\x66\x83\xF8\x6E\x75\x5B\x0F\xB7\x41\x08\x66\x83\xC8\x20\x66\x83\xF8\x65\x75\x4D\x0F\xB7\x41\x0A\x66\x83\xC8\x20\x66\x83\xF8\x6C\x75\x3F\x66\x83\x79\x0C\x33\x75\x38\x66\x83\x79\x0E\x32\x75\x31\x66\x83\x79\x10\x2E\x75\x2A\x0F\xB7\x41\x12\x66\x83\xC8\x20\x66\x83\xF8\x64\x75\x1C\x0F\xB7\x41\x14\x66\x83\xC8\x20\x66\x83\xF8\x6C\x75\x0E\x0F\xB7\x41\x16\x66\x83\xC8\x20\x66\x83\xF8\x6C\x74\x27\x48\x8B\x12\x49\x3B\xD0\x0F\x85\x4F\xFF\xFF\xFF\xB8\x01\x00\x00\x00\x48\x8B\x5C\x24\x78\x48\x8B\xB4\x24\x80\x00\x00\x00\x48\x83\xC4\x50\x41\x5E\x5F\x5D\xC3\x4C\x8B\x4A\x30\x4D\x85\xC9\x74\xDC\x41\x8B\x41\x3C\x33\xDB\x45\x33\xD2\x45\x33\xC0\x42\x8B\x8C\x08\x88\x00\x00\x00\x42\x8B\x7C\x09\x1C\x42\x8B\x74\x09\x20\x49\x03\xF9\x46\x8B\x74\x09\x24\x49\x03\xF1\x46\x8B\x5C\x09\x18\x4D\x03\xF1\x45\x85\xDB\x74\xA6\x0F\x1F\x00\x43\x8D\x0C\x00\x42\x0F\xB7\x0C\x31\x42\x8D\x04\x85\x00\x00\x00\x00\x8B\x04\x30\x8B\x14\x8F\x42\x0F\xB6\x0C\x08\x80\xF9\x47\x0F\x85\xA2\x00\x00\x00\x42\x80\x7C\x08\x01\x65\x0F\x85\xFE\x00\x00\x00\x42\x80\x7C\x08\x02\x74\x0F\x85\xF2\x00\x00\x00\x42\x80\x7C\x08\x03\x50\x0F\x85\xE6\x00\x00\x00\x42\x80\x7C\x08\x04\x72\x0F\x85\xDA\x00\x00\x00\x42\x80\x7C\x08\x05\x6F\x0F\x85\xCE\x00\x00\x00\x42\x80\x7C\x08\x06\x63\x0F\x85\xC2\x00\x00\x00\x42\x80\x7C\x08\x07\x41\x0F\x85\xB6\x00\x00\x00\x42\x80\x7C\x08\x08\x64\x0F\x85\xAA\x00\x00\x00\x42\x80\x7C\x08\x09\x64\x0F\x85\x9E\x00\x00\x00\x42\x80\x7C\x08\x0A\x72\x0F\x85\x92\x00\x00\x00\x42\x80\x7C\x08\x0B\x65\x0F\x85\x86\x00\x00\x00\x42\x80\x7C\x08\x0C\x73\x75\x7E\x42\x80\x7C\x08\x0D\x73\x75\x76\x42\x80\x7C\x08\x0E\x00\x75\x6E\x49\x8D\x1C\x11\xEB\x68\x80\xF9\x4C\x75\x63\x42\x80\x7C\x08\x01\x6F\x75\x5B\x42\x80\x7C\x08\x02\x61\x75\x53\x42\x80\x7C\x08\x03\x64\x75\x4B\x42\x38\x4C\x08\x04\x75\x44\x42\x80\x7C\x08\x05\x69\x75\x3C\x42\x80\x7C\x08\x06\x62\x75\x34\x42\x80\x7C\x08\x07\x72\x75\x2C\x42\x80\x7C\x08\x08\x61\x75\x24\x42\x80\x7C\x08\x09\x72\x75\x1C\x42\x80\x7C\x08\x0A\x79\x75\x14\x42\x80\x7C\x08\x0B\x41\x75\x0C\x42\x80\x7C\x08\x0C\x00\x75\x04\x4D\x8D\x14\x11\x41\xFF\xC0\x45\x3B\xC3\x0F\x82\xC5\xFE\xFF\xFF\x48\x85\xDB\x0F\x84\x5F\xFE\xFF\xFF\x4D\x85\xD2\x0F\x84\x56\xFE\xFF\xFF\x48\x8D\x4D\xD0\xC7\x45\xD0\x75\x73\x65\x72\xC7\x45\xD4\x33\x32\x2E\x64\x66\xC7\x45\xD8\x6C\x6C\xC7\x45\xE0\x4D\x65\x73\x73\xC7\x45\xE4\x61\x67\x65\x42\x66\xC7\x45\xE8\x6F\x78\xC6\x45\xEA\x41\xC7\x45\xF0\x68\x65\x6C\x6C\xC7\x45\xF4\x6F\x20\x77\x6F\x66\xC7\x45\xF8\x72\x6C\xC6\x45\xFA\x64\xC7\x45\x20\x4D\x65\x73\x73\x66\xC7\x45\x24\x61\x67\xC6\x45\x26\x65\x41\xFF\xD2\x48\x85\xC0\x0F\x84\xF1\xFD\xFF\xFF\x48\x8D\x55\xE0\x48\x8B\xC8\xFF\xD3\x48\x85\xC0\x0F\x84\xDF\xFD\xFF\xFF\x45\x33\xC9\x4C\x8D\x45\x20\x48\x8D\x55\xF0\x33\xC9\xFF\xD0\x33\xC0\xE9\xCE\xFD\xFF\xFF\x00";
int main()
{
// VirtualAlloc(起始地址,大小,分配类型,内存页的初始保护属性)
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_READWRITE);
memcpy(addr, buf, sizeof(buf));
DWORD old;
// VirtualProtect(初始地址,大小,内存页的初始保护属性,旧属性)
VirtualProtect(addr, sizeof(buf), PAGE_EXECUTE, &old);
// CrateThread(安全属性,线程栈大小,线程函数的起始地址,传递给线程函数的参数,创建标志,指向接收线程 ID 的变量的指针)
HANDLE hthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addr, NULL, 0, NULL);
WaitForSingleObject(hthread, -1);
}
