简介
代码注入在分析程序或制作外挂外挂时是非常爱使用的一种手段, 通常用于调用程序功能, 比如调用程序中的call
;其基本原理和DLL注入的原理一样, 目前已经有很多工具可以直接实现代码注入或DLL注入了, 本文只是学习一下原理....
思路
思路很简单, 基本就两大步:
OpenProcess
打开需要注入的程序, 获取句柄;
- 通过
CreateRemoteThread
函数将我们需要注入的代码以新的线程的方式进行运行, 到达注入的效果;
通过PEB获取模块基址
通常我们注入的代码一般都是汇编, 如果注入是很复杂的代码, 我们通常将代码写到DLL
中, 直接用DLL注入
了;
所以为了使我们的汇编根据有健壮性, 这里说一下如何用汇编获取程序模块的基地址, 注入的代码就相当于是写shellcode
;
PEB
在fs:[0x30]
地址处保存着一个指针, 指向了PEB结构, 结构基本如下:
typedef struct _PEB {
UCHAR InheritedAddressSpace;
UCHAR ReadImageFileExecOptions;
UCHAR BeingDebugged;
UCHAR SpareBool;
HANDLE Mutant;
HINSTANCE ImageBaseAddress;
VOID *DllList;
PPROCESS_PARAMETERS *ProcessParameters;
ULONG SubSystemData;
HANDLE DefaultHeap;
KSPIN_LOCK FastPebLock;
ULONG FastPebLockRoutine;
ULONG FastPebUnlockRoutine;
ULONG EnvironmentUpdateCount;
ULONG KernelCallbackTable;
LARGE_INTEGER SystemReserved;
ULONG FreeList;
ULONG TlsExpansionCounter;
ULONG TlsBitmap;
LARGE_INTEGER TlsBitmapBits;
ULONG ReadOnlySharedMemoryBase;
ULONG ReadOnlySharedMemoryHeap;
ULONG ReadOnlyStaticServerData;
ULONG AnsiCodePageData;
ULONG OemCodePageData;
ULONG UnicodeCaseTableData;
ULONG NumberOfProcessors;
LARGE_INTEGER NtGlobalFlag;
LARGE_INTEGER CriticalSectionTimeout;
ULONG HeapSegmentReserve;
ULONG HeapSegmentCommit;
ULONG HeapDeCommitTotalFreeThreshold;
ULONG HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
ULONG ProcessHeaps;
ULONG GdiSharedHandleTable;
ULONG ProcessStarterHelper;
ULONG GdiDCAttributeList;
KSPIN_LOCK LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
USHORT OSBuildNumber;
USHORT OSCSDVersion;
ULONG OSPlatformId;
ULONG ImageSubsystem;
ULONG ImageSubsystemMajorVersion;
ULONG ImageSubsystemMinorVersion;
ULONG ImageProcessAffinityMask;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
UCHAR TlsExpansionBitmapBits[0x80];
ULONG SessionId;
} PEB, *PPEB;
PEB结构的偏移0xc
处保存着另外一个指针ldr,该指针为PEB_LDR_DATA:
PEB_LDR_DATA
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA,*PPEB_LDR_DATA;
PEB_LDR_DATA结构
的后三个成员是指向LDR_MODULE
链表结构中相应三条双向链表头的指针, 分别是按照加载顺序, 在内存中地址顺序和初始化顺序排列的模块信息结构的指针, 其中LDR_MODULE结构
就是_LDR_DATA_TABLE_ENTRY
结构; 而链表的第一个就保存了当前程序的基地址;
_LDR_DATA_TABLE_ENTRY结构
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
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];
mov ebx, [eax + 0xc];
mov eax, [ebx + 0x14];
mov ebx, [eax + 0x10];
如果在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);
}
代码注入
首先这是我们要注入的程序代码:
#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);
while (true)
{
printf("Demo....\n");
getchar();
}
return 0;
}

此程序有一个add
函数, 可以接收一个参数, 并且在程序中只调用一次, 我们可以通过代码注入的方式调用这个函数. 注入代码如下:
#include<stdio.h>
#include<Windows.h>
#include<psapi.h>
void injected_code() {
__asm {
mov eax, fs:[0x30];
mov ebx, [eax + 0xc];
mov eax, [ebx + 0x14];
mov ebx, [eax + 0x10];
push 100;
add ebx, 0x0002964F;
call ebx;
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);
inject_fun(Pid);
}
else {
printf("FindWindow Error!\n");
}
system("pause");
return 0;
}
代码当中重新push 100
作为add
函数的参数进行注入:

总结
通过代码注入可以在不修改程序的情况下调用程序当中的代码段, 在实际过程中还是使用工具比较方便....