在x86下写x64代码以扰乱调试器

@lzeroyuee  October 2, 2020

x96简介

x96故名思意,x32 + x64,一指在32位代码中插入64位代码

在64位系统中,内核是64位的,而64位机兼容32位程序,很明显,操作系统会完成32位指令向64位的切换,以便进入内核向程序提供功能支持

jmp far 33:xxxx

  • x64

    使用x64dbg附加到64位程序上,转到Nt***系统函数,可以看到64位程序使用syscall进入内核,注意此时的CS段为33

    1.png

  • x32

    同样使用x32dbg附加到32位程序上,可以看到其执行流程如下

    2.png

    主要有两次关键跳转:

    1. jmp far 23:xxxx
    2. jmp far 33:xxxx

而转到33:xxxx之后可以看到,反汇编的结果明显是不对的

3.png

再开一个x64dbg,把二进制序列41 FF A7 F8 00 00 00贴进去,可以看到反汇编结果为jmp qword ptr ds:[r15+0xF8]

4.png

显然,在jmp far 33:xxxx之后,跳转到wow64cpu.dll中,将32位指令切换为64位指令,转换参数为64位,然后再次调用Nt***系列函数,最后在ntdll.dll中完成进入内核

6.png

如果在32位调试器中单步,则无法跟踪到目标地址上,而64位调试器又无法附加32位程序,由此就可以通过jmp far 33:xxxx在32位程序中添加64位的代码,将重要的功能藏于其中,可躲避调试器

但是这种方法对于内核调试器来说是没用的

5.png

例子

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


unsigned char key[8] = { 0 };
unsigned char jmp_far33[7] = { 0xea, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00 };  // jmp far 33:xxxx,中间00占据的是目标地址,0x0033是64位cs段选择子
unsigned char jmp_back[10] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00 };    // 前面00占据的是目标地址,0x0023是32位cs段选择子

void __declspec(naked) store()
{
    _asm {
        __emit 0x90

        // mov r12, 0x8877665544332211
        // 具体用什么寄存器要看在wow64cpu.dll的流程中什么寄存器没用
        // 32位程序本身自己是不会使用r8 - r15
        __emit 0x49
        __emit 0xBC
        __emit 0x11
        __emit 0x22
        __emit 0x33
        __emit 0x44
        __emit 0x55
        __emit 0x66
        __emit 0x77
        __emit 0x88

        mov eax, offset jmp_back

        // 跳转回去 jmp far tword ptr ds:[rax]
        __emit 0x48
        __emit 0xff
        __emit 0x28
    }
}



void __declspec(naked) retract()
{
    _asm {
        __emit 0x90

        pop edi

        // mov dword ptr [edi], r12d
        __emit 0x67
        __emit 0x44
        __emit 0x89
        __emit 0x27

        // mov rax, r12
        __emit 0x49
        __emit 0x8B
        __emit 0xC4
        // shr rax, 0x20
        __emit 0x48
        __emit 0xC1
        __emit 0xE8
        __emit 0x20
        // mov dword ptr [edi + 0x4], eax
        __emit 0x36
        __emit 0x67
        __emit 0x89
        __emit 0x47
        __emit 0x04

        pop edi // 还原edi


        mov eax, offset jmp_back

        // 跳转回去 jmp far tword ptr ds:[rax]
        __emit 0x48
        __emit 0xff
        __emit 0x28
    }
}

void show_key()
{
    printf("Key = ");
    for (int i = 0; i < sizeof(key); i++) {
        printf("%#x ", key[i]);
    }
    printf("\n");
}

int main(void)
{
    printf("Debug output: -----------\n");
    printf("key address: 0x%p\n", key);
    printf("store func: 0x%p\n", store);
    printf("retract func: 0x%p\n", retract);
    printf("-------------------------\n\n");

    void *p;
    __asm {
        mov p, offset L1    // 获取L2地址
    }
    *(unsigned int *)(jmp_far33 + 1) = (unsigned int)store;
    *(unsigned int *)jmp_back = (unsigned int)p;

    void *buf1 = VirtualAlloc(NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(buf1, jmp_far33, sizeof(jmp_far33));

    __asm {
        jmp buf1    // 跳转到store函数
    }
L1:
    __asm {
        mov p, offset L2    // 获取L2地址
    }
    *(unsigned int *)(jmp_far33 + 1) = (unsigned int)retract;
    memcpy(buf1, jmp_far33, sizeof(jmp_far33));
    *(unsigned int *)jmp_back = (unsigned int)p;
    __asm {
        push edi
        push offset key
        jmp buf1    // 跳转到retract函数
    }

L2:
    show_key();
    VirtualFree(buf1, 0x1000, MEM_RELEASE);
    return 0;
}

添加新评论