简介

代码注入在分析程序或制作外挂外挂时是非常爱使用的一种手段, 通常用于调用程序功能, 比如调用程序中的call;其基本原理和DLL注入的原理一样, 目前已经有很多工具可以直接实现代码注入或DLL注入了, 本文只是学习一下原理….

思路

思路很简单, 基本就两大步:

  1. OpenProcess打开需要注入的程序, 获取句柄;
  2. 通过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函数的参数进行注入:
inject

总结

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