简介
代码注入在分析程序或制作外挂外挂时是非常爱使用的一种手段, 通常用于调用程序功能, 比如调用程序中的call
;其基本原理和DLL注入的原理一样, 目前已经有很多工具可以直接实现代码注入或DLL注入了, 本文只是学习一下原理….
思路
思路很简单, 基本就两大步:
OpenProcess
打开需要注入的程序, 获取句柄;- 通过
CreateRemoteThread
函数将我们需要注入的代码以新的线程的方式进行运行, 到达注入的效果;
通过PEB获取模块基址
通常我们注入的代码一般都是汇编, 如果注入是很复杂的代码, 我们通常将代码写到DLL
中, 直接用DLL注入
了;
所以为了使我们的汇编根据有健壮性, 这里说一下如何用汇编获取程序模块的基地址, 注入的代码就相当于是写shellcode
PEB
在fs:[0x30]
地址处保存着一个指针, 指向了PEB结构, 结构基本如下:
typedef struct _PEB { // Size: 0x1D8
/*000*/ UCHAR InheritedAddressSpace;
/*001*/ UCHAR ReadImageFileExecOptions;
/*002*/ UCHAR BeingDebugged;
/*003*/ UCHAR SpareBool; // Allocation size
/*004*/ HANDLE Mutant;
/*008*/ HINSTANCE ImageBaseAddress; // Instance
/*00C*/ VOID *DllList; //_PEB_LDR_DATA ;进程加载的模块链表
/*010*/ PPROCESS_PARAMETERS *ProcessParameters;
/*014*/ ULONG SubSystemData;
/*018*/ HANDLE DefaultHeap;
/*01C*/ KSPIN_LOCK FastPebLock;
/*020*/ ULONG FastPebLockRoutine;
/*024*/ ULONG FastPebUnlockRoutine;
/*028*/ ULONG EnvironmentUpdateCount;
/*02C*/ ULONG KernelCallbackTable;
/*030*/ LARGE_INTEGER SystemReserved;
/*038*/ ULONG FreeList;
/*03C*/ ULONG TlsExpansionCounter;
/*040*/ ULONG TlsBitmap;
/*044*/ LARGE_INTEGER TlsBitmapBits;
/*04C*/ ULONG ReadOnlySharedMemoryBase;
/*050*/ ULONG ReadOnlySharedMemoryHeap;
/*054*/ ULONG ReadOnlyStaticServerData;
/*058*/ ULONG AnsiCodePageData;
/*05C*/ ULONG OemCodePageData;
/*060*/ ULONG UnicodeCaseTableData;
/*064*/ ULONG NumberOfProcessors;
/*068*/ LARGE_INTEGER NtGlobalFlag;
/*070*/ LARGE_INTEGER CriticalSectionTimeout;
/*078*/ ULONG HeapSegmentReserve;
/*07C*/ ULONG HeapSegmentCommit;
/*080*/ ULONG HeapDeCommitTotalFreeThreshold;
/*084*/ ULONG HeapDeCommitFreeBlockThreshold;
/*088*/ ULONG NumberOfHeaps;
/*08C*/ ULONG MaximumNumberOfHeaps;
/*090*/ ULONG ProcessHeaps;
/*094*/ ULONG GdiSharedHandleTable;
/*098*/ ULONG ProcessStarterHelper;
/*09C*/ ULONG GdiDCAttributeList;
/*0A0*/ KSPIN_LOCK LoaderLock;
/*0A4*/ ULONG OSMajorVersion;
/*0A8*/ ULONG OSMinorVersion;
/*0AC*/ USHORT OSBuildNumber;
/*0AE*/ USHORT OSCSDVersion;
/*0B0*/ ULONG OSPlatformId;
/*0B4*/ ULONG ImageSubsystem;
/*0B8*/ ULONG ImageSubsystemMajorVersion;
/*0BC*/ ULONG ImageSubsystemMinorVersion;
/*0C0*/ ULONG ImageProcessAffinityMask;
/*0C4*/ ULONG GdiHandleBuffer[0x22];
/*14C*/ ULONG PostProcessInitRoutine;
/*150*/ ULONG TlsExpansionBitmap;
/*154*/ UCHAR TlsExpansionBitmapBits[0x80];
/*1D4*/ ULONG SessionId;
} PEB, *PPEB;
PEB结构的偏移0xc
处保存着另外一个指针ldr,该指针为PEB_LDR_DATA:
PEB_LDR_DATA
typedef struct _PEB_LDR_DATA
{
ULONG Length; // +0x00
BOOLEAN Initialized; // +0x04
PVOID SsHandle; // +0x08
LIST_ENTRY InLoadOrderModuleList; // +0x0c
LIST_ENTRY InMemoryOrderModuleList; // +0x14
LIST_ENTRY InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
PEB_LDR_DATA结构
的后三个成员是指向LDR_MODULE
链表结构中相应三条双向链表头的指针, 分别是按照加载顺序, 在内存中地址顺序和初始化顺序排列的模块信息结构的指针, 其中LDR_MODULE结构
就是_LDR_DATA_TABLE_ENTRY
结构; 而链表的第一个就保存了当前程序的基地址;
_LDR_DATA_TABLE_ENTRY结构
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks; // +0x00
LIST_ENTRY InMemoryOrderLinks; // +0x04
LIST_ENTRY InInitializationOrderLinks; // +0x08
PVOID DllBase; // +0x0c
PVOID EntryPoint; // +0x10
DWORD SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
DWORD Flags;
WORD LoadCount;
WORD TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
DWORD CheckSum;
DWORD TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
}LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;
其中偏移为0x10
的位置的EntryPoint
就保存了模块的基地址;
综上所述, 我们可以利用一下这段汇编代码获取程序的基地址:
mov eax, fs:[0x30]; // PEB
mov ebx, [eax + 0xc]; // PEB_LDR_DATA
mov eax, [ebx + 0x14]; // InMemoryOrderModuleList
mov ebx, [eax + 0x10]; // ebx = EntryPoint(基址)
如果在DLL当中获取程序基地址, 可以使用下面的代码:
void Get_addr(DWORD pro_id){
HANDLE hpro = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pro_id);
if (hpro == 0){
printf("无法获取进程句柄");
}
printf("进程句柄id: %d\n",hpro);
// 获取每一个模块加载基址
DWORD pro_base = NULL;
HMODULE hModule[100] = {0};
DWORD dwRet = 0;
int num = 0;
int bRet = EnumProcessModulesEx(hpro, (HMODULE *)(hModule), sizeof(hModule),&dwRet,NULL);
if (bRet == 0){
printf("EnumProcessModules");
}
// 总模块个数
num = dwRet/sizeof(HMODULE);
printf("总模块个数: %d\n",num);
// 打印每一个模块加载基址
char lpBaseName[100];
for(int i = 0;i<num;i++){
GetModuleBaseNameA(hpro,hModule[i],lpBaseName,sizeof(lpBaseName));
printf("%-2d %-25s基址: 0x%p\n",i,lpBaseName,hModule[i]);
}
pro_base = (DWORD)hModule[0];
printf("程序基址: 0x%p\n",pro_base);
}
或者:
void Get_addr(){
HMODULE addr = GetModuleHandle(NULL);
printf("addr: 0x%p\n", addr);
}
代码注入
首先这是我们要注入的程序代码:
// Demo.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
#include <stdio.h>
#include <windows.h>
void add(int a) {
printf("a: %d\n", a);
}
int main()
{
add(8);
printf("add_addr: 0x%p\n", add); // add函数地址
while (true)
{
printf("Demo....\n");
getchar();
}
return 0;
}
此程序有一个add
函数, 可以接收一个参数, 并且在程序中只调用一次, 我们可以通过代码注入的方式调用这个函数. 注入代码如下:
// Win.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include<stdio.h>
#include<Windows.h>
#include<psapi.h>
void injected_code() {
__asm {
// 获取基地址
mov eax, fs:[0x30]; // PEB
mov ebx, [eax + 0xc]; // PEB_LDR_DATA
mov eax, [ebx + 0x14]; // InMemoryOrderModuleList
mov ebx, [eax + 0x10]; // ebx = 基址
push 100; // add函数参数
add ebx, 0x0002964F; // ebx = 基址 + 偏移 add函数地址
call ebx; // 调用add函数
add esp, 0x4;
}
}
void inject_fun(DWORD pid) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
printf("hProcess: 0x%x\n", hProcess);
LPVOID call_addr = VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("call_addr: 0x%x\n", call_addr);
int ret = WriteProcessMemory(hProcess, call_addr, injected_code, 0x1000, NULL);
printf("WriteProcessMemory: 0x%x\n", ret);
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)call_addr, NULL, 0, NULL);
printf("hthread: %x\n", hThread);
WaitForSingleObject(hThread, 2000);
CloseHandle(hProcess);
CloseHandle(hThread);
}
int main()
{
HWND Prohan = FindWindowA(NULL, "C:\\Users\\cc-sir\\Desktop\\Demo.exe");
if (Prohan) {
printf("Prohan: 0x%x\n", Prohan);
DWORD Pid;
GetWindowThreadProcessId(Prohan, &Pid);
printf("Pid: %d\n", Pid);
// LPCSTR title = "sir";
// SetWindowText(Prohan, title);
inject_fun(Pid);
}
else {
printf("FindWindow Error!\n");
}
system("pause");
return 0;
}
代码当中重新push 100
作为add
函数的参数进行注入:
总结
通过代码注入可以在不修改程序的情况下调用程序当中的代码段, 在实际过程中还是使用工具比较方便….