Skip to content

Exploration of Hook Hijacking Technology

Hook hijacking technology allows developers to intercept calls to system or application functions and execute custom code before or after function calls. Hook hijacking technology is commonly used for viruses and malicious software, and can also allow developers to extend or modify the functionality of system functions, thereby improving software performance and functionality expansion.

Write functions using assembly instructions

Firstly, it is necessary to automatically convert a segment of assembly code passed in by the user into equivalent machine code, and write this segment of machine code into a memory space. Finally, the first address of that memory space needs to be reversed.

To achieve this requirement, three steps are required: first, allocate a segment of memory space in memory using the create_alloc function, then obtain the number of bytes occupied by the assembly instruction by calling the assemble_code_size function, and write this bytecode into the corresponding heap by calling the assemble_write_memory function. Finally, the memory address of the heap is reversed.

python
from x32dbg import Debugger

# Encode a piece of assembly code and write it out into temporary heap space
def write_opcode_from_assemble(dbg_ptr,asm_list=[]):
    addr_count = 0
    addr = dbg_ptr.create_alloc(0,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

if __name__ == "__main__":
    dbg = Debugger(address="127.0.0.1",port=6589)
    if False == dbg.connect():
        exit()

    # Obtain the memory address of Message Box A in the user32.DLL library
    msg_ptr = dbg.get_module_proc_addr("user32.dll","MessageBoxA")
    call = "call {}".format(str(hex(msg_ptr)))
    print("Generate call assembly statements = {}".format(call))

    # List of instruction sets to be written
    asm_list = [
        'push 0',
        'push 0',
        'push 0',
        'push 0',
        call
    ]

    # Batch assemble and write to the corresponding memory
    write_addr = write_opcode_from_assemble(dbg,asm_list)
    print("Write out memory address = {}".format(hex(write_addr)))

    # Set the memory execution attribute to read write execution
    dbg.set_memory_protect(write_addr,32,1024)

    # Set EIP to instruction set position
    dbg.set_eip(write_addr)
    dbg.close()

As shown in the above code, in order to write a pop-up window, the first step is to obtain the loading address of the MessageBoxA function located in the user32.dll module through the get_module_proc_addr function. This function takes five parameters, with the first four being saved through push 0, and the last parameter being a call to the call statement. The constructed string is passed directly to the write_opcode_from_assemble function to write the data to the process heap, and the executable properties are set through set_memory_protect.

Implement Hook to replace MessageBox

In the previous case, we demonstrated how to write a piece of assembly code into peer memory. Next, we will take replacing the MessageBox pop-up as an example to demonstrate how to replace the functionality of specific functions in a process, and use this to achieve the effect of custom pop ups.

Firstly, encapsulate the implementation of the assembly function, which receives an assembly list and converts it into machine code, and sequentially writes it out to the memory of the specified process. By calling get_module_proc_addr, the memory address of the original pop-up window is obtained, and the first address of MessageBoxA is written out and replaced with the assembly jump contained in asm, as shown in the following example;

python
from x32dbg import Debugger

# Encode a piece of assembly code and write it out to the specified memory
def assemble(dbg, address=0, asm_list=[]):
    asm_len_count = 0
    for index in range(0,len(asm_list)):
        # Write to memory
        dbg.assemble_at(address, asm_list[index])
        # Obtain assembly instruction length
        asm_len_count = dbg.assemble_code_size(asm_list[index])
        # Address increment each time
        address = address + asm_len_count

if __name__ == "__main__":
    dbg = Debugger(address="127.0.0.1",port=6589)
    if False == dbg.connect():
        exit()

    # Find MessageBoxA
    messagebox_address = dbg.get_module_proc_addr("user32.dll","MessageBoxA")
    print("MessageBoxA memory address = {}".format(hex(messagebox_address)))

    # Allocate space
    HookMem = dbg.create_alloc(0,1024)
    print("Custom memory space = {}".format(hex(HookMem)))

    # Write the MessageBoxA redirect address
    asm = [
        f"push {hex(HookMem)}",
        "ret"
    ]

    # Write assembly instructions from the list into memory
    assemble(dbg,messagebox_address,asm)
    dbg.close()

Due to the need for two temporary variables to represent the title and content in the MessageBox pop-up window, we allocate two heap spaces in the peer memory using the create_alloc function, and sequentially write out the pop-up window string into memory using set_memory_byte. At this point, the pop-up window content is also filled in, where txt represents the title and box represents the content;

python
# Define two variables to store strings
MsgBoxAddr = dbg.create_alloc(0,512)
MsgTextAddr = dbg.create_alloc(0,512)

# Fill the pop-up window with the title lyshark
txt = [0x6c, 0x79, 0x73, 0x68, 0x61, 0x72, 0x6b]
# Fill in the content as lyshark.com
box = [0x6C, 0x79, 0x73, 0x68, 0x61, 0x72, 0x6B, 0x2E, 0x63, 0x6F, 0x6D]

for txt_count in range(0,len(txt)):
    dbg.set_memory_byte(MsgBoxAddr + txt_count, txt[txt_count])

for box_count in range(0,len(box)):
    dbg.set_memory_byte(MsgTextAddr + box_count, box[box_count])

print("Title Address = {} content = {}".format(hex(MsgBoxAddr),hex(MsgTextAddr)))

Next, it is necessary to implement the main body of the pop-up window. For a complete pop-up window, only the core code of the pop-up window needs to be extracted, without the need to extract all instruction sets.

However, it should be noted that the call 0x75B20E20 address needs to be replaced, and the address here may change depending on the system. Special attention should be paid when extracting it;

python
# This is the segment after replacing the Message Box
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"
]

# Write to custom memory
assemble(dbg, HookMem, PatchCode)

# Set the memory execution attribute to read write execution
dbg.set_memory_protect(HookMem,32,1024)

The above is the code explanation for replacing pop ups. Integrating the code together can achieve the replacement of pop ups;

python
from x32dbg import Debugger

# Encode a piece of assembly code and write it out to the specified memory
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 = Debugger(address="127.0.0.1",port=6589)
    if False == dbg.connect():
        exit()

    # Find MessageBoxA
    messagebox_address = dbg.get_module_proc_addr("user32.dll","MessageBoxA")
    print("MessageBoxA memory address = {}".format(hex(messagebox_address)))

    # Allocate space
    HookMem = dbg.create_alloc(0,1024)
    print("自定义内存空间 = {}".format(hex(HookMem)))

    # Write the MessageBoxA redirect address
    asm = [
        f"push {hex(HookMem)}",
        "ret"
    ]

    # Write assembly instructions from the list into memory
    assemble(dbg,messagebox_address,asm)

    # Define two variables to store strings
    MsgBoxAddr = dbg.create_alloc(0,512)
    MsgTextAddr = dbg.create_alloc(0,512)

    # Fill in string content
    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.set_memory_byte(MsgBoxAddr + txt_count, txt[txt_count])

    for box_count in range(0,len(box)):
        dbg.set_memory_byte(MsgTextAddr + box_count, box[box_count])

    print("Title Address = {} content = {}".format(hex(MsgBoxAddr),hex(MsgTextAddr)))

    # This is the segment after replacing the MessageBox
    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"
    ]

    # Write to custom memory
    assemble(dbg, HookMem, PatchCode)

    # Set the memory execution attribute to read write execution
    dbg.set_memory_protect(HookMem,32,1024)

    print("Pop up has been replaced")
    dbg.debug_run()
    dbg.close()

When the above code is run, it will replace the MessageBoxA function in the process with a custom function. Therefore, when the user clicks the pop-up button again, the original content will not be prompted, but a custom pop-up will be prompted;