CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Shellcode实战 专栏说明
  • shellcodisation在病毒学中的应用
  • 编写shellcode
    • 2.1 分析编译器生成的二进制数据
    • 2.2 shellcode生成原理
      • 2.2.1 第一种方法:汇编代码修补
      • 2.2.2 第二种方法:使用栈
      • 2.2.3 第三种方法:使用全局数据
  • WiShMaster:shellcodisation过程
  • 使用WiShMaster开发应用程序
  • 未来工作
  • 总结
目录

编写shellcode

# 2 编写shellcode

# 2.1 分析编译器生成的二进制数据

要将恶意代码转化为shellcode,可直接使用汇编语言(assembly)编写源代码。但如果程序规模较大,这项工作很快会变得冗长且枯燥。后续我们将摒弃这种方案,转而考虑使用C语言编写源代码。

第一个思路可能是:先用C语言编写程序,对其进行编译(compile),从可执行文件(executable)中提取二进制数据(binary data),再将这些数据组合成shellcode。

我们以一个简单的测试程序(后文称为“simpletest”)为例,该程序仅用于打印消息(print messages)并显示文件“test.txt”的内容。

以下是部分代码片段:

文件 user.h

#define SIZE_USERNAME 32
#define SIZE_PASSWORD 32

typedef struct _USER {
    CHAR szUsername[SIZE_USERNAME];
    CHAR szPassword[SIZE_PASSWORD];
} USER, *PUSER;
1
2
3
4
5
6
7

文件 display.cpp

CHAR g_szMessage[] = "This is a message stored as a global variable";

VOID DisplayMessage(IN CHAR * szMessage)
{
    PrintMsg(LOG_LEVEL_TRACE, ">>> %s <<<", szMessage);
}

BOOL DisplayFile(IN CHAR * szFilePath)
{
    ...
    CreateFile(szFilePath, ...);
    pData = (UCHAR *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize + 1);
    ReadFile(hFile, pData, ...);
    PrintMsg(LOG_LEVEL_TRACE, "File successfully read: %s", pData);
    ...
}

BOOL DisplayData(VOID)
{
    DisplayMessage(g_szMessage);
    PrintMsg(LOG_LEVEL_TRACE, "Username: %s", g_User.szUsername);
    PrintMsg(LOG_LEVEL_TRACE, "Password: %s", g_User.szPassword);
    if (DisplayFile("test.txt") == FALSE)
        return FALSE;
    return TRUE;
}
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

文件 main.cpp

USER g_User = {"jmerchat", "password"};

BOOL DisplayData(VOID);

int main(int argc, char * argv[])
{
    DisplayData();
    return 0;
}
1
2
3
4
5
6
7
8
9

文件 print_msg.cpp

VOID PrintMsg(IN UINT uiMessageLevel, IN const CHAR * fmt, ...)
{
    CHAR szBuffer[SIZE_OF_LOCAL_LOG_BUFFER + 1];

    UINT i = 0;
    if (uiMessageLevel == LOG_LEVEL_ERROR)
        i += _snprintf(&szBuffer[i], SIZE_OF_LOCAL_LOG_BUFFER - i, "[ERROR]   :  ");
    else if (uiMessageLevel == LOG_LEVEL_WARNG)
        ...

    va_list ap;
    va_start(ap, fmt);
    i += _vsnprintf(&szBuffer[i], SIZE_OF_LOCAL_LOG_BUFFER - i, fmt, ap);
    va_end(ap);

    printf("[%4d] %s\n", GetCurrentThreadId(), szBuffer);
    fflush(stdout);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

该程序包含以下内容:

  • 两个全局变量(global variable):
    • 全局变量“g_User”,在main.cpp中声明并初始化(initialized),“User”是在“user.h”中定义的“USER”结构体(structure)类型;
    • 全局字符串(global string)“g_szMessage”;
  • 五个内部函数(internal function):
    • “DisplayMessage”:用于显示“g_szMessage”;
    • “DisplayFile”:尝试打开文件“test.txt”并显示其内容;
    • “DisplayData”:实际执行所有操作的函数;
    • “main”:程序入口点(entry point),仅调用“DisplayData”;
    • “PrintMsg”:用于显示日志消息(log message);
  • 多个字符串(string);
  • 多次调用导入函数(imported function):创建文件函数(CreateFile)、堆分配函数(HeapAlloc)、字符串格式化函数(_snprintf)等。

这个程序实用性不强,但它汇集了C程序中可能出现的各种元素:全局数据(global data)、字符串、带参数或不带参数的内部函数、导入函数等。

现在,我们将编译该程序,并分析生成的二进制数据:

函数 DisplayMessage 的调用(Call of the function DisplayMessage)

DisplayMessage(g_szMessage);
00413119  68 00304300        PUSH 433000                     ; ASCII "This is a message ... "
0041311E  E8 E8E7FFFF        CALL 0041190B                 ; call jmp on DisplayMessage
00413123  83C4 04            ADD ESP,4
1
2
3
4

函数 DisplayFile 的调用(Call of the function DisplayFile)

if (DisplayFile("test.txt") == FALSE)
00413152  68 CCFF4200        PUSH 42FFCC                     ; ASCII "test.txt"
00413157  E8 26E4FFFF        CALL 00411582                 ; call DisplayFile
1
2
3

函数 CreateFile 的调用(Call of the function CreateFile)

CreateFile(szFilePath, ...)
...
00412FD2  50                  PUSH EAX
00412FD3  FF15 14724300       CALL DWORD PTR DS:[437214]  ; call kernel32.CreateFileA
1
2
3
4

有几个因素导致我们无法直接使用这段二进制代码创建shellcode:

  • 存在大量硬编码地址(hardcoded address):每次引用字符串或全局数据都会生成硬编码地址;
  • 内部函数调用是相对的(relative),但指令中包含的值是下一条指令地址与被调用函数入口点(entry point of function)的差值。因此,我们必须保持内部函数之间的相对位置不变。这可能会带来问题,因为如果编译器(compiler)在两个必须包含在shellcode中的函数之间插入了一些无用的函数或数据,我们就必须维持这些函数之间的距离。此外,内部函数调用不会直接跳转到函数入口点,而是跳转到位于第一个节(section)开头的跳转指令(jmp instruction):

函数 DisplayMessage 的调用(Call of the function DisplayMessage)

0041190B  E9 50160000        JMP 00412F60                ; call DisplayMessage
1

不过,编译器(compiler)或许有一个选项可以禁用这种行为。

  • 导入函数调用是通过跳转到导入地址表(Import Address Table)中某个条目的值来实现的。这里同样存在硬编码地址。此外,由于Windows使用早期绑定机制(early binding mechanism),所有必需的库(required library)必须已加载(loaded),所有导入函数必须已解析(resolved),且导入地址表必须已填充(filled)。

总而言之,我们发现目前的二进制代码距离可用于生成shellcode的要求还相差甚远。

# 2.2 shellcode生成原理

要获取可用于生成shellcode的二进制代码,我们需要解决两个问题:

  • 找到一种方法,强制编译器生成不包含硬编码地址的代码;

  • 找到一种方法,动态解析(resolve)导入函数。

# 2.2.1 第一种方法:汇编代码修补

第一种解决方案可能是:利用编译器生成汇编代码(assembly),使用转换工具(transformation tool)对其进行修补(patch),最后生成二进制数据。

然而,这种方法存在几个问题:

  • 首先,编译器生成的汇编代码包含大量硬编码地址,因此需要对其进行大量修改;
  • 其次,开发转换工具需要对汇编语言进行操作,而汇编语言并非一种通用语言;
  • 最后,转换工具将与特定的汇编代码绑定,进而与特定的硬件平台(hardware platform)绑定。

因此,我决定不在汇编语言层面进行操作,而是直接对C代码进行处理。

# 2.2.2 第二种方法:使用栈

第二种解决方案是按照特定方式编写C代码,使所有操作都在栈(stack)中进行。

例如,可以在栈中重建字符串:

使用栈存储字符串(Use of the stack to store a string)

CHAR szStrUsername[] = {'U', 's', 'e', 'r', 'n', 'a', 'm', 'e', ':', ' ', '%', 's'};
004130DA  C645 F4 55        MOV BYTE PTR SS:[EBP-C],55
004130DE  C645 F5 73        MOV BYTE PTR SS:[EBP-B],73
004130E2  C645 F6 65        MOV BYTE PTR SS:[EBP-A],65
004130E6  C645 F7 72        MOV BYTE PTR SS:[EBP-9],72
...
1
2
3
4
5
6

要调用导入函数,可使用动态地址解析(dynamic address resolution)在本地函数指针(local function pointer)中获取所需函数的地址:

使用动态地址解析调用导入函数(Use of dynamic address resolution to call an imported function)

hLib = LoadLibrary(szStrLibraryName);
pFunc = (FunctionTypeDef)GetProcAddress(hLib, szStrFunctionName);
pFunc(...);
1
2
3

然而,这种技术存在几个问题:

  • 按照这种方式编写代码很快会变得枯燥乏味;

  • 它无法解决内部函数的调用问题;

  • 如果我们从互联网等渠道获取了一些正常编写的有用代码,就必须重写该代码的大部分内容以适应这种方式。

# 2.2.3 第三种方法:使用全局数据

第三种解决方案是使用一个结构体(structure)来存储所有全局数据,并在每次内部函数调用时传递该结构体。这个结构体(后文称为“GLOBAL_DATA”)将包含:

  • 内部函数指针(Pointer on internal functions);
  • 导入函数指针(Pointer on imported functions);
  • 全局变量(Global variables);
  • 字符串(Strings)。

定义好该结构体后,我们对C代码进行修改,使得所有对上述元素的引用都通过该结构体实现。

以函数“DisplayFile”为例:

原始的 DisplayFile 函数(Original function DisplayFile)

BOOL DisplayFile(IN CHAR * szFilePath)
{
    ...
    CreateFile(szFilePath, ...);
    pData = (UCHAR *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize + 1);
    ReadFile(hFile, pData, ...);
    PrintMsg(LOG_LEVEL_TRACE, "File successfully read: %s", pData);
    ...
}
1
2
3
4
5
6
7
8
9

修改后(修改部分用红色标注):

修补后的 DisplayFile 函数(Patched function DisplayFile)

BOOL DisplayFile(IN PGLOBAL_DATA pGlobalData, IN CHAR * szFilePath)
{
    ...
    pGlobalData->CreateFile(szFilePath, ...);
    pData = (UCHAR *)pGlobalData->HeapAlloc(pGlobalData->GetProcessHeap(),
        HEAP_ZERO_MEMORY, dwFileSize + 1);
    pGlobalData->ReadFile(hFile, pData, ...);
    pGlobalData->PrintMsg(pGlobalData, LOG_LEVEL_TRACE, pGlobalData->szString_00000001,
        pData);
    ...
}
1
2
3
4
5
6
7
8
9
10
11

GLOBAL_DATA 的定义如下:

GLOBAL_DATA 结构体概述(Overview of structure GLOBAL_DATA)

typedef struct _GLOBAL_DATA {
    /* Internal functions */
    PrintMsgTypeDef fp_PrintMsg;

    /* Imported functions */
    CreateFileTypeDef fp_CreateFile;
    HeapAllocTypeDef fp_HeapAlloc;
    GetProcessHeapTypeDef fp_GetProcessHeap;
    ReadFileTypeDef fp_ReadFile;

    /* Data strings */
    CHAR szString_00000001[27];

} GLOBAL_DATA, *PGLOBAL_DATA;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

通过利用C语言宏(C macro)的强大功能,我们可以大幅减少对源代码的修改量。我们定义以下宏:

宏定义(Definitions of macros)

/* 在内部函数定义中添加 GLOBAL_DATA 参数 */
#define DisplayFileTempDefinition(...)  \
    DisplayFileDefinition(PGLOBAL_DATA pGlobalData, __VA_ARGS__)

/* 在内部函数调用中添加重定向和 GLOBAL_DATA 参数 */
#define PrintMsg(...)         pGlobalData->fp_PrintMsg(pGlobalData, __VA_ARGS__)
#define DisplayFile(...)   	  pGlobalData->fp_DisplayFile(pGlobalData, __VA_ARGS__)

/* 为导入函数添加重定向 */
#define CreateFile                  pGlobalData->fp_CreateFile
#define HeapAlloc                   pGlobalData->fp_HeapAlloc
#define GetProcessHeap       		pGlobalData->fp_GetProcessHeap
#define ReadFile                    pGlobalData->fp_ReadFile

/* 为字符串添加重定向 */
#define STR_00000001(x)      pGlobalData->szString_00000001
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

修改后的代码如下:

使用宏修补后的 DisplayFile 函数(Patched function DisplayFile with the macros)

BOOL DisplayFileTempDefinition(IN CHAR * szFilePath)
{
    ...
    CreateFile(szFilePath, ...);
    pData = (UCHAR *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize + 1);
    ReadFile(hFile, pData, ...);
    PrintMsg(LOG_LEVEL_TRACE, STR_00000001("File successfully read: %s"), pData);
    ...
}
1
2
3
4
5
6
7
8
9

可以看到,现在需要修改的内容非常少。让我们分析生成的汇编代码:

函数 DisplayMessage 的调用(Call of the function DisplayMessage)

DisplayMessage(g_szMessage);
00412F99  8B45 08            MOV EAX,DWORD PTR SS:[EBP+8]     ; 获取 GLOBAL_DATA 的地址
00412F9C  05 58010000        ADD EAX,158                      ; 获取 g_szMessage 的地址
00412FA1  50                 PUSH EAX                         ; 压入 g_szMessage 的地址
00412FA2  8B4D 08            MOV ECX,DWORD PTR SS:[EBP+8]     ; 获取 pGlobalData 的地址
00412FA5  51                 PUSH ECX                         ; 压入 pGlobalData 的地址
00412FA6  8B55 08            MOV EDX,DWORD PTR SS:[EBP+8]     ; 获取 DisplayMessage 的地址
00412FA9  8B82 88000000      MOV EAX,DWORD PTR DS:[EDX+88]
00412FAF  FFD0               CALL EAX                         ; 调用 DisplayMessage
1
2
3
4
5
6
7
8
9

函数 CreateFile 的调用(Call of the function CreateFile)

CreateFile(szFilePath, ...)
...
00412DE2  8B4D 08            MOV ECX,DWORD PTR SS:[EBP+8]     ; 获取 pGlobalData 的地址
00412DE5  8B91 D8000000      MOV EDX,DWORD PTR DS:[ECX+D8]    ; 获取 CreateFile 的地址
00412DEB  FFD2               CALL EDX                         ; 调用 CreateFile
1
2
3
4
5

函数 DisplayFile 的调用(Call of the function DisplayFile)

if (DisplayFile("test.txt") == FALSE)
00412FFC  8B45 08            MOV EAX,DWORD PTR SS:[EBP+8]     ; 获取 pGlobalData 的地址
00412FFF  05 A1040000        ADD EAX,4A1                      ; 获取字符串的地址
00413004  50                 PUSH EAX                         ; 压入字符串的地址
00413005  8B4D 08            MOV ECX,DWORD PTR SS:[EBP+8]     ; 获取 pGlobalData 的地址
00413008  51                 PUSH ECX                         ; 压入 pGlobalData 的地址
00413009  8B55 08            MOV EDX,DWORD PTR SS:[EBP+8]
0041300C  8B42 78            MOV EAX,DWORD PTR DS:[EDX+78]    ; 获取 DisplayFile 的地址
0041300F  FFD0               CALL EAX                         ; 调用 DisplayFile
1
2
3
4
5
6
7
8
9

生成的二进制文件不再包含任何硬编码地址!我们可以提取这段二进制代码,并用它来生成shellcode。

只需将提取的函数拼接起来,并在末尾添加GLOBAL_DATA结构体,即可创建shellcode。图7展示了该结构的概述。

img

图7:shellcode结构概述(Figure 7: Overview of the structure of the shellcode)

这是第一步;然而,仍然有几个问题需要解决:

  • 编写GLOBAL_DATA结构体的定义是一项冗长且枯燥的任务;
  • 必须添加一些代码来初始化(initialize)该结构体;
  • 必须从编译生成的可执行文件中提取二进制数据,并将其组装(assemble)以创建最终的shellcode。

因此,我决定编写一个工具来自动执行所有这些操作:WiShMaster。

shellcodisation在病毒学中的应用
WiShMaster:shellcodisation过程

← shellcodisation在病毒学中的应用 WiShMaster:shellcodisation过程→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式