Thunk技术 QA

Thunk技术QA

 

1

Q:那么什么是Thunk技术呢?

A:这个问题其实问的不是很好,可以这么问,Thunk技术可以解决什么问题呢?Thunk技术就是解决在执行期改变CPU所执行的机器指令这个问题。

 

2

Q:怎么可能,一段程序不就是机器指令集嘛,怎么可能在运行时候改变机器指令执行顺序?

A:这里的改变是相对传统意义上CPU所执行的机器指令顺序来说的。具体来说,一般情况下,程序在运行时,机器指令是存在内存中的code segment里的,然后读到CPU中(缓存中),由EIP寄存器存储当前(将要)执行的机器指令,交由CPU执行。但是Thunk允许你将机器指令存储在data segment里,经过一些小技巧,这些机器指令最后能被CPU执行。

3

Q:看上去很Hacking的样子,应该很难吧?

A:Thunk技术本身并不复杂,但涉及到的知识点比较多,如果想搞的比较清楚,最需要的是耐心和刨根问底的态度。不过如果你耐心看完这篇QA,应该会有所了解: )

 

4

Q:那到底需要哪些背景知识呢?

A:

1. C/C++语言的知识,包括

1.1类/结构体基本知识

1.2函数指针

1.3回调函数

2. 汇编的知识,包括

2.1基本汇编指令

2.2Calling conventions

2.3对一段简单代码反汇编的阅读能力

 

5

Q:好吧,不得不说很多概念只是听过,不是很扎实,有的学过,现在忘的差不多了。。。

A:那很好啊,知识串烧起来学不仅量有保证,而且由于知识点彼此间有联系,对知识的理解与记忆也会加强,正好趁此机会学习一下吧。

1. C/C++语言的知识,包括

1.1类/结构体基本知识

1.1.1 编译器在编译类的成员函数时会在其参数列表里增加一个以该类为类型的参数,并在所有调用的地方将this作为参数传入

1.1.2当一个类所处的继承体系中没有虚函数时,其对象的地址也是其第一个成员变量的地址

1.2函数指针

1.2.1 typedef void (pFunc*) (int)定义了一种新的函数指针类型,该类型可以指向一个返回值为void,接受一个int类型参数的函数的入口地址,如 void func(int)

1.3回调函数

1.3.1回调函数首先是一个函数,其特点是一般我们将回调函数入口地址作为参数传入某一个希望调用回调函数的函数(Caller) ,然后在这个函数里调用回调函数,回调函数和函数指针的关系很密切,它们的关系是Caller函数的参数列表中一定有个形参是符合回调函数函数原型的函数指针。

 

2 汇编的知识(以32位机器为例)

 

2.1基本汇编指令

2.1.1寄存器

ESP:栈顶指针寄存器,总是存着当前栈顶内存的地址

EBP:栈基指针寄存器,总是存着当前栈基内存的地址

ESP,EBP所指的栈一般来讲都是一个特定函数的所用栈的栈顶和栈基,也就是说在程序执行的过程中,但凡遇到函数调用,它们俩就会双双变化,指向新的函数的栈顶和栈基,当函数调用结束会,它们会再一次发生变化,重新指向调用者栈的栈顶和栈基。当然这里涉及一个问题:如何在子函数调用结束后,ESP,EBP可以重新指向之前调用子函数的主函数栈的栈顶和栈基。

EIP:指令寄存器,总是存着将要执行的机器指令

2.1.2汇编指令

Push X:先ESP = ESP – 4再把X推进ESP所指栈上内存

Pop X:先把ESP所指栈上内存所存数据取出来放入X中,再ESP = ESP + 4

Jmp X:将EIP寄存器的值修改为(EIP原先的值+X),作用是让CPU执行的下一条指令更改为存在于地址为(EIP原先的值+X)的内存中存储的指令,这里X是一个相对量

Call X:先把Call后紧接着的指令(CS:EIP)Push进栈,再执行Jmp X指令

 

2.2Calling Conventions

2.2.1stdcall和cdecl

一个plain c function一般遵循stdcall或者cdecl或者fastcall(这里就不讲了),windows visual家族的编译器与linux下的gcc对这两者规范都支持。两者的特点是

stdcall:参数从右至左传入/callee清理函数栈/函数名字由下划线作为开始以“@参数列表长度”作为结束,如_myFunc@8

cdecall:参数从右至左传入/caller清理函数栈/函数名字由下划线作为开始

2.2.2thiscall

C++成员函数采用的是thiscall规范,其其特点是:

它的第一个参数传this至EAX寄存器/剩下的处理方式和stdcall一样/函数命名比较复杂,因为C++支持重载

 

2.3对一段简单代码反汇编的阅读能力

#include "stdafx.h"

#include<iostream>

using namespace std;

void free(int a){

int b = 1;

cout<<b<<endl;

}

 

int _tmain(int argc, _TCHAR* argv[])

{

free(1);

return 0;

}

 

------------------以下是上面程序在vs2008下反汇编的结果,有删减-------------------------------

0EA1127h  jmp         free (0EC13C0h)

 

void free(int a){

00EA13C0  push        ebp

00EA13C1  mov         ebp,esp

00EA13C3  sub         esp,0CCh

00EA13C9  push        ebx

00EA13CA  push        esi

00EA13CB  push        edi

00EA13CC  lea         edi,[ebp-0CCh]

00EA13D2  mov         ecx,33h

00EA13D7  mov         eax,0CCCCCCCCh

00EA13DC  rep stos    dword ptr es:[edi]

int b = 1;

00EA13DE  mov         dword ptr [b],1

}

00EA13E5  pop         edi

00EA13E6  pop         esi

00EA13E7  pop         ebx

00EA13E8  mov         esp,ebp

00EA13EA  pop         ebp

00EA13EB  ret

int _tmain(int argc, _TCHAR* argv[])

{

00EA1450  push        ebp

00EA1451  mov         ebp,esp

00EA1453  sub         esp,0C0h

00EA1459  push        ebx

00EA145A  push        esi

00EA145B  push        edi

00EA145C  lea         edi,[ebp-0C0h]

00EA1462  mov         ecx,30h

00EA1467  mov         eax,0CCCCCCCCh

00EA146C  rep stos    dword ptr es:[edi]

free(1);

00EA146E  push        1

00EA1470  call        free (0EA1127h)

00EA1475  add         esp,4

return 0;

00EA1478  xor         eax,eax

}

00EA147A  pop         edi

00EA147B  pop         esi

00EA147C  pop         ebx

00EA147D  add         esp,0C0h

00EA1483  cmp         ebp,esp

00EA1485  call        @ILT+340(__RTC_CheckEsp) (0EA1159h)

00EA148A  mov         esp,ebp

00EA148C  pop         ebp

00EA148D  ret

------------------反汇编结束----------------------------------------------------------------

 

我们先看一下free函数调用前后(以 call指令为基准)的汇编指令

free(1);

 

00EA146E  push        1                  调用前我们把唯一一个参数压栈

 

00EA1470  call        free (0EA1127h)    调用函数,马上就要跳到其它地址了,不能顺序执行了 T T

 

00EA1475  add         esp,4              调用后清理栈,这里是调用者在清理,这是…cdecall方式

 

再来看下free函数调用时的汇编指令

void free(int a){

00EA13C0  push        ebp            把ebp寄存器所存内容压栈

00EA13C1  mov         ebp,esp     让ebp现在保存现在esp寄存器所存内容,即push ebp后的栈顶指针

00EA13C3  sub         esp,0CCh       让esp向上指向新的栈顶,0CCh是预留空间

00EA13C9  push        ebx

00EA13CA  push        esi

00EA13CB  push        edi

00EA13CC  lea         edi,[ebp-0CCh]

00EA13D2  mov         ecx,33h

00EA13D7  mov         eax,0CCCCCCCCh

00EA13DC  rep stos    dword ptr es:[edi]

int b = 1;

00EA13DE  mov         dword ptr [b],1 给b复制,这里[b] == [esp + 4],vc 6.0就是这样显示的

}

00EA13E5  pop         edi

00EA13E6  pop         esi

00EA13E7  pop         ebx

00EA13E8  mov         esp,ebp       让esp重新指向push ebp后的esp,因为ebp正保存着它呢

00EA13EA  pop         ebp           因为这时esp所指内存空间里存的就是ebp,所以ebp的值得以恢复

00EA13EB  ret                       让EIP指向当前esp寄存器中保存的地址,调转到free(1)之后那条指令,

最后看和Thunk关系最紧密的call指令

00EA1470  call        free (0EA1127h)  首先要跳到0EA1127h

 

0EA1127h  jmp         free (0EC13C0h)  0EA1127h还是一条jmp指令,跳到free的入口地址上

 

6

Q:基础知识我都懂了,Thunk技术具体怎么实现呢?

A:先考虑如下场景:

现有某第三方库提供接口如下

typedef void (*CB)(int a);

void thirdpart (CB func,int a);

你这边有一个类A,其中f函数是为Interface中的参数“量身定制的”。

class A

{

public:

void f(int a);

};

问题还是出现了,由于成员函数的prototype中存在隐式的参数this,所以当你写下

thirdpart (&A::f,1);时编译器还是无情的报错了。怎么办?第三方库显然是改不了的,自己这边也不好改,怎么办?嗯,是时候使用Thunk技术了。

 

#include "stdafx.h"

#include "wtypes.h"

#include <iostream>

using namespace std;

typedef void (_stdcall *CB)(int a);

void thirdpart(CB func,int a)

{

    func(a);

};

template<typename dst_type,typename src_type>

dst_type pointer_cast(src_type src) {

    return *static_cast<dst_type*>( static_cast<void*>(&src) );

}

#pragma pack(push,1)

typedef struct thunk

{

    BYTE  ins_move_this_to_ecx;

    DWORD this_A;

    BYTE  ins_jmp;

    DWORD addr;

    void init(DWORD proc,void* pthis)

    {

        ins_move_this_to_ecx = 0xb9;

        this_A = (DWORD)pthis;

        addr = (DWORD)proc - ((DWORD)this + sizeof(struct thunk));

        ins_jmp = 0xe9;

        //FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));

    }

}THUNK;

#pragma pack(pop)

class A

{

    public:

        A():x(10){}

    thunk t;

    CB c;

    int x;

    void f(int a)

    {

        printf("im in f:%d %d\n",a,x);

    }

    void start()

    {

        t.init(pointer_cast<int>(&A::f),this);

        c = (CB)&t;

        c(1);

    }

};

int _tmain(int argc, _TCHAR* argv[])

{

    A a;

    a.start();

    thirdpart(CB(&a.t),1);

    system("pause");

    return 0;

}

先解释下这里一些语句

1

#pragma pack(push,1)

struct x

{

    …

}

#pragma pack(pop)

是希望编译器把x结构体中的数据按照1字节的方式对齐。

#pragma pack(push,1)做了两件事,先把当前对齐方式压栈(32位下一般是4字节),然后修改对齐方式为1

#pragma pack(pop)将先前对齐方式出栈,就是恢复到4字节的对齐方式,为什么要恢复?因为我们只希望编译器对特定的结构体进行这种操作,对于其余的我们希望用默认操作

2

DWORD == unsigned long

BYTE == unsigned char

 

这段代码的核心是THUNK结构体,

typedef struct thunk

{

    BYTE  ins_move_this_to_ecx;

    DWORD this_A;

    BYTE  ins_jmp;

    DWORD addr;

    void init(DWORD proc,void* pthis)

    {

        ins_move_this_to_ecx = 0xb9;

        this_A = (DWORD)pthis;

        ins_jmp = 0xe9;

        addr = (DWORD)proc - ((DWORD)this + sizeof(struct thunk));

        //FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));

    }

}THUNK;

 

结构体里面存有2条机器指令,因为每条机器指令分为操作符合操作数2部分,所以2条机器指令用4个变量来表示。值得注意的是操作符只需要用BYTE来存储就够了,而操作数因为涉及地址,需要用DWORD来存储。

 

当编译器看到如下两条语句后,就会把他们会变成(已简化)

        c = (CB)&t;

        c(1);

push 1

call c

c:mov ecx,this

jmp addr

为什么会这样呢?首先c里面存的是THUNK结构体的入口地址,即里面存的第一条指令的地址,然后我们把c转化成一个函数指针,类型是CB。这时,当编译器看到c(1)这句时,它完全认为这只是一个函数调用,所以它把这句汇编成:

push 1

call c

倘若c是一个实实在在的函数指针,这时会跳转到该函数的入口处,即该函数的第一条指令处,假如c是像我们这样指的是一个结构体的入口地址,而结构体里面存的又是机器指令,情况会怎样?其实和第一种情况一样的,这时CPU已经准备开始执行结构体里的这个“变量”了。这就是Thunk技术的核心原理了。

 

整理一下思路,为了让机器在某一个时候执行我们写下的机器指令:

1

我们定义了一个结构体,在结构体里面我们可以用2个变量来准确描述一条机器指令,于是我们便有了机器指令集

2

把该结构体地址赋给一个函数指针

3

用该函数指针假装函数调用,实际上是“欺骗”编译器执行我们的结构体里面的指令

 

掌握原理后,下面要做的就是在不同的场合合理利用这个技术了。回到之前提出的三方库的问题,thirdpart(&A::f,1);已经不行了,但是thirdpart(CB(&a.t),1);却是可以的,只要THUNK对象t里面含有跳转到f的指令即可(我们不是已经这样做了么?)

 

好了,至此已经把Thunk技术介绍的差不多了,犹如一个拼图游戏,我已经把大部分piece找好了放在你的面前,并且拼了大概,剩下的一些piece还有把它完整拼出来的活就交给你了(比如这句怎么理解:addr = (DWORD)proc - ((DWORD)this + sizeof(struct thunk)); 又比如typedef void (_stdcall *CB)(int a);这里为什么是__stdcall而不是__thiscall,我们不是在调用成员函数吗?)。

Have fun!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值