LyScript实现Hook隐藏调试器的方法详解

2022-10-07,,,,

lyscript 插件集成的内置api函数可灵活的实现绕过各类反调试保护机制,前段时间发布的那一篇文章并没有详细讲解各类反调试机制的绕过措施,本次将补充这方面的知识点,运用lyscript实现绕过大多数通用调试机制,实现隐藏调试器的目的。

我们以此实现patches如下函数:

  • isdebuggerpresent
  • zwqueryinformationprocess
  • checkremotedebuggerpresent
  • peb.isdebugged
  • peb.processheap.flag
  • peb.ntglobalflag
  • peb.ldr 0xfeeefeee filling
  • gettickcount
  • zwquerysysteminformation
  • findwindowa
  • findwindoww
  • findwindowexa
  • findwindowexw
  • enumwindows

首先第一步我们需要自己封装实现一个反汇编转机器码的函数,其作用是当用户传入汇编列表时,自动将其转为机器码并输出为列表格式。

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    shellcode = getopcode(dbg, ["db 0x64","mov eax,dword ptr ds:[18]","sub eax,eax","ret"])

    print(shellcode)

    dbg.close()

输出效果如下:

patch_peb

peb结构存在许多反调试变量,首先我们需要先将这些变量填充为空。

# ----------------------------------------------
# by: lyshark
# email: me@lyshark.com
# project: https://github.com/lyshark/lyscript
# ----------------------------------------------

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

def patch_peb(dbg):
    peb = dbg.get_peb_address(dbg.get_process_id())
    if peb == 0:
        return 0

    # 写出0 patching peb.isdebugged
    dbg.write_memory_byte(peb + 0x2,getopcode(dbg,["db 0"])[0])
    print("补丁地址: {}".format(hex(peb+0x2)))

    # 写出0 patching peb.processheap.flag
    temp = dbg.read_memory_dword(peb + 0x18)
    temp += 0x10
    dbg.write_memory_dword(temp, getopcode(dbg,["db 0"])[0])
    print(("补丁地址: {}".format(hex(temp))))

    # 写出0 atching peb.ntglobalflag
    dbg.write_memory_dword(peb+0x68, 0)
    print(("补丁地址: {}".format(hex(peb+0x68))))

    # 循环替换 patch peb_ldr_data 0xfeeefeee fill bytes about 3000 of them
    addr = dbg.read_memory_dword(peb + 0x0c)

    while addr != 0:
        addr += 1

        try:
            b = dbg.read_memory_dword(addr)
            c = dbg.read_memory_dword(addr + 4)

            # 仅修补填充运行
            print(b)
            if (b == 0xfeeefeee) and (c == 0xfeeefeee):
                dbg.write_memory_dword(addr,0)
                dbg.write_memory_dword(addr + 4, 0)
                print("patch")
        except exception:
            break

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    patch_peb(dbg)
    
    dbg.close()

patch_isdebuggerpresent

该函数用于检测自身是否处于调试状态,其c系列代码如下所示,绕过此种方式很简单,只需要在函数头部写出ret指令即可。

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

int _tmain(int argc, _tchar* argv[])
{
	bool ref = isdebuggerpresent();
	printf("是否被调试: %d \n", ref);

	getchar();
	return 0;
}

注意:此api检查peb中的值,因此如果修补peb,则无需修补api,这段绕过代码如下。

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

def patch_isdebuggerpresent(dbg):
    # 得到模块句柄
    ispresent = dbg.get_module_from_function("kernel32.dll","isdebuggerpresent")
    print(hex(ispresent))

    if(ispresent <= 0):
        print("无法得到模块基地址,请以管理员方式运行调试器.")
        return 0

    # 将反调试语句转为机器码
    shellcode = getopcode(dbg, ["db 0x64", "mov eax,dword ptr ds:[18]", "sub eax,eax", "ret"])
    print(shellcode)

    flag = 0
    for index in range(0,len(shellcode)):
        flag = dbg.write_memory_byte(ispresent + index,shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0
    return flag

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    ref = patch_isdebuggerpresent(dbg)
    print("补丁状态: {}".format(ref))

    dbg.close()

当程序运行后会向isdebuggerpresent函数写出返回,从而实现绕过调试的目的。

patch_checkremotedebuggerpresent

此api调用zwqueryinformationprocess因此通常不需要对两者进行修补。

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

def patch_checkremotedebuggerpresent(dbg):
    # 得到模块句柄
    ispresent = dbg.get_module_from_function("kernel32.dll","checkremotedebuggerpresent")
    print(hex(ispresent))

    # 将反调试语句转为机器码
    shellcode = getopcode(dbg,
                          [
                              "mov edi,edi",
                              "push ebp",
                              "mov ebp,esp",
                              "mov eax,[ebp+0xc]",
                              "push 0",
                              "pop dword ptr ds:[eax]",
                              "xor eax,eax",
                              "pop ebp",
                              "ret 8"
                          ]
                          )

    print(shellcode)

    flag = 0
    for index in range(0,len(shellcode)):
        flag = dbg.write_memory_byte(ispresent + index,shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0
    return flag

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    ref = patch_checkremotedebuggerpresent(dbg)
    print("写出状态: {}".format(ref))

    dbg.close()

写出效果如下:

patch_gettickcount

gettickcount返回(retrieve)从操作系统启动所经过(elapsed)的毫秒数,常用于定时计数,绕过方式只需初始化即可。

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

def patch_gettickcount(dbg):
    # 得到模块句柄
    ispresent = dbg.get_module_from_function("kernel32.dll","gettickcount")
    print(hex(ispresent))

    # 将反调试语句转为机器码
    shellcode = getopcode(dbg,
                          [
                              "mov edx,0x7ffe0000",
                              "sub eax,eax",
                              "add eax,0xb0b1560d",
                              "ret"
                          ]
                          )

    print(shellcode)

    flag = 0
    for index in range(0,len(shellcode)):
        flag = dbg.write_memory_byte(ispresent + index,shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0
    return flag

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    ref = patch_gettickcount(dbg)
    print("写出状态: {}".format(ref))

    dbg.close()

写出效果如下:

patch_zwqueryinformationprocess

此函数打补丁需要跳转两次,原因是因为函数开头部分无法填充更多指令,需要我们自己来申请空间,并实现跳转。

# ----------------------------------------------
# by: lyshark
# email: me@lyshark.com
# project: https://github.com/lyshark/lyscript
# ----------------------------------------------

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

# 获取指定位置前index条指令的长度
def getopcodesize(dbg,address,index):
    ref_size = 0

    dasm = dbg.get_disasm_code(address,index)
    for index in dasm:
        count = dbg.assemble_code_size(index.get("opcode"))
        ref_size += count
    return ref_size

def patch_zwqueryinformationprocess(dbg):
    # 得到模块句柄
    ispresent = dbg.get_module_from_function("ntdll.dll","zwqueryinformationprocess")
    print(hex(ispresent))

    create_address = dbg.create_alloc(1024)
    print("分配空间: {}".format(hex(create_address)))


    # 将反调试语句转为机器码
    shellcode = getopcode(dbg,
                          [
                              "cmp dword [esp + 8],7",
                              "db 0x74",
                              "db 0x06",
                              f"push {hex(ispresent)}",
                              "ret",
                              "mov eax,dword [esp +0x0c]",
                              "push 0",
                              "pop dword [eax]",
                              "xor eax,eax",
                              "ret 14"
                          ]
                          )

    print(shellcode)

    # 把shellcode写出到自己分配的堆中
    flag = 0
    for index in range(0,len(shellcode)):
        flag = dbg.write_memory_byte(create_address + index,shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0

    # 填充跳转位置
    jmp_shellcode = getopcode(dbg,
                              [
                                  f"push {hex(create_address)}",
                                  "ret"
                              ]
                              )
    for index in range(0,len(jmp_shellcode)):
        flag = dbg.write_memory_byte(ispresent + index,jmp_shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0

    return flag

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    ref = patch_zwqueryinformationprocess(dbg)

    print("补丁状态: {}".format(ref))

    dbg.close()

这段代码运行后,首先会申请内存,然后将特定的一段机器码写出到此内存中。

内存写出以后,再将函数头部替换为跳转,这样一来当函数被调用,也就自动转向了。

patch_findwindow

findwindow函数功能是取窗体句柄,有aw与ex系列,使用同上方法替代即可。

# ----------------------------------------------
# by: lyshark
# email: me@lyshark.com
# project: https://github.com/lyshark/lyscript
# ----------------------------------------------

from lyscript32 import mydebug
import ctypes

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

def patch_findwindow(dbg):
    # 得到模块句柄
    findwindowa = dbg.get_module_from_function("user32.dll","findwindowa")
    findwindoww = dbg.get_module_from_function("user32.dll","findwindoww")
    findwindowexa = dbg.get_module_from_function("user32.dll","findwindowexa")
    findwindowexw = dbg.get_module_from_function("user32.dll","findwindowexw")
    print("a = {} w = {} exa = {} exw = {}".format(hex(findwindowa),hex(findwindoww),hex(findwindowexa),hex(findwindowexw)))

    # 将反调试语句转为机器码
    shellcode = getopcode(dbg,
                          [
                              "xor eax,eax",
                              "ret 0x8",
                          ]
                          )

    shellcodeex = getopcode(dbg,
                            [
                                "xor eax,eax",
                                "ret 0x10",
                            ]
                            )
    # 写出
    flag = 0
    for index in range(0,len(shellcode)):
        flag = dbg.write_memory_byte(findwindowa + index,shellcode[index])
        flag = dbg.write_memory_byte(findwindoww + index,shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0

    for index in range(0,len(shellcodeex)):
        flag = dbg.write_memory_byte(findwindowexa + index,shellcodeex[index])
        flag = dbg.write_memory_byte(findwindowexw + index,shellcodeex[index])
        if flag:
            flag = 1
        else:
            flag = 0

    return flag

if __name__ == "__main__":
    dbg = mydebug()

    connect = dbg.connect()

    ref = patch_findwindow(dbg)
    print("补丁状态: {}".format(ref))

    dbg.close()

补丁应用会分别替换四个函数。

patch_enumwindows

枚举窗体的补丁与上方代码一致,此处就不再分析了。

如下案例,实现了在枚举窗体过程中实现弹窗,并不影响窗体的枚举。

from lyscript32 import mydebug

# 传入汇编代码,得到对应机器码
def get_opcode_from_assemble(dbg_ptr,asm):
    byte_code = bytearray()

    addr = dbg_ptr.create_alloc(1024)
    if addr != 0:
        asm_size = dbg_ptr.assemble_code_size(asm)
        # print("汇编代码占用字节: {}".format(asm_size))

        write = dbg_ptr.assemble_write_memory(addr,asm)
        if write == true:
            for index in range(0,asm_size):
                read = dbg_ptr.read_memory_byte(addr + index)
                # print("{:02x} ".format(read),end="")
                byte_code.append(read)
        dbg_ptr.delete_alloc(addr)
        return byte_code
    else:
        return bytearray(0)

# 传入汇编机器码得到机器码列表
def getopcode(dbg, code):
    shellcode = []

    for index in code:
        ref = get_opcode_from_assemble(dbg,index)
        for opcode in ref:
            shellcode.append(opcode)

    return shellcode

# 获取指定位置前index条指令的长度
def getopcodesize(dbg,address,index):
    ref_size = 0

    dasm = dbg.get_disasm_code(address,index)
    for index in dasm:
        count = dbg.assemble_code_size(index.get("opcode"))
        ref_size += count
    return ref_size

def patch_enumwindows(dbg):
    # 得到模块句柄
    address = dbg.get_module_from_function("user32.dll","enumwindows")
    print(hex(address))

    msg_box = dbg.get_module_from_function("user32.dll","messageboxa")
    print(hex(msg_box))

    create_address = dbg.create_alloc(1024)
    print("分配空间: {}".format(hex(create_address)))

    # 找call地址,找到后取出他的内存地址
    dasm_list = dbg.get_disasm_code(address,20)
    call_addr = 0
    call_next_addr = 0
    for index in range(0,len(dasm_list)):

        # 如果找到了call,取出call地址以及下一条地址
        if dasm_list[index].get("opcode").split(" ")[0] == "call":
            call_addr = dasm_list[index].get("addr")
            call_next_addr = dasm_list[index+1].get("addr")
            print("call = {} call_next = {}".format(hex(call_addr),hex(call_next_addr)))

    # 将反调试语句转为机器码
    shellcode = getopcode(dbg,
                          [
                              "push 0",
                              "push 0",
                              "push 0",
                              "push 0",
                              f"call {hex(msg_box)}",
                              "mov eax,1",
                              "pop ebp",
                              "ret 10",

                              f"call {hex(call_addr)}",
                              "pop ebp",
                              "ret 8"
                          ]
                          )

    print(shellcode)

    # 把shellcode写出到自己分配的堆中
    flag = 0
    for index in range(0,len(shellcode)):
        flag = dbg.write_memory_byte(create_address + index,shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0


    # 填充跳转位置
    jmp_shellcode = getopcode(dbg,
                              [
                                  f"push {hex(create_address)}",
                                  "ret"
                              ]
                              )
    for index in range(0,len(jmp_shellcode)):
        flag = dbg.write_memory_byte(call_addr + index,jmp_shellcode[index])
        if flag:
            flag = 1
        else:
            flag = 0

    return flag

if __name__ == "__main__":
    dbg = mydebug()
    connect = dbg.connect()

    ref = patch_enumwindows(dbg)

    dbg.close()

输出效果如下:

以上就是lyscript实现hook隐藏调试器的方法详解的详细内容,更多关于lyscript hook隐藏调试器的资料请关注其它相关文章!

《LyScript实现Hook隐藏调试器的方法详解.doc》

下载本文的Word格式文档,以方便收藏与打印。