现在的位置: 首页 > 综合 > 正文

屌丝来谈Thunk Trick

2013年11月26日 ⁄ 综合 ⁄ 共 2226字 ⁄ 字号 评论关闭

最近又冬眠了,时间很充裕。作为屌丝的我常常在互联网上找寻信息,自己也获得过很多人的帮助,无以回馈,所以想想就写些博客来。

Thunk是内联汇编中的一种技巧把戏,它代表着一串机器指令。从编程的角度来看,数据放在数据段,执行的指令放在代码段,用户无法修改某块内存的内容,把它变成机器码来用作指令执行。更何况操作系统对内存页面还有许多访问控制,一不留神把数据段中的内容当成指令操作和执行,操作系统就会毫不留情地抛一个0xC0000005
Access Violation
的异常给你。即便是如此,但是Windows还是很自信地将本来属于系统一级的API导出给用户空间的程序调用,来操作内核的页面属性。这样对程序员来说就有更多灵活的空间可以发挥。VirtualAlloc VirtualProtect FlushInstructionCache VirtualFree等就是这一类函数。


废话不多说,先来定义一个代表机器指令的结构体:

#pragma pack(1)
struct _NiceThunk{
BYTE m_jump;//OxE9 jmp段内的直接跳转
DWORD m_offset;//标号偏移的相对值,直接加到EIP上去的
};

初始化:

void __stdcall InitThunk(_NiceThunk* pThunkImpl){
	pThunkImpl->m_jump = 0xE9;
	pThunkImpl->m_offset = (INT_PTR)ThunkFunc-(INT_PTR)(pThunkImpl+1);
}

这里有几点需要说明,首先是需要将数据格式对齐,指令的格式不对不能跳转,可以定义好了之后简单的测试下是否对齐assert(sizeof(_NiceThunk)==5)。另外,读MSDN可能发现pragma pack 和__declspec (align(#))似乎意思相似,但两者目的和作用是不一样的,不要搞混淆了。

然后从虚拟内存页上调拨一个页面分配_NiceThunk结构体,目的只有一个:能修改该页,并使它可以执行。

SYSTEM_INFO siSysInfo;
GetSystemInfo(&siSysInfo);
_NiceThunk* pThunkImpl = (_NiceThunk*)VirtualAlloc(NULL,siSysInfo.dwPageSize,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
InitThunk(pThunkImpl);
FlushInstructionCache(GetCurrentProcess(),pThunkImpl,sizeof(_NiceThunk));

最后只需要一个能让Thunk执行跳转的函数,该函数采用内联汇编编写,有关于MASM请参考MSDN文档:http://msdn.microsoft.com/zh-cn/library/4ks26t93(v=vs.90).aspx

void __stdcall ThunkFunc(int /*iArg*/){
	int iA = 0;
	iA++;
}
__declspec (naked) void __stdcall CallThunk(_NiceThunk* /*pThunkImpl*/){
	__asm{
		mov eax,[esp+4] _NiceThunk的地址
		mov ecx,0 
		push ecx  ThunkFunc参数入栈
		call eax 执行Thunk代码
		ret 4 参数出栈
	}
}

补充一句,Thunk可以作为函数指针使用,不信可以试试,这才是最大的Trick大笑

最近也在看John Tang在codeproject上发表的文章:http://www.codeproject.com/Articles/27908/Thunk-and-its-uses可以提供更多的参考。

大半年之前记录学习的这段内容,由于例子引用的不充分太过浅显,虽然已经说明了__stdcall以及参数传递等部分内容,但还不够清晰的说明thunk的本质,天资过人的朋友那就另当别论啦。下面从另外一种角度说明thunk,然后可能会引出一些有关编译器和符号的东西。

typedef void  (__stdcall *CallThunkFunc)(int);
CallThunkFunc pfnCall = (CallThunkFunc)&(*pThunkImpl);
pfnCall(1);

咋看之下好像没什么奇怪。广大的屌丝朋友们,只要展开汇编新的风景将呈现在我们眼前:

//pfnCall(1) 调用thunk跳转到ThunkFunc
00100000  jmp  ThunkFunc (0EA11B3h) 00100000是分配thunk的虚存地址
//直接调用ThunkFunc(1)函数
call ThunkFunc(000711B3)
ThunkFunc:
000711B3  jmp   ThunkFunc (71460h)  

两种情况是不一样的。我和很多屌丝朋友们一起,第一次看到编译器对函数调用的反汇编,居然是这个样子时吃了一斤(吃得太晚了)。原来源程序中的函数定义的名称,会被编译器拿来作为一个标号(符号),函数的实现只是一个代码段,函数的调用就是一个跳转表的实现。综合pfnCall(1)不难形象的理解到:thunk的实现恰恰就是动态的构造了这个跳转表。话说回来,如果编译器对函数调用都用跳转表实现话,尼玛要是有人改了这个跳转表会怎样呢,请教各位屌丝朋友们……




抱歉!评论已关闭.