钩子劫持技术是计算机编程中的一种技术,它们可以让开发者拦截系统函数或应用程序函数的调用,并在函数调用前或调用后执行自定义代码,钩子劫持技术通常用于病毒和恶意软件,也可以让开发者扩展或修改系统函数的功能,从而提高软件的性能和增加新功能。
4.5.1 探索反汇编写出函数原理
钩子劫持技术的实现一般需要在对端内存中通过create_alloc()
函数准备一块空间,并通过assemble_write_memory()
函数,将一段汇编代码转为机器码,并循环写出自定义指令集到堆中,函数write_opcode_from_assemble()
就是我们自己实现的,该函数传入一个汇编指令列表,自动转为机器码并写出到堆内,函数的核心代码如下所示。
def write_opcode_from_assemble(dbg_ptr,asm_list): addr_count = 0 addr = dbg_ptr.create_alloc(1024) if addr != 0: for index in asm_list: asm_size = dbg_ptr.assemble_code_size(index) if asm_size != 0: write = dbg_ptr.assemble_write_memory(addr + addr_count, index) if write == True: addr_count = addr_count + asm_size else: dbg_ptr.delete_alloc(addr) return 0 else: dbg_ptr.delete_alloc(addr) return 0 else: return 0 return addr
|
我们以写出一段MessageBox
弹窗代码为例,首先通过get_module_from_function
函数获取到位于user32.dll
模块内MessageBoxA
的函数地址,该函数的栈传参数为五个,其中前四个为push
压栈,最后一个则是调用call
,为了构建这个指令集需要在asm_list
写出所需参数列表及调用函数地址,并通过set_local_protect
设置可执行属性,通过set_register
将当前EIP设置到写出位置,并执行程序。
from LyScript32 import MyDebug
def write_opcode_from_assemble(dbg_ptr,asm_list): pass
if __name__ == "__main__": dbg = MyDebug() dbg.connect()
msg_ptr = dbg.get_module_from_function("user32.dll","MessageBoxA") call = "call {}".format(str(hex(msg_ptr))) print("函数地址: {}".format(call))
asm_list = ['push 0','push 0','push 0','push 0',call] write_addr = write_opcode_from_assemble(dbg,asm_list) print("写出地址: {}".format(hex(write_addr)))
dbg.set_local_protect(write_addr,32,1024)
dbg.set_register("eip",write_addr)
dbg.set_debug("Run") dbg.close()
|
运行上述代码片段,则首先会在0x3130000
的位置处写出调用MessageBox
的指令集。
当执行set_debug("Run")
则会执行如下图所示代码,这些代码则是经过填充的,由于此处仅仅只是一个演示案例,所以不具备任何实战性,读者在该案例中学会指令的替换是如何实现的即可;
4.5.2 实现Hook改写MsgBox弹窗
在之前的内容中笔者通过封装write_opcode_from_assemble
函数实现了自定义写出内存的功能,本章将继续探索Hook劫持技术的实现原理,如下案例中我们先来实现一个Hook
通用模板,在代码中实现中转机制,代码中以MessageBoxA
函数为案例实现修改汇编参数传递。
from LyScript32 import MyDebug
def assemble(dbg, address=0, asm_list=[]): asm_len_count = 0 for index in range(0,len(asm_list)): dbg.assemble_at(address, asm_list[index]) asm_len_count = dbg.assemble_code_size(asm_list[index]) address = address + asm_len_count
if __name__ == "__main__": dbg = MyDebug() connect_flag = dbg.connect() print("连接状态: {}".format(connect_flag))
messagebox_address = dbg.get_module_from_function("user32.dll","MessageBoxA") print("MessageBoxA内存地址 = {}".format(hex(messagebox_address)))
HookMem = dbg.create_alloc(1024) print("自定义内存空间: {}".format(hex(HookMem)))
asm = [ f"push {hex(HookMem)}", "ret" ]
assemble(dbg,messagebox_address,asm)
dbg.close()
|
如上代码中,通过找到user32.dll
库中的MessageBoxA
函数,并返回其内存地址。接着,程序会分配1024
字节大小的自定义内存空间,获取刚刚写入的内存地址,并将其写入到MessageBoxA
函数的内存地址中,代码运行后读者可看到如下图所示的提示信息;
提示:解释一下为什么需要增加asm
列表中的指令集,此处的指令集作用只有一个那就是跳转,当原始MessageBoxA
函数被调用时,则此处通过push;ret
的组合跳转到我们自定义的HookMem
内存空间中,而此内存空间中后期则需要填充我们自己的弹窗代码片段,所以需要提前通过HookMem = dbg.create_alloc(1024)
构建出这段内存区域;
由于MessageBox
弹窗需要使用两个变量这两个变量依次代表标题和内容,所以我们通过create_alloc
函数在对端内存中分配两块堆空间,并依次将弹窗字符串通过write_memory_byte
写出到内存中,至此弹窗内容也算填充好了,其中txt
代表标题,而box
则代表内容;
MsgBoxAddr = dbg.create_alloc(512) MsgTextAddr = dbg.create_alloc(512)
txt = [0x6c, 0x79, 0x73, 0x68, 0x61, 0x72, 0x6b]
box = [0x6C, 0x79, 0x73, 0x68, 0x61, 0x72, 0x6B, 0x2E, 0x63, 0x6F, 0x6D]
for txt_count in range(0,len(txt)): dbg.write_memory_byte(MsgBoxAddr + txt_count, txt[txt_count])
for box_count in range(0,len(box)): dbg.write_memory_byte(MsgTextAddr + box_count, box[box_count])
print("标题地址: {} 内容: {}".format(hex(MsgBoxAddr),hex(MsgTextAddr)))
|
紧接着,我们需要跳转到MessageBoxA
函数所在内存中,并提取出该函数调用时的核心汇编指令集,如下图所示则是弹窗的具体实现流程;
而对于一个完整的弹窗来说,只需要提取出核心代码即可不必提取所有指令集,但需要注意的是图中的call 0x75B20E20
地址需要进行替换,根据系统的不同此处的地址也不会相同,在提取时需要格外注意;
PatchCode =\ [ "mov edi, edi", "push ebp", "mov ebp,esp", "push -1", "push 0", "push dword ptr ss:[ebp+0x14]", f"push {hex(MsgBoxAddr)}", f"push {hex(MsgTextAddr)}", "push dword ptr ss:[ebp+0x8]", "call 0x75B20E20", "pop ebp", "ret 0x10" ]
assemble(dbg, HookMem, PatchCode)
|
如上则是替换弹窗的代码解释,将这段代码整合在一起,读者则可实现一段替换弹窗功能的代码,如下弹窗中的消息替换成我们自己的版权信息,此处完整代码实现如下所示;
from LyScript32 import MyDebug
def assemble(dbg, address=0, asm_list=[]): asm_len_count = 0 for index in range(0,len(asm_list)): dbg.assemble_at(address, asm_list[index]) asm_len_count = dbg.assemble_code_size(asm_list[index]) address = address + asm_len_count
if __name__ == "__main__": dbg = MyDebug() connect_flag = dbg.connect() print("连接状态: {}".format(connect_flag))
messagebox_address = dbg.get_module_from_function("user32.dll","MessageBoxA") print("MessageBoxA内存地址 = {}".format(hex(messagebox_address)))
HookMem = dbg.create_alloc(1024) print("自定义内存空间: {}".format(hex(HookMem)))
asm = [ f"push {hex(HookMem)}", "ret" ]
assemble(dbg,messagebox_address,asm)
MsgBoxAddr = dbg.create_alloc(512) MsgTextAddr = dbg.create_alloc(512)
txt = [0x6c, 0x79, 0x73, 0x68, 0x61, 0x72, 0x6b] box = [0x6C, 0x79, 0x73, 0x68, 0x61, 0x72, 0x6B, 0x2E, 0x63, 0x6F, 0x6D]
for txt_count in range(0,len(txt)): dbg.write_memory_byte(MsgBoxAddr + txt_count, txt[txt_count])
for box_count in range(0,len(box)): dbg.write_memory_byte(MsgTextAddr + box_count, box[box_count])
print("标题地址: {} 内容: {}".format(hex(MsgBoxAddr),hex(MsgTextAddr)))
PatchCode =\ [ "mov edi, edi", "push ebp", "mov ebp,esp", "push -1", "push 0", "push dword ptr ss:[ebp+0x14]", f"push {hex(MsgBoxAddr)}", f"push {hex(MsgTextAddr)}", "push dword ptr ss:[ebp+0x8]", "call 0x75B20E20", "pop ebp", "ret 0x10" ]
assemble(dbg, HookMem, PatchCode)
print("地址已被替换,可以运行了.") dbg.set_debug("Run") dbg.set_debug("Run")
dbg.close()
|
当如上代码被运行后,则会替换进程内MessageBoxA
函数为我们自己的地址,运行输出效果如下图所示;
读者可通过Ctrl+G
并输入MessageBoxA
跳转到原函数弹窗位置,此时输出的则是一个跳转地址0x6C0000
该地址则代表我们自己的自定义内存区域,如下图所示;
继续跟进这内存区域,读者可看到我们自己构建的MessageBoxA
弹窗的核心代码片段,当这段代码被执行结束后则通过ret
会返回到程序领空,如下图所示;
至此,当用户再次打开弹窗按钮时,则不会提示原始内容,而是提示自定义弹窗,如下图所示;