@zoand
2017-04-24T09:45:46.000000Z
字数 6808
阅读 2375
amd64驱动
英文原文:http://blog.rewolf.pl/blog/?p=102
前几个月,我做了一些关于32位进程在WOW64里执行原生x64代码的探索。第二个想法就是在64位进程里运行原生x86代码。以上两个想法都是可能的,且我搜索到一些人已经使用它了:
http://vx.netlux.org/lib/vrg02.html
http://www.corsix.org/content/dll-injection-and-wow64
http://int0h.wordpress.com/2009/12/24/the-power-of-wow64/
http://int0h.wordpress.com/2011/02/22/anti-anti-debugging-via-wow64/
很不幸的是,在我刚开始研究时我没有注意到以上的结果,所以我仅仅提出我自己的独立想法。
最简单的办法来查看x86和x64间的转换莫过于去观察64位windows系统中32位版本的ntdll.dll中的任何系统调用:
32-bits ntdll from Win7 x86
mov eax, X
mov edx, 7FFE0300h
call dword ptr [edx]
;ntdll.KiFastSystemCall
retn Z
32-bits ntdll from Win7 x64
mov eax, X
mov ecx, Y
lea edx, [esp+4]
call dword ptr fs:[0C0h]
;wow64cpu!X86SwitchTo64BitMode
add esp, 4
ret Z
正如你可以看到,在64位系统上有一个到fs:[0xC0](wow64cpu!X86SwitchTo64BitMode)
的调用,替代了到ntdll.KiFastSystemCall
的标准调用。wow64cpu!X86SwitchTo64BitMode
实施了一个远跳到64位段:
wow64cpu!X86SwitchTo64BitMode:
748c2320 jmp 0033:748C271E ;wow64cpu!CpupReturnFromSimulatedCode
这就是在64位windows系统里关于x64和x86模式切换的所有内幕戏法了。而且它也能工作在非WOW64进程里(标准的原生64位应用程序),所以32位代码也能在64位应用程序里执行。综上所述,在64位系统里运行的所有进程(x86 & x64)都分配了两个代码段:
cs = 0x23 -> x86 mode
cs = 0x33 -> x64 mode
首先我已经准备了一些宏,它们将会在64位代码的头尾作为标记使用:
#define EM(a) __asm __emit (a)
#define X64_Start_with_CS(_cs) \
{ \
EM(0x6A) EM(_cs) /* push _cs */ \
EM(0xE8) EM(0) EM(0) EM(0) EM(0) /* call $+5 */ \
EM(0x83) EM(4) EM(0x24) EM(5) /* add dword [esp], 5 */ \
EM(0xCB) /* retf */ \
}
#define X64_End_with_CS(_cs) \
{ \
EM(0xE8) EM(0) EM(0) EM(0) EM(0) /* call $+5 */ \
EM(0xC7) EM(0x44) EM(0x24) EM(4) /* */ \
EM(_cs) EM(0) EM(0) EM(0) /* mov dword [rsp + 4], _cs */ \
EM(0x83) EM(4) EM(0x24) EM(0xD) /* add dword [rsp], 0xD */ \
EM(0xCB) /* retf */ \
}
#define X64_Start() X64_Start_with_CS(0x33)
#define X64_End() X64_End_with_CS(0x23)
执行完X64_Start()
宏后CPU会马上切换到x64模式,执行完X64_End()
宏之后又会马上回到x86模式。以上两个宏的地位是独立的,这要归功于远程返回的操作码(译者注:这句不知如何翻译好,原文是Above macros are position independent thanks to far return opcode.)。
在调用x64版本的API时,以上两个宏也是有用的。我曾经尝试加载x64版本的kernel32.dll但这不是琐碎的任务且我没干成功,所以我坚持只调用原生API。现在主要的问题是一些附加的检查导致无法加载64位版本的kernel32.dll
,因为(进程)已经完全加载了x86版本的kernel32.dll
库。我相信还是可能实现这个目标的,通过一些下流的挂钩手段突破kernel32!BaseDllInitialize
,但这是一个非常复杂的的任务。当我开始做这个研究时,我是在WindowsVista
下测试的,我成功加载了64位的kernel32
和user32
库(使用了一些黑客手段),但是它们并没有完全工作。而且当我换成Windows7(测试)时(发现)那个能在Vista上使用的方法已经完全不能工作了。
言归正传,调用原生API需要查找x64版本的ntdll.dll
在内存中的位置。为了完成这一任务我分析了来自_PEB_LDR_DATA
中InLoadOrderModuleList
的结构。64位_PEB
能从64位_TEB
中获得。而获得64位_TEB
的过程和在x86平台上的过程很相似(在x64上我需要使用gs
来替代fs
):
mov eax, gs:[0x30]
它还可以更简单,因为wow64cpu!CpuSimulate
(此函数负责CPU切换到x86模式)移动gs:[0x30]
的值到r12
寄存器,所以我的getTEB64
代码如下:
//to fool M$ inline asm compiler I'm using 2 DWORDs instead of DWORD64
//use of DWORD64 will generate wrong 'pop word ptr[]' and it will break stack
union reg64
{
DWORD dw[2];
DWORD64 v;
};
//macro that simplifies pushing x64 registers
#define X64_Push(r) EM(0x48 | ((r) >> 3)) EM(0x50 | ((r) & 7))
WOW64::TEB64* getTEB64()
{
reg64reg;
reg.v = 0;
X64_Start();
//R12 register should always contain pointer to TEB64 in WoW64 processes
X64_Push(_R12);
//below pop will pop QWORD from stack, as we're in x64 mode now
__asm pop reg.dw[0]
X64_End();
//upper 32 bits should be always 0 in WoW64 processes
if(reg.dw[1] != 0)
return0;
return(WOW64::TEB64*)reg.dw[0];
}
WOW64
的命名空间已经定义在了os_structs.h
文件里,这个文件和源代码一起都在本文的附件里。
以下函数负责获得64位ntdll.dll
的位置:
DWORD getNTDLL64()
{
static DWORD ntdll64 = 0;
if(ntdll64 != 0)
returnntdll64;
WOW64::TEB64* teb64 = getTEB64();
WOW64::PEB64* peb64 = teb64->ProcessEnvironmentBlock;
WOW64::PEB_LDR_DATA64* ldr = peb64->Ldr;
printf("TEB: %08X\n", (DWORD)teb64);
printf("PEB: %08X\n", (DWORD)peb64);
printf("LDR: %08X\n", (DWORD)ldr);
printf("Loaded modules:\n");
WOW64::LDR_DATA_TABLE_ENTRY64* head = \
(WOW64::LDR_DATA_TABLE_ENTRY64*)ldr->InLoadOrderModuleList.Flink;
do
{
printf(" %ws\n", head->BaseDllName.Buffer);
if(memcmp(head->BaseDllName.Buffer, L"ntdll.dll",
head->BaseDllName.Length) == 0)
{
ntdll64 = (DWORD)head->DllBase;
}
head = (WOW64::LDR_DATA_TABLE_ENTRY64*)head->InLoadOrderLinks.Flink;
}
while(head != (WOW64::LDR_DATA_TABLE_ENTRY64*)&ldr->InLoadOrderModuleList);
printf("NTDLL x64: %08X\n", ntdll64);
Return ntdll64;
}
为了完全支持x64原生API调用,我将需要一些等价于GetProcAddress
,可以被ntdll!LdrGetProcedureAddress
简单取代的函数。以下代码负责获取LdrGetProcedureAddress
的地址:
DWORD getLdrGetProcedureAddress()
{
BYTE* modBase = (BYTE*)getNTDLL64();
IMAGE_NT_HEADERS64* inh = \
(IMAGE_NT_HEADERS64*)(modBase + ((IMAGE_DOS_HEADER*)modBase)->e_lfanew);
IMAGE_DATA_DIRECTORY& idd = \
inh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if(idd.VirtualAddress == 0)
return0;
IMAGE_EXPORT_DIRECTORY* ied = \
(IMAGE_EXPORT_DIRECTORY*)(modBase + idd.VirtualAddress);
DWORD* rvaTable = (DWORD*)(modBase + ied->AddressOfFunctions);
WORD* ordTable = (WORD*)(modBase + ied->AddressOfNameOrdinals);
DWORD* nameTable = (DWORD*)(modBase + ied->AddressOfNames);
//lazy search, there is no need to use binsearch for just one function
for(DWORDi = 0; i < ied->NumberOfFunctions; i++)
{
if(strcmp((char*)modBase + nameTable, "LdrGetProcedureAddress"))
continue;
else
return(DWORD)(modBase + rvaTable[ordTable]);
}
Return 0;
}
接下来我介绍一个函数,能用x86的C/C++代码来调用x64原生API:
DWORD64 X64Call(DWORDfunc, intargC, ...)
{
va_listargs;
va_start(args, argC);
DWORD64_rcx = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
DWORD64_rdx = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
DWORD64_r8 = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
DWORD64_r9 = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
reg64 _rax;
_rax.v = 0;
DWORD64restArgs = (DWORD64)&va_arg(args, DWORD64);
//conversion to QWORD for easier use in inline assembly
DWORD64_argC = argC;
DWORD64_func = func;
DWORDback_esp = 0;
__asm
{
;//keep original esp in back_esp variable
mov back_esp, esp
;//align esp to 8, without aligned stack some syscalls
;//may return errors !
and esp, 0xFFFFFFF8
X64_Start();
;//fill first four arguments
push _rcx
X64_Pop(_RCX);
push _rdx
X64_Pop(_RDX);
push _r8
X64_Pop(_R8);
push _r9
X64_Pop(_R9);
push edi
push restArgs
X64_Pop(_RDI);
push _argC
X64_Pop(_RAX);
;//put rest of arguments on the stack
test eax, eax
jz _ls_e
lea edi, dword ptr [edi+ 8*eax- 8]
_ls:
test eax, eax
jz _ls_e
push dword ptr [edi]
sub edi, 8
sub eax, 1
jmp _ls
_ls_e:
;//create stack space for spilling registers
sub esp, 0x20
call _func
;//cleanup stack
push _argC
X64_Pop(_RCX);
lea esp, dword ptr [esp+ 8*ecx+ 0x20]
pop edi
;//set return value
X64_Push(_RAX);
pop _rax.dw[0]
X64_End();
mov esp, back_esp
}
Return _rax.v;
}
这函数有点长,但注释和整个想法非常简单。第一个参数是我想调用的x64函数的地址,第二个参数是函数参数的个数。剩下的参数依据要调用的函数而定(译者注:剩下的参数就是被调用函数的各个参数),但所有参数的类型必须都是DWORD64
类型的。以下是一个关于X64Call()
的简单用法:
DWORD64 GetProcAddress64(DWORDmodule, char* funcName)
{
static DWORD _LdrGetProcedureAddress = 0;
if(_LdrGetProcedureAddress == 0)
{
_LdrGetProcedureAddress = getLdrGetProcedureAddress();
printf("LdrGetProcedureAddress: %08X\n", _LdrGetProcedureAddress);
if(_LdrGetProcedureAddress == 0)
return0;
}
WOW64::ANSI_STRING64 fName = { 0 };
fName.Buffer = funcName;
fName.Length = strlen(funcName);
fName.MaximumLength = fName.Length + 1;
DWORD64funcRet = 0;
X64Call(_LdrGetProcedureAddress, 4,
(DWORD64)module, (DWORD64)&fName,
(DWORD64)0, (DWORD64)&funcRet);
printf("%s: %08X\n", funcName, (DWORD)funcRet);
Return funcRet;
}
在64位进程里执行x86代码:
这和刚才的例子非常像,不过有点小小的不便,就是64版本的微软C/C++编译器不支持内嵌汇编了。所有的技巧(tricks?)都在一个单独的.asm
文件里实现。以下是MASM64
中X86_Start
和X86_End
的宏定义:
X86_Start MACRO
LOCAL xx, rt
call $+5
xx equ$
mov dword ptr [rsp+ 4], 23h
add dword ptr [rsp], rt - xx
retf
rt:
ENDM
X86_End MACRO
db6Ah, 33h ; push 33h
db0E8h, 0, 0, 0, 0 ; call $+5
db83h, 4, 24h, 5 ; add dword ptr [esp], 5
db0CBh ; retf
ENDM