x96简介
x96故名思意,x32 + x64,一指在32位代码中插入64位代码
在64位系统中,内核是64位的,而64位机兼容32位程序,很明显,操作系统会完成32位指令向64位的切换,以便进入内核向程序提供功能支持
jmp far 33:xxxx
- x64
使用x64dbg附加到64位程序上,转到
Nt***
系统函数,可以看到64位程序使用syscall
进入内核,注意此时的CS
段为33
x32
同样使用x32dbg附加到32位程序上,可以看到其执行流程如下
主要有两次关键跳转:
jmp far 23:xxxx
jmp far 33:xxxx
而转到33:xxxx
之后可以看到,反汇编的结果明显是不对的
再开一个x64dbg,把二进制序列41 FF A7 F8 00 00 00
贴进去,可以看到反汇编结果为jmp qword ptr ds:[r15+0xF8]
显然,在jmp far 33:xxxx
之后,跳转到wow64cpu.dll
中,将32位指令切换为64位指令,转换参数为64位,然后再次调用Nt***
系列函数,最后在ntdll.dll
中完成进入内核
如果在32位调试器中单步,则无法跟踪到目标地址上,而64位调试器又无法附加32位程序,由此就可以通过jmp far 33:xxxx
在32位程序中添加64位的代码,将重要的功能藏于其中,可躲避调试器
但是这种方法对于内核调试器来说是没用的
例子
#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;
}