在Windows平台下,应用程序为了保护自己不被调试器调试会通过各种方法限制进程调试自身,通常此类反调试技术会限制我们对其进行软件逆向与漏洞分析,下面是一些常见的反调试保护方法:
- IsDebuggerPresent:检查当前程序是否在调试器环境下运行。
- OutputDebugString:向调试器发送特定的字符串,以检查是否有调试器在运行。
- CloseHandle:检查特定的句柄是否关闭,以判断是否有调试器在运行。
- GetTickCount:检查程序运行的时间,以判断是否有调试器在运行。
- PEB (Process Environment Block):检查PEB数据结构中的特定字段,以判断是否有调试器在运行。
- SEH (Structured Exception Handling):检查异常处理程序是否被替换,以判断是否有调试器在运行。
我们以第一种IsDebuggerPresent
反调试为例,该函数用于检查当前程序是否在调试器的环境下运行。函数返回一个布尔值,如果当前程序正在被调试,则返回True,否则返回False。
函数通过检查特定的内存地址来判断是否有调试器在运行。具体来说,该函数检查了PEB(进程环境块)
数据结构中的_PEB_LDR_DATA
字段,该字段标识当前程序是否处于调试状态。如果该字段的值为1,则表示当前程序正在被调试,否则表示当前程序没有被调试。
获取PEB的方式有许多,虽然LyScript插件内提供了get_peb_address(dbg.get_process_id())
系列函数可以直接获取到进程的PEB信息,但为了分析实现原理,笔者首先会通过代码来实现这个功能;
如下代码,通过在目标程序中创建一个堆空间并向其中写入汇编指令,最后将程序的EIP寄存器设置为堆空间的首地址,以使得程序运行时执行堆空间中的汇编指令。
具体来说,该代码通过调用MyDebug
类的create_alloc
方法创建一个堆空间,并通过调用assemble_at
方法向堆空间写入汇编指令。该代码先写入mov eax,fs:[0x30]
指令,该指令将FS寄存器
的值加上0x30
的偏移量存入EAX
寄存器,从而得到_PEB
数据结构的地址。
然后,代码再写入mov eax,[eax+0x0C]
指令,该指令将EAX寄存器加上0x0C
的偏移量后的值存入EAX寄存器,从而得到_PEB_LDR_DATA
数据结构的地址。最后,写入jmp eip
指令,以使得程序回到原来的EIP
位置。最后,代码通过调用set_register
方法设置EIP寄存器的值为堆空间的首地址,以使得程序运行时执行堆空间中的汇编指令。
from LyScript32 import MyDebug
if __name__ == "__main__":
dbg = MyDebug(address="127.0.0.1")
dbg.connect()
# 保存当前EIP
eip = dbg.get_register("eip")
# 创建堆
heap_addres = dbg.create_alloc(1024)
print("堆空间地址: {}".format(hex(heap_addres)))
# 写出汇编指令
# mov eax,fs:[0x30] 得到 _PEB
dbg.assemble_at(heap_addres,"mov eax,fs:[0x30]")
asmfs_size = dbg.get_disasm_operand_size(heap_addres)
# 写出汇编指令
# mov eax,[eax+0x0C] 得到 _PEB_LDR_DATA
dbg.assemble_at(heap_addres + asmfs_size, "mov eax, [eax + 0x0C]")
asmeax_size = dbg.get_disasm_operand_size(heap_addres + asmfs_size)
# 跳转回EIP位置
dbg.assemble_at(heap_addres+ asmfs_size + asmeax_size , "jmp {}".format(hex(eip)))
# 设置EIP到堆首地址
dbg.set_register("eip",heap_addres)
dbg.close()
当这段读入汇编指令被执行时,此时PEB入口
地址将被返回给EAX
寄存器,用户只需要取出该寄存器中的参数即可实现读取进程PEB
的功能。
当PEB入口地址得到之后,只需要检查PEB+2
的位置标志,通过write_memory_byte()
函数向此处写出0即可绕过反调试,从而让程序可以被正常调试。
from LyScript32 import MyDebug
if __name__ == "__main__":
# 初始化
dbg = MyDebug()
dbg.connect()
# 通过PEB找到调试标志位
peb = dbg.get_peb_address(dbg.get_process_id())
print("调试标志地址: 0x{:x}".format(peb+2))
flag = dbg.read_memory_byte(peb+2)
print("调试标志位: {}".format(flag))
# 将调试标志设置为0即可过掉反调试
nop_debug = dbg.write_memory_byte(peb+2,0)
print("反调试绕过状态: {}".format(nop_debug))
dbg.close()
这里笔者继续拓展一个新知识点,如何实现绕过进程枚举功能,病毒会利用进程枚举函数Process32FirstW
及Process32NextW
枚举所有运行的进程以确认是否有调试器在运行,我们可以在特定的函数开头处写入SUB EAX,EAX RET
指令让其无法调用枚举函数从而失效,写入汇编指令集需要依赖于set_assemble_opcde
函数,只需要向函数内传入内存地址,则自动替换地址处的汇编指令集;
from LyScript32 import MyDebug
# 得到所需要的机器码
def set_assemble_opcde(dbg,address):
# 得到第一条长度
opcode_size = dbg.assemble_code_size("sub eax,eax")
# 写出汇编指令
dbg.assemble_at(address, "sub eax,eax")
dbg.assemble_at(address + opcode_size , "ret")
if __name__ == "__main__":
# 初始化
dbg = MyDebug()
dbg.connect()
# 得到函数所在内存地址
process32first = dbg.get_module_from_function("kernel32","Process32FirstW")
process32next = dbg.get_module_from_function("kernel32","Process32NextW")
print("process32first = 0x{:x} | process32next = 0x{:x}".format(process32first,process32next))
# 替换函数位置为sub eax,eax ret
set_assemble_opcde(dbg, process32first)
set_assemble_opcde(dbg, process32next)
dbg.close()
当上述代码被运行后,则Process32FirstW
与Process32FirstW
函数位置将被依次写出返回指令,从而让进程枚举失效,输出效果图如下所示;