首页
社区
课程
招聘
实战dll注入(原理, 踩坑及排雷)
发表于: 2022-8-22 21:09 25611

实战dll注入(原理, 踩坑及排雷)

2022-8-22 21:09
25611

May contain jump thunks to handle relocation of functions to new addresses.

反射式dll注入后目标进程的内存

// shellcode函数
void shellcodeFunc(PMY_PARAMS pParams) {
    // pParams保存LdrLoadDll等系统api的内存地址
    // 用NtAllocateVirtualMemory在目标进程中开辟一块内存(需指定PAGE_EXECUTE_READWRITE权限)
    // 将dll的文件内容写入开辟的内存
    // 修复导入表; 重定位
    // 执行dll的入口函数DLLMain
}
 
DWORD size = 0, ssss=0;
 
// 获取jmp指令后的双字操作数(即jmp的目的地址偏移)
DWORD* jmpAddr = (DWORD*) ((BYTE*) shellcodeFunc + 1);
 
// 加5得到jmp指令的下一条指令地址, 然后加上jmp的目的地址偏移, 得到函数体的实际起始地址
WORD* Memx0 = (WORD*) ((BYTE*) shellcodeFunc + 5 + *jmpAddr);
LONG_PTR* Memx = (LONG_PTR*) Memx0;
 
// 用0xCCCCCCCCCCCCCCCC作为函数体结束识别标识.
while (*Memx != 0xCCCCCCCCCCCCCCCC) {
    Memx++;
    size += 8;
}
 
// 将shellcode写入文件
HANDLE hFile = CreateFile(LOADECODE, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
if (hFile) {
    WriteFile(hFile, Memx0, size, &ssss, NULL);
    CloseHandle(hFile);
}
// shellcode函数
void shellcodeFunc(PMY_PARAMS pParams) {
    // pParams保存LdrLoadDll等系统api的内存地址
    // 用NtAllocateVirtualMemory在目标进程中开辟一块内存(需指定PAGE_EXECUTE_READWRITE权限)
    // 将dll的文件内容写入开辟的内存
    // 修复导入表; 重定位
    // 执行dll的入口函数DLLMain
}
 
DWORD size = 0, ssss=0;
 
// 获取jmp指令后的双字操作数(即jmp的目的地址偏移)
DWORD* jmpAddr = (DWORD*) ((BYTE*) shellcodeFunc + 1);
 
// 加5得到jmp指令的下一条指令地址, 然后加上jmp的目的地址偏移, 得到函数体的实际起始地址
WORD* Memx0 = (WORD*) ((BYTE*) shellcodeFunc + 5 + *jmpAddr);
LONG_PTR* Memx = (LONG_PTR*) Memx0;
 
// 用0xCCCCCCCCCCCCCCCC作为函数体结束识别标识.
while (*Memx != 0xCCCCCCCCCCCCCCCC) {
    Memx++;
    size += 8;
}
 
// 将shellcode写入文件
HANDLE hFile = CreateFile(LOADECODE, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
if (hFile) {
    WriteFile(hFile, Memx0, size, &ssss, NULL);
    CloseHandle(hFile);
}

May contain jump thunks to handle relocation of functions to new addresses.

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
// shellcode函数
void shellcodeFunc(PMY_PARAMS pParams) {
    // pParams保存LdrLoadDll等系统api的内存地址
    // 用NtAllocateVirtualMemory在目标进程中开辟一块内存(需指定PAGE_EXECUTE_READWRITE权限)
    // 将dll的文件内容写入开辟的内存
    // 修复导入表; 重定位
    // 执行dll的入口函数DLLMain
}
 
DWORD size = 0, ssss=0;
 
// 获取jmp指令后的双字操作数(即jmp的目的地址偏移)
DWORD* jmpAddr = (DWORD*) ((BYTE*) shellcodeFunc + 1);
 
// 加5得到jmp指令的下一条指令地址, 然后加上jmp的目的地址偏移, 得到函数体的实际起始地址
WORD* Memx0 = (WORD*) ((BYTE*) shellcodeFunc + 5 + *jmpAddr);
LONG_PTR* Memx = (LONG_PTR*) Memx0;
 
// 用0xCCCCCCCCCCCCCCCC作为函数体结束识别标识.
while (*Memx != 0xCCCCCCCCCCCCCCCC) {
    Memx++;
    size += 8;
}
 
// 将shellcode写入文件
HANDLE hFile = CreateFile(LOADECODE, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
if (hFile) {
    WriteFile(hFile, Memx0, size, &ssss, NULL);
    CloseHandle(hFile);
}
  • 使用vs2019编写注入器程序, 在生成的注入器可用前, 踩了不少坑, 因此记录一下.
  • 本文涉及三种恶意代码注入方法: 直接dll注入, 反射式dll注入, 镂空注入. 之所以选这三种注入方法, 是因最近在做一个检测进程内存空间以期发现代码注入的程序, 而实验发现这三种方法对目标进程的改变各有特点:
    • 直接dll注入: 还有APC注入, 本质都是在目标进程中执行LoadLibrary函数, 因而在枚举目标进程的模块列表时可看到注入的dll.
    • 反射式dll注入: 这种方法也会在目标进程中开辟新的内存空间并写入代码. 但因为没有调用LoadLibrary, 所以**枚举目标进程的模块列表并不能看到注入的dll. (意味着目标进程的PEB没有变化). **
    • 镂空注入: 直接改进程中某一模块的内存空间, 或者先注入一个合法模块, 再镂空该模块. **因为该方法没有注入恶意dll, 所以直接枚举目标进程的模块列表也无法发现恶意代码, 需扫描进程的内存空间. **
  • 标了注: 的地方基本都是踩的坑.
  • 直接dll注入: 还有APC注入, 本质都是在目标进程中执行LoadLibrary函数, 因而在枚举目标进程的模块列表时可看到注入的dll.
  • 反射式dll注入: 这种方法也会在目标进程中开辟新的内存空间并写入代码. 但因为没有调用LoadLibrary, 所以**枚举目标进程的模块列表并不能看到注入的dll. (意味着目标进程的PEB没有变化). **
  • 镂空注入: 直接改进程中某一模块的内存空间, 或者先注入一个合法模块, 再镂空该模块. **因为该方法没有注入恶意dll, 所以直接枚举目标进程的模块列表也无法发现恶意代码, 需扫描进程的内存空间. **
  • 示例: LibSpy项目的OnMouseMoveInjectDll
  • 流程:
    • OpenProcess打开目标进程(一参为PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE| PROCESS_VM_READ, 后面CreateRemoteThread才能执行)
      • 需要给进程提权, 得到SeDebug权限.
        1. 注: 准确地说不是提权, 而是将访问令牌中禁用的权限启用. 成功调用下面几个函数的前提是进程具备该权限, 只是访问令牌中没有启用该权限. 而如果进程没有该权限, 则使用下面的函数后再调用GetLastError会返回ERROR_NOT_ALL_ASSIGN.
        2. 先用LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid)得到用户的debug权限.
        3. 然后用OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,&hToken)获取进程的令牌句柄.
        4. 最后用AdjustTokenPrivileges启用特权.
      • 若要打开关键进程(csrss.exe等), 需在驱动中打开, 去掉关键进程的EPROCESS中的PsIsProtectProcess标志位, 并关闭dll签名策略. (参考开源项目blackbone)
    • 获取待注入的dll路径, 在目标进程内分配一块内存(VirtualAllocateEx), 将路径拷贝到该内存中(WriteProcessMemory)
    • 获取kernel32中的LoadLibraryA地址(如果dll路径是宽字符串, 则用LoadLibraryW)
    • 调用CreateRemoteThread, 在目标进程中执行LoadLibrary, 进而执行DllMain函数中的目标代码
  • OpenProcess打开目标进程(一参为PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE| PROCESS_VM_READ, 后面CreateRemoteThread才能执行)
    • 需要给进程提权, 得到SeDebug权限.
      1. 注: 准确地说不是提权, 而是将访问令牌中禁用的权限启用. 成功调用下面几个函数的前提是进程具备该权限, 只是访问令牌中没有启用该权限. 而如果进程没有该权限, 则使用下面的函数后再调用GetLastError会返回ERROR_NOT_ALL_ASSIGN.
      2. 先用LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid)得到用户的debug权限.
      3. 然后用OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,&hToken)获取进程的令牌句柄.
      4. 最后用AdjustTokenPrivileges启用特权.
    • 若要打开关键进程(csrss.exe等), 需在驱动中打开, 去掉关键进程的EPROCESS中的PsIsProtectProcess标志位, 并关闭dll签名策略. (参考开源项目blackbone)
  • 获取待注入的dll路径, 在目标进程内分配一块内存(VirtualAllocateEx), 将路径拷贝到该内存中(WriteProcessMemory)
  • 获取kernel32中的LoadLibraryA地址(如果dll路径是宽字符串, 则用LoadLibraryW)
  • 调用CreateRemoteThread, 在目标进程中执行LoadLibrary, 进而执行DllMain函数中的目标代码
  • 需要给进程提权, 得到SeDebug权限.
    1. 注: 准确地说不是提权, 而是将访问令牌中禁用的权限启用. 成功调用下面几个函数的前提是进程具备该权限, 只是访问令牌中没有启用该权限. 而如果进程没有该权限, 则使用下面的函数后再调用GetLastError会返回ERROR_NOT_ALL_ASSIGN.
    2. 先用LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid)得到用户的debug权限.
    3. 然后用OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,&hToken)获取进程的令牌句柄.
    4. 最后用AdjustTokenPrivileges启用特权.
  • 若要打开关键进程(csrss.exe等), 需在驱动中打开, 去掉关键进程的EPROCESS中的PsIsProtectProcess标志位, 并关闭dll签名策略. (参考开源项目blackbone)
  • 概要
    • 没有使用LoadLibrary函数注入dll.
    • 需由注入器自行解析PE文件:
      • 将dll头部(包括DOS头, PE头, 区块表)逐字节写入新开辟的内存.
      • 按重定位表的信息手动重定位
      • 修复导入函数表: 使用LdrLoadDll得到shellcode需要的库的内存地址, 用LdrGetProcedureAddress得到要导入的函数的内存地址, 然后将这些地址填入导入表.
  • 流程
    • 将自己实现的LoadLibrary功能函数保存为shellcode.
      • 注: 在shellcode中使用的系统api都要事先通过GetProcAddress获取(使用GetModuleHandleA获取模块句柄, 传入的模块名不用后缀), 并作为参数传给shellcode.
      • 注: 需要审查注入器保存的shellcode是否是真实的函数体. 调试发现在vs2019中, 按默认选项编译得到的函数地址处是一条跳转到实际函数体的jmp指令. 因此需要使用jmp指令的操作数计算实际函数地址. 如下为获取一个shellcode函数的代码:
        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
        // shellcode函数
        void shellcodeFunc(PMY_PARAMS pParams) {
            // pParams保存LdrLoadDll等系统api的内存地址
            // 用NtAllocateVirtualMemory在目标进程中开辟一块内存(需指定PAGE_EXECUTE_READWRITE权限)
            // 将dll的文件内容写入开辟的内存
            // 修复导入表; 重定位
            // 执行dll的入口函数DLLMain
        }
         
        DWORD size = 0, ssss=0;
         
        // 获取jmp指令后的双字操作数(即jmp的目的地址偏移)
        DWORD* jmpAddr = (DWORD*) ((BYTE*) shellcodeFunc + 1);
         
        // 加5得到jmp指令的下一条指令地址, 然后加上jmp的目的地址偏移, 得到函数体的实际起始地址
        WORD* Memx0 = (WORD*) ((BYTE*) shellcodeFunc + 5 + *jmpAddr);
        LONG_PTR* Memx = (LONG_PTR*) Memx0;
         
        // 用0xCCCCCCCCCCCCCCCC作为函数体结束识别标识.
        while (*Memx != 0xCCCCCCCCCCCCCCCC) {
            Memx++;
            size += 8;
        }
         
        // 将shellcode写入文件
        HANDLE hFile = CreateFile(LOADECODE, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
        if (hFile) {
            WriteFile(hFile, Memx0, size, &ssss, NULL);
            CloseHandle(hFile);
        }
      • 20230810:发现可以通过修改编译和链接选项, 让编译的程序不会多一个跳转表.
        • 编译时带上/GL选项(C/C++ -> 优化 -> 全程序优化, 选择是 (/GL)).
        • 链接时带上/INCREMENTAL:NO选项(链接器 -> 常规 -> 启用增量链接, 选否 (/INCREMENTAL:NO)). 在微软对这个链接选项的说明文档中, 有如下一句:

          May contain jump thunks to handle relocation of functions to new addresses.

          也就是说/INCREMENTAL选项会让程序多一张跳转表, 以处理函数的重定位. 在Debug模式下就会默认开启此选项.
      • 注: 用windbg调试远程线程, 发现远程线程中出现地址访问冲突:
        • 原因1: 出问题的地方试图读取__security_cookie
          • 解决: 编译时关闭/GS选项, 禁用栈保护.
        • 原因2: 远程线程中试图调用一些不可用的函数, 包括__CheckForDebuggerJustMyCode, _RTC_CheckStackVars
          • 解决: 禁用/JMC选项, /RTC选项, 其他的如果是必要使用的动态库函数, 则需要用LoadLibraryGetProcAddress获取, 且要确保目标进程已载入相应dll.
      • 注: shellcode函数中不要用字符串常量, 因为这些字符串总是存在于注入器进程的堆栈上, 不会随着shellcode一起注入到目标进程中, 这样一来shellcode在运行时无从获取这些字符串常量. 在shellcode中最好是通过传参的方式获取需要用到的字符串常量.
    • 在目标进程中开辟新内存, 依次写入:
      • 要注入的dll的文件内容.
      • shellcode.
      • 传给shellcode的参数, 主要是shellcode需要的如下系统api的函数地址.
        • LdrLoadDll: 获取注入的dll依赖的dll的内存地址.
        • LdrGetProcedureAddress: 获取注入的dll需导入的函数的内存地址.
        • RtlInitAnsiString RtlAnsiStringToUnicodeString RtlFreeUnicodeString: 用以配合上述两个函数, 得到导入函数的内存地址.
        • NtAllocateVirtualMemory: 分配内存空间, 以写入要注入的dll的文件内容.
    • 创建远程线程, 执行shellcode, 由shellcode载入dll.
  • 优点
    • 仅枚举进程模块列表并不能发现注入的dll.
  • 没有使用LoadLibrary函数注入dll.
  • 需由注入器自行解析PE文件:
    • 将dll头部(包括DOS头, PE头, 区块表)逐字节写入新开辟的内存.
    • 按重定位表的信息手动重定位
    • 修复导入函数表: 使用LdrLoadDll得到shellcode需要的库的内存地址, 用LdrGetProcedureAddress得到要导入的函数的内存地址, 然后将这些地址填入导入表.
  • 将dll头部(包括DOS头, PE头, 区块表)逐字节写入新开辟的内存.
  • 按重定位表的信息手动重定位
  • 修复导入函数表: 使用LdrLoadDll得到shellcode需要的库的内存地址, 用LdrGetProcedureAddress得到要导入的函数的内存地址, 然后将这些地址填入导入表.
  • 将自己实现的LoadLibrary功能函数保存为shellcode.
    • 注: 在shellcode中使用的系统api都要事先通过GetProcAddress获取(使用GetModuleHandleA获取模块句柄, 传入的模块名不用后缀), 并作为参数传给shellcode.
    • 注: 需要审查注入器保存的shellcode是否是真实的函数体. 调试发现在vs2019中, 按默认选项编译得到的函数地址处是一条跳转到实际函数体的jmp指令. 因此需要使用jmp指令的操作数计算实际函数地址. 如下为获取一个shellcode函数的代码:
      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
      // shellcode函数
      void shellcodeFunc(PMY_PARAMS pParams) {
          // pParams保存LdrLoadDll等系统api的内存地址
          // 用NtAllocateVirtualMemory在目标进程中开辟一块内存(需指定PAGE_EXECUTE_READWRITE权限)
          // 将dll的文件内容写入开辟的内存
          // 修复导入表; 重定位
          // 执行dll的入口函数DLLMain
      }
       
      DWORD size = 0, ssss=0;
       
      // 获取jmp指令后的双字操作数(即jmp的目的地址偏移)
      DWORD* jmpAddr = (DWORD*) ((BYTE*) shellcodeFunc + 1);
       
      // 加5得到jmp指令的下一条指令地址, 然后加上jmp的目的地址偏移, 得到函数体的实际起始地址
      WORD* Memx0 = (WORD*) ((BYTE*) shellcodeFunc + 5 + *jmpAddr);
      LONG_PTR* Memx = (LONG_PTR*) Memx0;
       
      // 用0xCCCCCCCCCCCCCCCC作为函数体结束识别标识.
      while (*Memx != 0xCCCCCCCCCCCCCCCC) {
          Memx++;
          size += 8;
      }
       
      // 将shellcode写入文件
      HANDLE hFile = CreateFile(LOADECODE, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
      if (hFile) {
          WriteFile(hFile, Memx0, size, &ssss, NULL);
          CloseHandle(hFile);
      }
    • 20230810:发现可以通过修改编译和链接选项, 让编译的程序不会多一个跳转表.
      • 编译时带上/GL选项(C/C++ -> 优化 -> 全程序优化, 选择是 (/GL)).
      • 链接时带上/INCREMENTAL:NO选项(链接器 -> 常规 -> 启用增量链接, 选否 (/INCREMENTAL:NO)). 在微软对这个链接选项的说明文档中, 有如下一句:

        May contain jump thunks to handle relocation of functions to new addresses.

        也就是说/INCREMENTAL选项会让程序多一张跳转表, 以处理函数的重定位. 在Debug模式下就会默认开启此选项.
    • 注: 用windbg调试远程线程, 发现远程线程中出现地址访问冲突:
      • 原因1: 出问题的地方试图读取__security_cookie
        • 解决: 编译时关闭/GS选项, 禁用栈保护.
      • 原因2: 远程线程中试图调用一些不可用的函数, 包括__CheckForDebuggerJustMyCode, _RTC_CheckStackVars
        • 解决: 禁用/JMC选项, /RTC选项, 其他的如果是必要使用的动态库函数, 则需要用LoadLibraryGetProcAddress获取, 且要确保目标进程已载入相应dll.
    • 注: shellcode函数中不要用字符串常量, 因为这些字符串总是存在于注入器进程的堆栈上, 不会随着shellcode一起注入到目标进程中, 这样一来shellcode在运行时无从获取这些字符串常量. 在shellcode中最好是通过传参的方式获取需要用到的字符串常量.
  • 在目标进程中开辟新内存, 依次写入:
    • 要注入的dll的文件内容.
    • shellcode.
    • 传给shellcode的参数, 主要是shellcode需要的如下系统api的函数地址.
      • LdrLoadDll: 获取注入的dll依赖的dll的内存地址.
      • LdrGetProcedureAddress: 获取注入的dll需导入的函数的内存地址.
      • RtlInitAnsiString RtlAnsiStringToUnicodeString RtlFreeUnicodeString: 用以配合上述两个函数, 得到导入函数的内存地址.
      • NtAllocateVirtualMemory: 分配内存空间, 以写入要注入的dll的文件内容.
  • 创建远程线程, 执行shellcode, 由shellcode载入dll.
  • 注: 在shellcode中使用的系统api都要事先通过GetProcAddress获取(使用GetModuleHandleA获取模块句柄, 传入的模块名不用后缀), 并作为参数传给shellcode.
  • 注: 需要审查注入器保存的shellcode是否是真实的函数体. 调试发现在vs2019中, 按默认选项编译得到的函数地址处是一条跳转到实际函数体的jmp指令. 因此需要使用jmp指令的操作数计算实际函数地址. 如下为获取一个shellcode函数的代码:
    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
    // shellcode函数
    void shellcodeFunc(PMY_PARAMS pParams) {
        // pParams保存LdrLoadDll等系统api的内存地址
        // 用NtAllocateVirtualMemory在目标进程中开辟一块内存(需指定PAGE_EXECUTE_READWRITE权限)
        // 将dll的文件内容写入开辟的内存
        // 修复导入表; 重定位
        // 执行dll的入口函数DLLMain
    }
     
    DWORD size = 0, ssss=0;
     
    // 获取jmp指令后的双字操作数(即jmp的目的地址偏移)
    DWORD* jmpAddr = (DWORD*) ((BYTE*) shellcodeFunc + 1);
     
    // 加5得到jmp指令的下一条指令地址, 然后加上jmp的目的地址偏移, 得到函数体的实际起始地址
    WORD* Memx0 = (WORD*) ((BYTE*) shellcodeFunc + 5 + *jmpAddr);
    LONG_PTR* Memx = (LONG_PTR*) Memx0;
     
    // 用0xCCCCCCCCCCCCCCCC作为函数体结束识别标识.
    while (*Memx != 0xCCCCCCCCCCCCCCCC) {
        Memx++;
        size += 8;
    }
     
    // 将shellcode写入文件
    HANDLE hFile = CreateFile(LOADECODE, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
    if (hFile) {
        WriteFile(hFile, Memx0, size, &ssss, NULL);
        CloseHandle(hFile);
    }
  • 20230810:发现可以通过修改编译和链接选项, 让编译的程序不会多一个跳转表.
    • 编译时带上/GL选项(C/C++ -> 优化 -> 全程序优化, 选择是 (/GL)).
    • 链接时带上/INCREMENTAL:NO选项(链接器 -> 常规 -> 启用增量链接, 选否 (/INCREMENTAL:NO)). 在微软对这个链接选项的说明文档中, 有如下一句:

      May contain jump thunks to handle relocation of functions to new addresses.

      也就是说/INCREMENTAL选项会让程序多一张跳转表, 以处理函数的重定位. 在Debug模式下就会默认开启此选项.
  • 注: 用windbg调试远程线程, 发现远程线程中出现地址访问冲突:
    • 原因1: 出问题的地方试图读取__security_cookie
      • 解决: 编译时关闭/GS选项, 禁用栈保护.
    • 原因2: 远程线程中试图调用一些不可用的函数, 包括__CheckForDebuggerJustMyCode, _RTC_CheckStackVars
      • 解决: 禁用/JMC选项, /RTC选项, 其他的如果是必要使用的动态库函数, 则需要用LoadLibraryGetProcAddress获取, 且要确保目标进程已载入相应dll.
  • 注: shellcode函数中不要用字符串常量, 因为这些字符串总是存在于注入器进程的堆栈上, 不会随着shellcode一起注入到目标进程中, 这样一来shellcode在运行时无从获取这些字符串常量. 在shellcode中最好是通过传参的方式获取需要用到的字符串常量.
  • 编译时带上/GL选项(C/C++ -> 优化 -> 全程序优化, 选择是 (/GL)).
  • 链接时带上/INCREMENTAL:NO选项(链接器 -> 常规 -> 启用增量链接, 选否 (/INCREMENTAL:NO)). 在微软对这个链接选项的说明文档中, 有如下一句:

    May contain jump thunks to handle relocation of functions to new addresses.

    也就是说/INCREMENTAL选项会让程序多一张跳转表, 以处理函数的重定位. 在Debug模式下就会默认开启此选项.
  • 原因1: 出问题的地方试图读取__security_cookie
    • 解决: 编译时关闭/GS选项, 禁用栈保护.
  • 原因2: 远程线程中试图调用一些不可用的函数, 包括__CheckForDebuggerJustMyCode, _RTC_CheckStackVars
    • 解决: 禁用/JMC选项, /RTC选项, 其他的如果是必要使用的动态库函数, 则需要用LoadLibraryGetProcAddress获取, 且要确保目标进程已载入相应dll.
  • 解决: 编译时关闭/GS选项, 禁用栈保护.
  • 解决: 禁用/JMC选项, /RTC选项, 其他的如果是必要使用的动态库函数, 则需要用LoadLibraryGetProcAddress获取, 且要确保目标进程已载入相应dll.
  • 要注入的dll的文件内容.
  • shellcode.
  • 传给shellcode的参数, 主要是shellcode需要的如下系统api的函数地址.
    • LdrLoadDll: 获取注入的dll依赖的dll的内存地址.
    • LdrGetProcedureAddress: 获取注入的dll需导入的函数的内存地址.
    • RtlInitAnsiString RtlAnsiStringToUnicodeString RtlFreeUnicodeString: 用以配合上述两个函数, 得到导入函数的内存地址.
    • NtAllocateVirtualMemory: 分配内存空间, 以写入要注入的dll的文件内容.
  • LdrLoadDll: 获取注入的dll依赖的dll的内存地址.
  • LdrGetProcedureAddress: 获取注入的dll需导入的函数的内存地址.
  • RtlInitAnsiString RtlAnsiStringToUnicodeString RtlFreeUnicodeString: 用以配合上述两个函数, 得到导入函数的内存地址.
  • NtAllocateVirtualMemory: 分配内存空间, 以写入要注入的dll的文件内容.
  • 仅枚举进程模块列表并不能发现注入的dll.
  • 概要:
    • 两种方式:
      • 镂空已有进程模块: 直接修改进程中已有模块的代码节, 注入恶意代码.
      • 先注入后镂空: 注入一个合法dll(拥有合法签名), 然后修改dll入口点处代码为自己想执行的代码.
    • 下面只讲先注入dll后镂空的方法.
  • 流程
    • 首先, 如普通的dll注入, CreateRemoteThread创建远程线程, 执行LoadLibrary注入一个dll, 不同的是注入到进程的是一个合法dll(比如system32目录下的dll).
    • EnumProcessModules枚举进程模块, GetModuleBaseNameA得到每个模块的名称, 从而找到注入的dll.
    • 注入器进程中分配0x1000的内存空间(可用mallocHeapAlloc), 然后将找到的dll的PE头部内容读进来.
    • 由PE头中的optional头得到目标dll的入口地址, 加上枚举模块时得到的dll的基址, 得到实际dll入口地址, 并用WriteProcessMemory向该地址写入shellcode.
      • 注: 通过windbg调试发现, 注入入口地址不一定能成功执行shellcode, 因为DllMain函数可能多次执行. 如果只想执行一次shellcode, 可把shellcode及其写在dll的末尾对齐空间.
      • 如同反射式dll注入, 生成shellcode的方法也是在注入器中定义shellcode函数并获取其机器码.
    • 创建远程线程, 并以写入shellcode的地址为线程执行地址.
  • 优点
    • 无需将恶意dll保存在磁盘中, 可躲避静态查杀.
  • 两种方式:
    • 镂空已有进程模块: 直接修改进程中已有模块的代码节, 注入恶意代码.
    • 先注入后镂空: 注入一个合法dll(拥有合法签名), 然后修改dll入口点处代码为自己想执行的代码.
  • 下面只讲先注入dll后镂空的方法.
  • 镂空已有进程模块: 直接修改进程中已有模块的代码节, 注入恶意代码.
  • 先注入后镂空: 注入一个合法dll(拥有合法签名), 然后修改dll入口点处代码为自己想执行的代码.
  • 首先, 如普通的dll注入, CreateRemoteThread创建远程线程, 执行LoadLibrary注入一个dll, 不同的是注入到进程的是一个合法dll(比如system32目录下的dll).
  • EnumProcessModules枚举进程模块, GetModuleBaseNameA得到每个模块的名称, 从而找到注入的dll.
  • 注入器进程中分配0x1000的内存空间(可用mallocHeapAlloc), 然后将找到的dll的PE头部内容读进来.
  • 由PE头中的optional头得到目标dll的入口地址, 加上枚举模块时得到的dll的基址, 得到实际dll入口地址, 并用WriteProcessMemory向该地址写入shellcode.
    • 注: 通过windbg调试发现, 注入入口地址不一定能成功执行shellcode, 因为DllMain函数可能多次执行. 如果只想执行一次shellcode, 可把shellcode及其写在dll的末尾对齐空间.
    • 如同反射式dll注入, 生成shellcode的方法也是在注入器中定义shellcode函数并获取其机器码.
  • 创建远程线程, 并以写入shellcode的地址为线程执行地址.
  • 注: 通过windbg调试发现, 注入入口地址不一定能成功执行shellcode, 因为DllMain函数可能多次执行. 如果只想执行一次shellcode, 可把shellcode及其写在dll的末尾对齐空间.
  • 如同反射式dll注入, 生成shellcode的方法也是在注入器中定义shellcode函数并获取其机器码.
  • 无需将恶意dll保存在磁盘中, 可躲避静态查杀.
  1. 注: 准确地说不是提权, 而是将访问令牌中禁用的权限启用. 成功调用下面几个函数的前提是进程具备该权限, 只是访问令牌中没有启用该权限. 而如果进程没有该权限, 则使用下面的函数后再调用GetLastError会返回ERROR_NOT_ALL_ASSIGN.
  2. 先用LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid)得到用户的debug权限.
  3. 然后用OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,&hToken)获取进程的令牌句柄.
  4. 最后用AdjustTokenPrivileges启用特权.
  • 使用vs2019编写注入器程序, 在生成的注入器可用前, 踩了不少坑, 因此记录一下.
  • 本文涉及三种恶意代码注入方法: 直接dll注入, 反射式dll注入, 镂空注入. 之所以选这三种注入方法, 是因最近在做一个检测进程内存空间以期发现代码注入的程序, 而实验发现这三种方法对目标进程的改变各有特点:
    • 直接dll注入: 还有APC注入, 本质都是在目标进程中执行LoadLibrary函数, 因而在枚举目标进程的模块列表时可看到注入的dll.
    • 反射式dll注入: 这种方法也会在目标进程中开辟新的内存空间并写入代码. 但因为没有调用LoadLibrary, 所以**枚举目标进程的模块列表并不能看到注入的dll. (意味着目标进程的PEB没有变化). **
    • 镂空注入: 直接改进程中某一模块的内存空间, 或者先注入一个合法模块, 再镂空该模块. **因为该方法没有注入恶意dll, 所以直接枚举目标进程的模块列表也无法发现恶意代码, 需扫描进程的内存空间. **

  • [招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

    最后于 2023-8-11 14:06 被wx_Niatruc编辑 ,原因: 修改错误表述
    收藏
    免费 16
    支持
    分享
    打赏 + 50.00雪花
    打赏次数 1 雪花 + 50.00
     
    赞赏  Editor   +50.00 2022/08/29 恭喜您获得“雪花”奖励,安全圈有你而精彩!
    最新回复 (10)
    雪    币: 2096
    活跃值: (1781)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    2
    收藏 了。
    2022-8-22 21:41
    0
    雪    币: 9771
    活跃值: (2596)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    3
    收藏备用
    2022-8-23 19:04
    0
    雪    币: 38
    活跃值: (1931)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    4
    厉害厉害
    2022-8-25 11:33
    0
    雪    币: 407
    活跃值: (501)
    能力值: ( LV3,RANK:20 )
    在线值:
    发帖
    回帖
    粉丝
    5
    楼主能讲下 禁用栈保护在哪不?找了半天没找到
    2022-8-30 16:59
    0
    雪    币: 853
    活跃值: (1417)
    能力值: ( LV3,RANK:30 )
    在线值:
    发帖
    回帖
    粉丝
    6
    kardpan 楼主能讲下 禁用栈保护在哪不?找了半天没找到
    项目属性 -> C/C++ -> 所有选项 -> 安全检查
    2022-8-30 19:45
    0
    雪    币: 407
    活跃值: (501)
    能力值: ( LV3,RANK:20 )
    在线值:
    发帖
    回帖
    粉丝
    7
    wx_Niatruc 项目属性 -> C/C++ -> 所有选项 -> 安全检查
    关了,但是开新线程还是起不来,jmp过去又是正常的。观察了下dll的入口点,多了__security_init_cookie这个函数。不知道楼主有没遇到这种情况。
    2022-8-31 09:01
    0
    雪    币: 853
    活跃值: (1417)
    能力值: ( LV3,RANK:30 )
    在线值:
    发帖
    回帖
    粉丝
    8
    kardpan 关了,但是开新线程还是起不来,jmp过去又是正常的。观察了下dll的入口点,多了__security_init_cookie这个函数。不知道楼主有没遇到这种情况。
    dll中有__security_cookie没问题的,我说的是shellcode中要去除__security_cookie. 新线程起不来的话,你可以用windbg调试一下目标进程看看,最有可能存在的问题是内存访问冲突. 
    2022-8-31 19:49
    0
    雪    币: 3448
    活跃值: (2658)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    9
    楼主可以提供一下相关的资料链接吗
    2023-12-21 11:53
    0
    雪    币: 853
    活跃值: (1417)
    能力值: ( LV3,RANK:30 )
    在线值:
    发帖
    回帖
    粉丝
    10
    wx_荣仔 楼主可以提供一下相关的资料链接吗
    抱歉,当时谷歌了一堆,没有整理我只记得redteam note(https://www.ired.team/offensive-security/code-injection-process-injection),他里面整理了很多实用的方法
    2023-12-26 11:20
    0
    雪    币: 24
    活跃值: (90)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    11
    收藏备用
    2023-12-26 11:48
    0
    游客
    登录 | 注册 方可回帖
    返回
    //