游戏安全实验室 首页 技术入门 查看内容

 阅读目录

5种隐蔽的外挂获取执行时机方法介绍

发布于:2016-6-28 15:30   |    140991次阅读 作者: 管理员    |   原作者: TP   |   来自: 原创

一、模拟前的设定

    本次模拟外挂获取执行时机,新建一MFC程序FakeGamke,并模拟游戏中玩家攻击后的伤害计算,界面如下:

            

 

    模拟游戏中玩家相关的类如下:

class CGameObject  //游戏对象基类

{

public:

    CGameObject()

    {

        m_pAttack = new int;

    }

    ~CGameObject()

    {

        delete m_pAttack;

    }

    int GetAttack()

    {

        return *m_pAttack;

    }

    void SetAttack(int nAttack)

    {

        *m_pAttack = nAttack;

    }

 

    int GetAdditionAttack()

    {

        return m_nAdditionAttack;

    }

    void SetAdditionAttack(int nAdditionAttack)

    {

        m_nAdditionAttack = nAdditionAttack;

    }

    

    virtual TCHAR * GetName()

    {

        return _T("");

    }

    //其他成员函数...

    virtual float ComputeDamage() //计算伤害

    {

        float fDamage = (GetAttack() + GetAdditionAttack()) * 1.0;

        return fDamage;

    }

 

private:

    int *m_pAttack;         //基础攻击力

    int m_nAdditionAttack//攻击力加成

    //....游戏对象其他属性

};

 

class CGamePlayer : public CGameObject

{

public:

    CGamePlayer()

    {

        m_szPlayerName = new TCHAR[20];

        _tcscpy(m_szPlayerName_T("天下第一"));

        SetAttack(55);

        SetAdditionAttack(12);

        SetPreAttack(0.234);

    }

    ~CGamePlayer()

    {

        delete[] m_szPlayerName;

    }

    float GetPreAttack()

    {

        return m_fPreAttrack;

    }

    void SetPreAttack(float fPreAttack)

    {

        m_fPreAttrack = fPreAttack;

    }

 

    virtual TCHAR * GetName()

    {

        return m_szPlayerName;

    }

    virtual float ComputeDamage()  //计算伤害的函数

    {

        float fDamage = (__super::ComputeDamage())*(1 + GetPreAttack());

        return fDamage;

    }

private:

    float m_fPreAttrack;     //攻击比例加成

    TCHARm_szPlayerName;   //玩家名称

    // ... 玩家其他属性

};

 

    其中假设 输出伤害 = (攻击力 + 攻击力加成)*(1+攻击比例加成)。当点击攻击按键时表示玩家攻击动作,此时计算输出伤害并显示。

#include "FakePlayer.h"

CGameObjectg_LocalPlayer = new CGamePlayer();  //本地玩家

 

void CFakeGameDlg::OnBnClickedButtonAttack()  //点击攻击将计算伤害

{

    float fDamage = g_LocalPlayer->ComputeDamage();

    CString stTmp;

    stTmp.Format(_T("%0.3f"), fDamage);

    SetDlgItemText(IDC_EDIT_DAMAGEstTmp);

 

    stTmp.Format(_T("[TFFLAG] ThreadID:0x%x"), GetCurrentThreadId());

    OutputDebugString(stTmp);

}

 

void CFakeGameDlg::OnBnClickedButtonDisplay()  //点击显示玩家属性

{

    // TODO: Add your control notification handler code here

    CString stTmp;

    stTmp.Format(_T("%d"), g_LocalPlayer->GetAttack());

    SetDlgItemText(IDC_EDIT_ATTACKPOWERstTmp);

    stTmp.Format(_T("%d"), g_LocalPlayer->GetAdditionAttack());

    SetDlgItemText(IDC_EDIT_ATTACKADDITIONstTmp);

    stTmp.Format(_T("%0.3f"), ((CGamePlayer*)g_LocalPlayer)->GetPreAttack());

    SetDlgItemText(IDC_EDIT_ATTACKPERstTmp);

    stTmp.Format(_T("%s"), g_LocalPlayer->GetName());

    SetDlgItemText(IDC_EDIT_NAMEstTmp);

}

 

    此次模拟 输出伤害为82.678 = (55 + 12)*(1+0.234)。FakeGame正常执行可得伤害值82.678。现模拟如何获取FakeGame的伤害计算时机,并改变其中部分属性或输出伤害结果值,达到高额伤害效果。

 

 

二、方法

1、虚函数地址&虚表地址替换

 

C++中的虚函数的作用主要是实现了多态的机制,虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表保存的是类的虚函数地址,这张表解决了类的继承、多态、覆盖问题。有虚函数的类的实例中这个表地址会被分配到这个实例的内存中。

既然虚表中存放的是类的成员函数的地址,那如果将其替换,就给了外挂代码一次执行的机会。在FakeGame中,CGamePlayer类继承于CGameObject类。其中ComputeAttack(计算伤害函数)为虚函数。这两个类的结构如下:

 

    CGameObject 与CGamePlayer的虚表会被分配到PE文件中的.rdata区段,具体如下:

 

    在生成CGameObject、CGamePlayer对象时,虚表地址会赋值为对象内存的第一个四字节。(C++对象的内存布局非本文重点,读者自己了解一下)。

    在CGamePlayer对象调用函数ComputeAttack时,通过对象地址获取虚表地址,再通过虚表获取函数地址。

    

    根据上面的知识,现在我们通过替换虚表内的函数地址的方式,替换CGamePlayer对象虚表内的ComputeAttack函数地址,使得FakeGame在攻击计算伤害时执行替换后的函数,以此达到劫持FakeGame执行时机的目的。

 

 

首先我们要获取FakeGame程序中类CGamePlayer的对象地址,其保存在全局变量g_LocalPlayer中,g_LocalPlayer地址为0x005b3db8。

 

 

新建一DLL工程VTFuncHijack,生成注入FakeGame的VTFuncHijack.dll。其中劫持FakeGame执行的相关代码如下:

typedef float(*FuncComputeDamage)();

 

DWORD g_dwLocalPlayer = 0x005B3DB8;  //保存本地玩家的全局变量

FuncComputeDamage g_funcOldComputeDamage = 0;

float My_ComputeDamage()

{

    //修改方法一 直接返回高额伤害值

    //return 999999.54f;  

    //方法二  修改参与伤害计算的玩家属性,并恢复

    DWORD dwAttack = 0;

    //通过ASM汇编代码修改,可以防止修改部分寄存器,比如ecx

    _asm

    {

        pushad

            mov eax, g_dwLocalPlayer

            mov eax, dword ptr ds : [eax]; 获取对象地址

            mov eax, dword ptr ds : [eax + 4]; 获取int *m_pAttack; 成员

            mov edx, dword ptr ds : [eax]; 获取原始攻击力

            mov dwAttack, edx

            mov dword ptr ds : [eax], 999999; 修改为高额攻击力

            popad

    }

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;  //获取对象地址

    DWORD dwAttachPtr = *(DWORD*)(dwPlayer + 4);

    *(DWORD*)dwAttachPtr = 999999;               //赋值高额攻击力

    float fDamage = g_funcOldComputeDamage();  //调用原计算伤害的函数

    //攻击力计算之后恢复,防止外挂对抗者检测到玩家攻击力的改变

    _asm

    {

        pushad

            mov eax, g_dwLocalPlayer

            mov eax, dword ptr ds : [eax]

            mov eax, dword ptr ds : [eax + 4]

            mov edx, dwAttack

            mov dword ptr ds : [eax], edx; 恢复攻击力

            popad

    }

 

    return fDamage;

}

 

void InitDll()  //DLL_PROCESS_ATTACH 时调用 初始化

{

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;   //获取对象地址

    DWORD dwPlayerVt = *(DWORD*)dwPlayer;        //获取对象虚表地址

    //保存原函数地址,便于卸载时恢复

    g_funcOldComputeDamage = *(FuncComputeDamage*)(dwPlayerVt + 0x4);

 

    //虚表保存在rdata段,属于只读内存,修改前需要改变内存属性

    DWORD oldPro;

    VirtualProtect((LPVOID)dwPlayerVt, 4, PAGE_READWRITE, &oldPro);

    //dwPlayerVt + 4 为虚函数 ComputeAttack

    *(DWORD*)(dwPlayerVt + 0x4) = (DWORD)My_ComputeDamage;

    VirtualProtect((LPVOID)dwPlayerVt, 4, oldPro, &oldPro);

}

 

void UninitDll() //DLL_PROCESS_DETACH 时调用 卸载

{

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;   //获取对象地址

    DWORD dwPlayerVt = *(DWORD*)dwPlayer;        //获取对象虚表地址

 

    //虚表保存在rdata段,属于只读内存,修改前需要改变内存属性

    DWORD oldPro;

    VirtualProtect((LPVOID)dwPlayerVt, 4, PAGE_READWRITE, &oldPro);

    *(DWORD*)(dwPlayerVt + 0x4) = (DWORD)g_funcOldComputeDamage;           //恢复原地址

    VirtualProtect((LPVOID)dwPlayerVt, 4, oldPro, &oldPro);

}

 

VTFuncHijack工程中,虚函数替换法是通过对象g_LocalPlayer获取.rdata段中CGamePlayer的虚表地址。然后替换虚表内函数CGamePlayer::ComputeDamage地址,达到劫持程序执行的目的。但是这样做的话,外挂分析工作者通过dump程序FakeGame的模块内存,然后对比使用外挂前后的内存。可以明显分析到外挂修改了rdata段的虚函数地址。

     上图可以看到左边是VTFuncHijack.dll注入之前的rdata段,值0x402270为CGamePlayer::ComputeDamage函数的地址。右边为VTFuncHijack.dll注入之后的,已被VTFuncHijack.dll改为0x71571000。所以外挂分析工作者,只要简单的通过dump内存对比,就可以找出外挂的修改点,从而对抗外挂。

现我们使用一个新的方法叫虚表替换法,我们在DLL中自己分配一块内存,然后把CGamePlayer的虚表全部拷贝过来,修改我们要劫持的函数,再将g_LocalPlayer对象的虚表地址改为分配的新内存地址。这样的话,我们没有修改.rdata段,使得外挂分析工作者比较难通过dump内存的方式找到外挂的修改点。

 

新建一DLL工程VTHijack,生成注入FakeGame的VTHijack.dll。劫持FakeGame执行的相关代码如下:

 

typedef float (*FuncComputeDamage)();

 

DWORD g_dwLocalPlayer = 0x005B3DB8;  //保存本地玩家的全局变量

FuncComputeDamage g_funcOldComputeDamage = NULL;

PVOID g_MyVfTable = NULL;

DWORD g_dwOrgPlayerVt = 0;

float My_ComputeDamage()

{

    //修改方法一 直接返回高额伤害值

    //return 999999.54f;  

    //方法二  修改参与伤害计算的玩家属性,并恢复

    DWORD dwAttack = 0;

    //通过ASM汇编代码修改,可以防止修改部分寄存器,比如ecx

    _asm

    {

        pushad 

        mov eax, g_dwLocalPlayer

        mov eax, dword ptr ds:[eax] ;获取对象地址

        mov eax, dword ptr ds:[eax + 4]; 获取int *m_pAttack; 成员

        mov edx, dword ptr ds:[eax]      ;获取原始攻击力

        mov dwAttack, edx

        mov dword ptr ds:[eax], 999999   ;修改为高额攻击力

        popad 

    }

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;  //获取对象地址

    DWORD dwAttachPtr = *(DWORD*)(dwPlayer + 4);

    *(DWORD*)dwAttachPtr= 999999;               //赋值高额攻击力

    float fDamage =  g_funcOldComputeDamage();  //调用原计算伤害的函数

    //攻击力计算之后恢复,防止外挂对抗者检测到玩家攻击力的改变

    _asm

    {

        pushad

        mov eax, g_dwLocalPlayer

        mov eax, dword ptr ds:[eax]

        mov eax, dword ptr ds:[eax + 4]

        mov edx, dwAttack

        mov dword ptr ds:[eax], edx  ;恢复攻击力

        popad

    }

 

    return fDamage;

}

 

void InitDll()  //DLL_PROCESS_ATTACH 时调用 初始化

{

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;   //获取对象地址

    g_dwOrgPlayerVt = *(DWORD*)dwPlayer;         //获取对象虚表地址 并保存

    //先拷贝虚表

    g_MyVfTable = malloc(8);

    if (g_MyVfTable)

    {

        memcpy(g_MyVfTable, (PVOID)g_dwOrgPlayerVt, 8);

        g_funcOldComputeDamage = FuncComputeDamage(*(DWORD*)((unsigned char*)g_MyVfTable + 0x4));

        *(DWORD*)((unsigned char*)g_MyVfTable + 0x4) = (DWORD)My_ComputeDamage;

        *(DWORD*)dwPlayer = (DWORD)g_MyVfTable;  //替换虚表

    }

}

 

void UninitDll() //DLL_PROCESS_DETACH 时调用 卸载

{

DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;   //获取对象地址

 

    //恢复虚表

    if (g_MyVfTable)

    {

        free(g_MyVfTable);

        g_MyVfTable = NULL;

        *(DWORD*)dwPlayer = g_dwOrgPlayerVt;  //替换虚表

    }

}

 

 

两次劫持FakeGame效果如下图:

  

        图1  VTHijack.dll注入前                                图2  VTHijack.dll注入后

 

 

2、硬件断点法

    硬件断点和DRx调试寄存器有关,共有8个,从DRx0到DRx7。各个寄存器用途如下:

    DR0~DR3:调试地址寄存器,保存设置硬件断点的地址;

    DR4~DR5:保留,未公开具体作用;

    DR6:调试寄存器组状态寄存器;

    DR7:调试寄存器组控制寄存器。

硬件断点原理是使用4个调试寄存器(DR0、DR1、DR2、DR3)来设置断点地址,DR7设定断点状态(对地址硬件读、写还是执行,是对字节Byte、字Wrod还是双字Dword)。最多只能设置4个断点(只有四个寄存器)。

 

    模拟使用硬件断点方法劫持FakeGame计算输出伤害,通过IDA可知在函数CFakeGameDlg::OnBnClickedButtonAttack(CFakeGameDlg *this) + 0x33(0x004022D3)处调用了函数CGamePlayer::ComputeDamage。

    新建DLL工程HDbp,生成注入FakeGame的HDbp.dll。使用硬件断点劫持FakeGame执行的相关代码如下:

 

 

DWORD g_dwHDbpAddr = 0x004022D0;     //设置硬件断点地址

DWORD g_dwLocalPlayer = 0x005B3DB8;  //FakeGame程序中g_LocalPlayer地址

DWORD g_dwAttack = 0;                //保存玩家原始攻击力

LONG WINAPI TopLevelExceptionFilter(__in struct _EXCEPTION_POINTERS *ExceptionInfo)

{

    if((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == g_dwHDbpAddr) 

    {

        OutputDebugStringA("[HDBP] HDbp has happend");

        DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer; //获取对象地址

        DWORD dwAttackPtr = *(DWORD*)(dwPlayer + 4);

        if(g_dwAttack == 0)

        {

            g_dwAttack = *(DWORD*)dwAttackPtr;  //保存原始攻击力,用于恢复

        }

        *(DWORD*)dwAttackPtr= 999999;           //获取对象成员m_nAttack的地址,并赋值高额攻击力

        //模拟指令:.text:004022D0 8B 40 04   mov     eax, [eax+4]; 通过虚表获取函数ComputeDamage

        //需要跳过设置软件断点的指令,不然一直触发异常导致死循环

        ExceptionInfo->ContextRecord->Eax = *(DWORD*)(ExceptionInfo->ContextRecord->Eax + 0x4); // mov edx, [eax]

        ExceptionInfo->ContextRecord->Eip = g_dwHDbpAddr + 3;  //修改eip 跳过指令

        return EXCEPTION_CONTINUE_EXECUTION;

    }

    return EXCEPTION_CONTINUE_SEARCH;

}

 

//获取主线程ID

DWORD GetMainThreadId()

{

    DWORD processId = GetCurrentProcessId();

    DWORD threadId = 0;

    THREADENTRY32 te32 = { sizeof(te32) };

    HANDLE threadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);

 

    if(Thread32First(threadSnap, &te32))

    {

        do

        {

            if( processId == te32.th32OwnerProcessID)

            {

                threadId = te32.th32ThreadID;

                break;

            }

        }while(Thread32Next(threadSnap, &te32));

    }

    return threadId;

}

 

LPTOP_LEVEL_EXCEPTION_FILTER g_OldFilter = NULL;

void InitDll()  //DLL_PROCESS_ATTACH 时调用 初始化

{

    //设置硬件断点 以及捕捉断点异常的函数

 

    //使用UEH 接管异常

    g_OldFilter = SetUnhandledExceptionFilter(&TopLevelExceptionFilter); //设置异常捕捉函数

    

    char szMsg[100] = {0};

    sprintf_s(szMsg, 100, "[HDBP] OldFilter:%d", g_OldFilter);

    OutputDebugStringA(szMsg);

 

    //获取FakeGame主线程handle

    DWORD dwThreadId = GetMainThreadId();

    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);

    if(!hThread)

    {

        sprintf_s(szMsg, 100, "[HDBP] InitDll OpenThread MainThreadID:0x%x LastError:%d", dwThreadId, GetLastError());

        OutputDebugStringA(szMsg);

        return;

    }

 

    CONTEXT ctxt;

    ctxt.ContextFlags = CONTEXT_ALL; //设置CONTEXT 标志

    if(!GetThreadContext(hThread, &ctxt))

    {

        sprintf_s(szMsg, 100, "[HDBP] InitDll GetThreadContext hThread:%d LastError:%d", hThread, GetLastError());

        OutputDebugStringA(szMsg);

        return;

    }

 

    //设置硬件执行断点地址

    ctxt.Dr0 = g_dwHDbpAddr;

 

    //决定使用哪个寄存器

    DWORD dwDrFlags = ctxt.Dr7;

    dwDrFlags |= (DWORD)0x1;

    ctxt.Dr7 = dwDrFlags;

 

    if(!SetThreadContext(hThread, &ctxt))

    {

        sprintf_s(szMsg, 100, "[HDBP] InitDll SetThreadContext LastError:%d", GetLastError());

        OutputDebugStringA(szMsg);

        return;

    }

}

 

void UninitDll() //DLL_PROCESS_DETACH 时调用 卸载

{

    //取消硬件断点

    SetUnhandledExceptionFilter(g_OldFilter); //设置异常捕捉函数

 

    OutputDebugStringA("[HDBP] UninitDll");

 

    char szMsg[100] = {0};

    //获取FakeGame主线程handle

    DWORD dwThreadId = GetMainThreadId();

    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);

    if(!hThread)

    {

        sprintf_s(szMsg, 100, "[HDBP] UninitDll OpenThread MainThreadID:0x%x LastError:%d", dwThreadId, GetLastError());

        OutputDebugStringA(szMsg);

        return;

    }

 

    CONTEXT ctxt;

    ctxt.ContextFlags = CONTEXT_ALL; //设置CONTEXT 标志

    if(!GetThreadContext(hThread, &ctxt))

    {

        sprintf_s(szMsg, 100, "[HDBP] UninitDll GetThreadContext hThread:%d LastError:%d", hThread, GetLastError());

        OutputDebugStringA(szMsg);

        return;

    }

 

    //设置硬件执行断点地址

    ctxt.Dr0 = 0;

    //决定使用哪个寄存器

    ctxt.Dr7 = 0;

 

    if(!SetThreadContext(hThread, &ctxt))

    {

        sprintf_s(szMsg, 100, "[HDBP] UninitDll SetThreadContext LastError:%d", GetLastError());

        OutputDebugStringA(szMsg);

        return;

    }

 

    //恢复玩家原始攻击力

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer; //获取对象地址

    DWORD dwAttackPtr = *(DWORD*)(dwPlayer + 4);

    *(DWORD*)dwAttackPtr = g_dwAttack;      

}

 

     硬件断点法的好处在于没有对游戏代码进行修改,所以常规的代码完整性对比检测是发现不了的,相对安全性较高。其不便的地方在于硬件断点是基于硬件的,能设置硬件断点个数是有限的,如果需要挂钩的地方太多则硬件断点可能不够用。

注意到,当DLL卸载时,才恢复玩家的攻击力。读者可以如何通过硬件断点的方式防止外挂对抗者检测到玩家攻击力的改变(参考VTHijack和VTFuncHijack工程)。

 

 

3、软件断点法

软件断点法即Int3 指令(0xCC)对应OD调试器里面的F2。当程序执行INT3指令时,会触发一个异常交给调试器或者异常处理函数。INT3断点可以设置无数个,而硬件断点只能设置四个。但是INT3断点容易被软件检测到。

 

   新建DLL工程INT3bp,生成注入FakeGame的INT3bp.dll。使用软件断点劫持FakeGame执行的相关代码如下:

 

DWORD g_dwINT3bpAddr = 0x004022D0;    //设置软件断点地址

DWORD g_dwLocalPlayer = 0x005B3DB8;   //FakeGame程序中g_LocalPlayer地址

DWORD g_dwAttack = 0;                 //用于保存原始 攻击力

LPTOP_LEVEL_EXCEPTION_FILTER g_OldFilter = NULL; 

BYTE g_bOrgCode = 0;                  //保存设置软件断点地址处的code

 

LONG WINAPI TopLevelExceptionFilter(__in struct _EXCEPTION_POINTERS *ExceptionInfo)

{

    if((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == g_dwINT3bpAddr) 

    {

        OutputDebugStringA("[INT3BP] INT3bp has happend");

        DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer; //获取对象地址

        DWORD dwAttackPtr = *(DWORD*)(dwPlayer + 4);

        if(g_dwAttack == 0)

        {

            g_dwAttack = *(DWORD*)dwAttackPtr;  //保存原始攻击力,用于恢复

        }

        *(DWORD*)dwAttackPtr= 999999;           //获取对象成员m_nAttack的地址,并赋值高额攻击力

        //模拟指令:.text:004022D0 8B 40 04   mov     eax, [eax+4]; 通过虚表获取函数ComputeDamage

        //需要跳过设置软件断点的指令,不然一直触发异常导致死循环

        ExceptionInfo->ContextRecord->Eax = *(DWORD*)(ExceptionInfo->ContextRecord->Eax + 0x4); // mov edx, [eax]

        ExceptionInfo->ContextRecord->Eip = g_dwINT3bpAddr + 3;  //修改eip 跳过指令

 

        return EXCEPTION_CONTINUE_EXECUTION;

    }

    return EXCEPTION_CONTINUE_SEARCH;

}

 

void InitDll()  //DLL_PROCESS_ATTACH 时调用 初始化

{

    //使用UEH 接管异常

    g_OldFilter = SetUnhandledExceptionFilter(&TopLevelExceptionFilter); //设置异常捕捉函数

 

    //修改内存属性

    DWORD oldPro;

    VirtualProtect((LPVOID)g_dwINT3bpAddr, 1, PAGE_EXECUTE_READWRITE, &oldPro);  

    g_bOrgCode = *(BYTE*)g_dwINT3bpAddr;        //保存原始指令,方便恢复

    *(BYTE*)g_dwINT3bpAddr = 0xcc;              //设置INT3 指令

    VirtualProtect((LPVOID)g_dwINT3bpAddr, 1, oldPro, &oldPro);

}

 

void UninitDll() //DLL_PROCESS_DETACH 时调用 卸载

{

    SetUnhandledExceptionFilter(g_OldFilter); //恢复程序异常捕捉函数

 

    //修改内存属性

    DWORD oldPro;

    VirtualProtect((LPVOID)g_dwINT3bpAddr, 1, PAGE_EXECUTE_READWRITE, &oldPro);  

    *(BYTE*)g_dwINT3bpAddr = g_bOrgCode;

    VirtualProtect((LPVOID)g_dwINT3bpAddr, 1, oldPro, &oldPro);

 

    //恢复玩家原始攻击力

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer; //获取对象地址

    DWORD dwAttackPtr = *(DWORD*)(dwPlayer + 4);

    *(DWORD*)(dwAttackPtr) = g_dwAttack;      

}

 

    通过代码可以看到,软件断点和硬件断点差不多,只是触发异常的方式不太一样。软件断点优势是断点个数无限,可以有任意多个软断点。而不便之处在于通过修改内存实现,所以代码完整性是可以检测的,而且性能上与硬件断点相比也会差一些。

 

 

 

4、构造异常法

硬件断点法和软件断点法都是通过触发异常,并设置异常处理函数来获取程序执行逻辑。这里介绍一个新的实现方式,设置某些需要被访问的地址为不可访问的方式触发异常。

 

在CGamePlayer类中继承了CGameObject的一个指针成员int *m_pAttack; 在类构造函数中被初始化,后面计算伤害函数ComputeDamage中,m_pAttack被访问。如何我们在m_pAttack被访问之前设置其为NULL,则在其被访问时会触发异常。通过IDA以及源码分析,有两处会访问m_pAttack:

 

 

 

新建DLL工程Except,生成注入FakeGame的Except.dll。使用触发异常法劫持FakeGame执行的相关代码如下:

 

DWORD g_dwLocalPlayer = 0x005B3DB8;     //FakeGame程序中g_LocalPlayer地址

DWORD g_dwAddAttack = 0;                   //用于保存原始 攻击力

LPTOP_LEVEL_EXCEPTION_FILTER g_OldFilter = NULL; 

DWORD g_dwAttackPtr = NULL;

 

LONG WINAPI TopLevelExceptionFilter(__in struct _EXCEPTION_POINTERS *ExceptionInfo)

{

    //打印异常相关地址

    char szMsg[100];

    sprintf_s(szMsg, 100, "[EXCEPT] Except has happend 0x%08x", (DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress);

    OutputDebugStringA(szMsg);

 

    if ((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == 0x004023ED)

    {

        //.text:004023ED FF 30   push    dword ptr[eax]; 获取指针指向的值;会触发异常

        //模拟 push dword ptr[eax]  其中eax为int *m_pAttack指针。 dword ptr[eax]为攻击力

        ExceptionInfo->ContextRecord->Esp = ExceptionInfo->ContextRecord->Esp - 4;

        *(DWORD*)ExceptionInfo->ContextRecord->Esp = 999999;

        ExceptionInfo->ContextRecord->Eip = ExceptionInfo->ContextRecord->Eip + 2;  //修改eip 跳过指令

        return EXCEPTION_CONTINUE_EXECUTION;

    }

    if ((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == 0x00402284)

    {

        //.text:00402284 8B 00                                         mov     eax, [eax]   

        ExceptionInfo->ContextRecord->Eax = 999999;

        ExceptionInfo->ContextRecord->Eip = ExceptionInfo->ContextRecord->Eip + 2;  //修改eip 跳过指令

        return EXCEPTION_CONTINUE_EXECUTION;

    }

    return EXCEPTION_CONTINUE_SEARCH;

}

 

void InitDll()  //DLL_PROCESS_ATTACH 时调用 初始化

{

    //使用UEH 接管异常

    g_OldFilter = SetUnhandledExceptionFilter(&TopLevelExceptionFilter); //设置异常捕捉函数

 

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;

    g_dwAttackPtr = *(DWORD*)(dwPlayer + 0x4);  //保存原始指针

    *(DWORD*)(dwPlayer + 0x4) = 0;               //将指针设置为0,当访问时触发异常

 

}

 

void UninitDll() //DLL_PROCESS_DETACH 时调用 卸载

{

    SetUnhandledExceptionFilter(g_OldFilter); //恢复程序异常捕捉函数

 

    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;

    *(DWORD*)(dwPlayer + 0x4) = g_dwAttackPtr;  //恢复

}

 

    构造异常法与硬件断点与软件断点相比隐蔽性更胜一筹。硬件断点可以通过查看线程上线文来确认,软件断点可以通过代码完整性来确认,而构造异常法则没有明显的破绽。

    基于异常的逻辑接管VEH、SEH、UEH。当程序发生异常时,操作系统一般有三种方式处理异常,一种是筛选器异常处理UEH(通过函数SetUnhandledExceptionFilter设置的回调),一种是SEH结构化异常处理(Structure Exception Handler,一种是VEH向量化异常处理(Vectored Exception Handling,可通过函数AddVectoredExceptionHandler和AddVectoredContinueHandler设置),具体内容,可以百度相关关键字或查阅相关书籍。

 前面例子中硬件断点、软件断点法、触发异常法演示了使用SetUnhandledExceptionFilter设置的UEH去捕捉异常接管程序的执行流程,读者如果有兴趣可以尝试使用VEH去实现。

 

5、钩子以及消息响应

在Windows平台上可以设置回调函数以监视指定窗口的某种消息,比如鼠标点击、键盘按键等。外挂中,会使用钩子方式去注入DLL,关于使用钩子注入DLL的原因,可以网上搜索。现在我们演示,如何通过消息钩子的方式去感知/劫持程序的一些执行流程。

首先建立一个MFC工程SetMsgHook,用于设置钩子,以及钩子的回调例程。相关代码如下:

 

 

HHOOK g_hhMsg = NULL;

HMODULE g_hMsgHook = NULL;

 

int MsgHook(TCHARszDll)

{

    g_hMsgHook = LoadLibrary(szDll);

    int nLastError = GetLastError();

    if(!g_hMsgHook)

    {

        ::MessageBox(NULL_T("获取钩子dll失败"), _T("提示"), 0);

        return 0;

    }

    HOOKPROC hkProcess = (HOOKPROC)GetProcAddress(g_hMsgHook"MsgHookProc");

 

    if(hkProcess == NULL)

    {

        ::MessageBox(NULL_T("获取钩子procaddr失败"), _T("提示"), 0);

        return 0;

    }

    FARPROC hHook = GetProcAddress(g_hMsgHook"g_hHook");

    if (hHook != NULL )

    {

        g_hhMsg = SetWindowsHookEx(WH_GETMESSAGEhkProcessg_hMsgHook, 0);

        if(g_hhMsg != NULL)

        {

            ::MessageBox(NULL_T("全局键盘钩子安装成功"), _T("提示"), 0);

        }else{

            ::MessageBox(NULL_T("全局键盘钩子安装失败"), _T("提示"), 0);

            return 1;

        }

    }else{

        ::MessageBox(NULL_T("全局键盘钩子已经安装"), _T("提示"), 0);

        return 1;

    }

}

 

void CSetMsgHookDlg::OnBnClickedSethookBtn() //设置钩子

{

    // TODO: Add your control notification handler code here

    //注意测试时,这里可能要修改DLL路径

    MsgHook(_T("E:\\Projects\\GSLab\\Release\\MsgHook.dll")); 

}

 

int UnKeyHook()

{

    if(g_hhMsg != NULL)

    {

        bool bRet = UnhookWindowsHookEx(g_hhMsg);

        if(g_hMsgHook)

        {

            FreeLibrary(g_hMsgHook);

        }

 

        if(bRet)

        {

            AfxMessageBox(_T("全局键盘钩子卸载成功"));

        }else{

            AfxMessageBox(_T("全局键盘钩子卸载失败"));

            return 0;

        }

    }else{

        AfxMessageBox(_T("没有安装全局键盘钩子"));

    }

    return 1;

}

 

void CSetMsgHookDlg::OnBnClickedUnsethookBtn()

{

    // TODO: Add your control notification handler code here

    UnKeyHook();

    this->OnOK();

}

 

 

新建DLL工程MsgHook,生成用于钩子回调的MsgHook.dll。使用消息钩子解除FakeGame执行流程的代码如下:

 

#pragma data_seg("ShareForxxWinHook")

HHOOK g_hHook = NULL;

#pragma data_seg()

 

BOOL APIENTRY DllMainHMODULE hModule,

                      DWORD  ul_reason_for_call,

                      LPVOID lpReserved

                      )

{

    return TRUE;

}

 

DWORD g_dwLocalPlayer = 0x005B3DB8;     //FakeGame程序中g_LocalPlayer地址

LRESULT CALLBACK MsgHookProc(int nCodeWPARAM wParamLPARAM lParam)

{

    PMSG pMsg = (PMSG)lParam;

    char szExeName[MAX_PATH];

 

    if (nCode==HC_ACTION)

    {

        GetModuleFileNameA(GetModuleHandle(NULL), szExeNameMAX_PATH);

        char szDri[MAX_PATH], szSubDir[MAX_PATH], szFile[MAX_PATH], szExt[MAX_PATH];

        _splitpath(szExeNameszDriszSubDirszFileszExt);

        if(stricmp(szFile"FakeGame") == 0)  

        {

            if(pMsg->message == WM_CHAR)  //监控FakeGame的WM_CHAR

            {

                sprintf_s(szExeNameMAX_PATH"[MSGHOOK] %d"pMsg->wParam);

                OutputDebugStringA(szExeName);

            }

            if(pMsg->message == WM_LBUTTONDOWN// 鼠标按下消息 

            {

                //判断鼠标点击是不是在"攻击" 按钮内

                //这里通过窗口句柄,获取按钮的文本

                char szText[100] = {0};

                ::GetWindowTextA(pMsg->hwndszText, 100);

                if(strcmp(szText"攻击") == 0)

                {

                    //点击攻击按钮前修改攻击力

                    DWORD dwPlayer = *(DWORD*)g_dwLocalPlayer;

                    DWORD dwAttackPtr =  *(DWORD*)(dwPlayer + 0x4);  //获取攻击力指针

                    *(DWORD*)dwAttackPtr = 999999;                   //设置高额攻击力

                }

            }

        }

    }

    return ::CallNextHookEx(g_hHooknCodewParamlParam);

}

 

钩子法其优点在于很少有人能使用,所以也就很少有人能想到这茬,隐蔽性相对较高。

 

三、习题

1、尝试编译相关工程,测试是否劫持成功。

2、学习了解C++对象内存布局

3、 使用VEH实现硬件断点法、软件断点法、触发异常法。


*转载请注明来自游戏安全实验室(GSLAB.QQ.COM)

分享到:
踩0 赞1

收藏

上一篇:常见的6种外挂获取执行时机方法介绍

下一篇:变速齿轮类实现原理

最新评论
B Color Image Link Quote Code Smilies

发表评论