外挂基础

0x02 数据查找

2.1 查找数据

2.2 查找隐藏数据

主要思路为通过可以知道的数据,并且由于大概率是通过结构体的方式进行定义所以可以查询到

2.3自定义数据类型的解读

2.3.1首地址的寻找

类定义实例:

class Role
{
    int hp;
    void beAct(int damage)
    {
        this->hp-=damage;  //hp会被读写,直接找哪个内存读写了hp
        
        //hp访问过程this指针+offset
    }
}

寻找思路:通过追踪哪个改写了该变量的值,从而找偏移量

通过追踪改写了hp的地址,发现有如下的汇编指令:

外挂基础

翻译为:[esi+10]=[esi+10]-edi

所以可以得出offset为10,edi为damage(this+offset=(this+offset)-damage)

所以esi为人物指针,偏移量为hp,所以可以通过hp的地址直接推断出整个role的首地址

在本次测试中,hp地址为4B6CC4,所以减去10H为4B6CB4为Role变量的地址。

建立该地址的变量,然后直接查看内存结果如下:

外挂基础

由于我们知道的是HP采用int类型进行存储,所以可以只通过4字节十进制的形式查看该段内存

外挂基础

由此我们就找到了该结构体的首地址。

2.3.2数据拆解

数据拆解主要是依靠于每个数据类型的大小,常用大小如下:

char 1字节

short 2字节

int 4字节

float 4字节

long long 8字节

double 8字节

如果出现如下的情况:

xx 00 xx 00 xx 00 00 00... 00 00 对于该情况可以拆解为wchar_t *

xx xx xx xx xx 00 00 00... 00 00 对于该情况可以拆解为char *

同时在拆解的过程中应当考虑内存对齐的问题,例如取数据中的一部分进行划分

03 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 1A 06 00 00

如果做如下拆解

03 char

00 00 00 02 int

00 00 00 00 00 00 00 00 00 00 00 1A 06 00 00 1A 06 00 00 58 00 00 00 1F 01 00 00 2B 01 00 00

就说明拆解错误,因为对于该类的前两个变量如下代码所示:

#include <iostream>
using namespace std;
class role
{
 public:
    char a;
    int b;
}
int main()
{
    role r
    cout<<sizeof(r)<<endl;
}

对于该程序的结果并不是1+4=5,而是8,所以a应当考虑内存对齐的问题。

注意该规则只针对于内存中的内存对齐,如果是网络数据包可以不遵守

所以对于该内存,可以大量的采用4个一分,同时填入已经分析数据

03 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 1A 06 00 00 1A 06 00 00 58 00 00 00 1F 01 00 00 2B 01 00 00 4B 01 00 00 6E 01 00 00 1D 01 00 00 0D 00 00 00 0D 00 00 00 01 00 00 00 74 0E 00 00 26 0F 00 00 01 00 00 00 0A 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 71 00 00 00 77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 71 00 00 00 77 00 00 00 71 00 00 00 77 00 00 00 0A 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 CF B9 AC B7 C9 D4 C6 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7A 2D C4 CF B9 AC B7 C9 D4 C6 2E 69 6E 69 00 D3 2E 69 6E 69 00 00 00 00 00 00 00 00 00 00 00 00

可以分为如下情况

03 00 00 00

02 00 00 00

00 00 00 00

00 00 00 00

1A 06 00 00

1A 06 00 00

58 00 00 00

1F 01 00 00

2B 01 00 00

4B 01 00 00

6E 01 00 00

1D 01 00 00

0D 00 00 00

0D 00 00 00

01 00 00 00

74 0E 00 00

26 0F 00 00

01 00 00 00

0A 00 00 00

05 00 00 00

00 00 00 00

00 00 00 00

71 00 00 00

77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

71 00 00 00

77 00 00 00

71 00 00 00

77 00 00 00

0A 00 00 00

01 00 00 00

01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 该段数据同理

C4 CF B9 AC B7 C9 D4 C6 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

7A 2D C4 CF B9 AC B7 C9 D4 C6 2E 69 6E 69 00 D3 2E 69 6E 69 00 00 00 00 00 00 00 00 00 00 00 00

最后两端数据我们猜测为char *,这里通过程序可以验证

#include <iostream>
using namespace std;
int main()
{
    char str1[] = { 0xC4,0xCF,0xB9,0xAC,0xB7,0xC9,0xD4,0xC6,0x00};
    cout << str1 << endl;
    return 0;
}

外挂基础

所以可以判断为 char[]

注意如果是char *,那么这里将是4个字节,保存的是指向的地址,char[]那么就是如图的形式保存

然后通过拆解数据可以直接得到源代码的定义方式

同时CE中可以工具自带了分析,该工具可以更加快速的分析出结果

外挂基础

问题在于小工具无法显示代码页,所以对于字符串的显示会存在一定的问题。

2.4显示游戏数据

步骤:

  1. 获得权限,打开进程:OpenProcess
  2. 读取指定内存:ReadProcessMemory
  3. 显示数据
HANDLE OpenProcess(
DWORD dwDesiredAccess, //渴望得到的访问权限(标志)
BOOL bInheritHandle, // 是否继承句柄
DWORD dwProcessId// 进程标示符
);
// 例如
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid);

BOOL ReadProcessMemory(
HANDLE hProcess,    //读取句柄
LPCVOID lpBaseAddress,   //读取地址
LPVOID lpBuffer,       //保存位置,接受缓冲区的指针
size_t * nSize,           //读取大小
size_t * lpNumberOfBytesRead   //成功读取大小,不用检测使用NULL
);

由于我们已经得到了整个的结构,所以可以通过如下形式进行读取:

这里使用绝对地址,在之后可以利用函数使用偏移量

#include <iostream>
#include <windows.h>
using namespace std;

struct Role {
    int unknown_1[4];
    int hp[2];  //当前/最大生命
    int tp[2];  //体力
    int mp[2];   //内力
    int act;    //攻击
    int def;   //防御
    int sf;    //身法
    int lv;    //等级
    int unknow_2;
    int exp[2];
    int speed;
    int unk_3[4];
    int x;
    int y;
    int unk_4[36];
    char name[32];
};
int main()
{
    DWORD pid;
    Role role;
linput:
    cout << "请输入游戏pid:";
    cin >> pid;
    HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS, false, pid);
    if (hProcess == NULL)
    {
        /*打开失败*/
        cout << "打开句柄失败!" << endl;
        goto linput;
    }
    else
    {
        while (1)//实时读取
        {
            system("cls");
            BOOL re = ReadProcessMemory(hProcess, (LPCVOID)0x4B6CB4, (LPVOID)&role, sizeof(role), NULL);    
            if (re)
            {
                cout << "生命/最大生命:" << role.hp[0] << "/" << role.hp[1] << endl;
                cout << "体力/最大体力:" << role.tp[0] << "/" << role.tp[1] << endl;
                cout << "内力/最大内力:" << role.mp[0] << "/" << role.mp[1] << endl;
                cout << "姓名:" << role.name << endl;
            }
            else
            {
                cout << "读取失败" << endl;
                break;
            }
            Sleep(500);//休眠0.5s
        }
        
    }
}

2.5简易锁血功能

BOOL WriteProcessMemory(
HANDLE hProcess,   //句柄
LPVOID lpBaseAddress,   //写入地址
LPVOID lpBuffer,       //写入数据指针
DWORD nSize,             
LPDWORD lpNumberOfBytesWritten
);
void lock_hp(HANDLE hProcess,int add)
{
    int hp = 9999;
    WriteProcessMemory(hProcess, (LPVOID)(add+0x10), &hp, sizeof(hp), NULL);
}

这里通过偏移量直接写入整个程序中,然后再主程序中利用while循环实现锁血

该方式有如下弊端:

  • 当damage足够大的时候锁血会失败

所以之后会利用hook实现真的锁血

  • 容易被反外挂发现

0x03 hook

3.1 hook基本原理

编译本质:将C++高级代码翻译为机器码,人可以和编译器沟通,编译器和计算机直接沟通。

编译完成后,会生成对应的二进制文件,当我们执行某个文件时,那么文件就会被从硬盘中加载到硬盘中,所以对于破解有如下的两种方式:

  • 改变磁盘文件(补丁/破解)
  • 在内存中修改文件(Hook)

例如如下的C++代码:

int add(int a,int b)
{
    return a+b;
}

对应如下的汇编代码:

push ebp
mov ebp,esp
mov eax,dword ptr[ebp+8]
add eax,dword ptr[ebp+0ch]
pop ebp
ret

对应如下的机器代码:

55 8B EC 8B 45 08 03 45 0C 5D C3
//当我们想要让add变为sub的时候,只需要将03改为29

应用场景:

  • 截取目标软件数据,如截取封包软件WPE

    • 通过修改send的流程,调用send函数过程进入自己函数从而获取参数
  • 破解,外挂,病毒,木马

3.2 手写hook实现无敌

之前的无敌时通过不断的写入数据,这样容易被反外挂软件发现,并且对于较大伤害该方式无效,所以需要通过hook修改整个掉血的流程。

class Role
{
    int hp;
    void beAct(int damage)
    {
        this->hp-=damage;  //hp会被读写,直接找哪个内存读写了hp
        
        //hp访问过程this指针+offset
    }
}

如下我们有三种方式:

  • 将伤害改为0
  • 不执行这句
  • 修改-的流程我+

对于整个函数的记录过程:

  1. hp被读取
  2. hp-damage被放入到一个临时变量中
  3. 该临时变量被写入到hp中

由于读取操作是一个常规操作,所以我们直接跟踪写入的过程,该过程记录了整个hp被操作的过程

外挂基础

如图的形式,我们直接显示该过程的反汇编代码

外挂基础

sub [esi+10],edi

该过程为[esi+10]=[esi+10]-edi

这个就如同之前的C++代码,所以我们修改这句代码的流程

a.nop

我们通过nop将整个指令进行跳过

外挂基础

由于之前是3个字节,但是nop为一个字节,所以需要填入三个

于是在该过程中我们实现了锁血

但是问题在于怪物的血量由于也是使用继承的方式使用该函数,所以怪物也实现了锁血

b.sub->add

将减法改为加法

外挂基础

该方式有一个问题,当数据到达一个值的时候会小于0,于是会死亡(大于int范围)

c.修改edi为0

修改edi为0的方式

mov edi,0
xor edi,edi

如果修改后可得如下的汇编代码:

//原本
sub [esi+10],edi
mov eax,[esi+10]
test eax,eax
jg Sword2.exe+1FE0F
mov ecx,esi
mov [esi+10],00000000
call Sword2.exe+1F110
//修改后
xor edi,edi
nop 
mov eax,[esi+10]
test eax,eax

如此修改将破坏原本的代码,需要将减法的过程

但是剩下的一个字节无法写入我们的代码,所以该方案不可行,所以我们可以让指令跳转到我们的区域,然后写入代码再跳转回来

3.3 手写汇编实现无敌

3.3.1 理解怪物无敌的原理

对于代码的逆向:

Role user,wolf;
user.beact(100);
wolf.beact(100);

这里都调用了beact()函数,当我们直接修改代码的内容就会导致对于所有怪物实现了无敌

两个调用了相同的函数,但是对象不同,通过esi进行传递

if(esi==&user)
    damage=0;

所以实质是判断地址的方式

3.3.2 构建汇编代码实现无敌

相关汇编代码:

cmp esi,xxxx
jne      xxxxx    //如果不等跳转jump not equal

所以对于整个过程可以有如下代码:

cmp esi 0x44CEF08
jne xxxx

外挂基础

由于cmp的代码需要7个字节,所以无法直接写入,需要我们先跳转到我们的代码部分,然后再执行完成后跳转回来

所以我们可以分配一个内存空间

然后对于原本的sub [esi+10],edi 位置使用jmp代码跳转到自己位置

023B0000 分配位置

0041FDB2 当前代码位置

jmp 023B0000

然后在该位置进行填写,但是由于在jmp代码中会占用到其他代码的位置,所以需要在自己的代码位置进行补写

/*原本位置*/
sub [esi+10],edi
mov eax,[esi+10]
test eax,eax
jg Sword2.exe+1FE0F
mov ecx,esi
/*修改之后*/
jmp 023B0000
nop 
test eax,eax
jg Sword2.exe+1FE0F
/*需要补写代码*/
sub [esi+10],edi
mov eax,[esi+10]

所以对于自己hook代码位置可以使用如下的写法:

023B0000 - cmp esi,004CEF08
023B0006 - jne 023B000A
023B0008 - xor edi,edi
023B000A - sub [esi+10],edi
023B000D - mov eax,[esi+10]
023B0010 - jmp 0041FDB8

其中第二句限制了如果不是user,那么就正常操作,否则就将edi归零。最后jmp跳回到原本的代码

问题在于,对于上一行代码:

外挂基础

跳转到了0041FDB5,但是由于我们的操作,该代码放在了我们的hook部分,由于直接跳转到我们的代码中,跳转距离比较远就会使用E9,源代码又被破坏,所以该位置不是最好的hook点,需要重新寻找hook地方

3.3.3 寻找合适的hook点

所以我们需要找到整个函数开始部分的,需要理解整个函数的运行过程,分析edi是从哪里得来的。

push esi
mov esi,ecx
push edi
cmp dword ptr [esi+04],02
jne 0041FD86
mov eax,[00537424]
test eax,eax
jne 0041FE3E
mov eax,[esi+10]
mov edi,[esp+0C]       

后面esp表示栈的位置,说明参数在栈中,整个通过调用得到edi,所以我们查看调用该函数的位置

push ebx
push edx
mov ecx,edi
call 0041FD40

说明在函数传入了两个参数,分别为ebx和edx,所以我们猜测该函数调用如下:

p->beact(&user,damage)

hook的位置一般情况下在函数头部做,如果函数头部有5个字节就在函数头部进行跳转,这里可以在如下位置做

外挂基础

hook恢复地址:0041FD65

hook地址:023C0000

/*修改前*/
mov edx,eax
sub edx,edi
cmp edx,ecx

所以对于hook位置在好的位置做会避开大量的代码修改

3.4 C++实现无敌

3.4.1 使用函数

该过程需要在内存空间中分配一个内存,然后我们在分配的内存中写入数据,所以使用到如下函数:

LPVOID VirtualAllocEx(
HANDLE hProcess,              //进程句柄
LPVOID lpAddress,             //一般为空,让系统分配
SIZE_T dwSize,
DWORD flAllocationType,       //内存分配类型
DWORD flProtect              //分配内存的属性
);
hProcess

同时由于游戏内存可能只有执行权限,需要进行修改权限

VirtualProtectEx(hProcess, (LPVOID)0x41FD61, 6, PAGE_EXECUTE_READWRITE,NULL);
BOOL VirtualProtectEx(
HANDLE hProcess, // 要修改内存的进程句柄
LPVOID lpAddress, // 要修改内存的起始地址
DWORD dwSize, // 页区域大小
DWORD flNewProtect, // 新访问方式
PDWORD lpflOldProtect // 原访问方式 用于保存改变前的保护属性 易语言要传址
);

3.4.2 构建hook代码

对于机器代码实际为二进制代码,所以我们需要开辟空间然后通过char[]写入对应的Code,同时跳转jmp需要用以下公式计算,所以在建立字符串的开始时候空出对应的位置。

jmp公式如下:

地址=跳转到的地址-代码当前地址-0x5

分配对应的空间:

LPVOID lCode = VirtualAllocEx(hProcess, NULL, 1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

对应的hook部分代码如下:

char data[]
{    0x81,0xFE,       //cmp esi
    0x00,0x00 ,0x00 ,0x00,    //人物指针
    0x75,0x02,     //jne xxx 
    0x31,0xFF,   //xor edi,edi
    0x8B,0xD0,   //mov edx,eax
    0x29,0xFA,   //sub edx,edi
    0x39,0xCA,   //cmp edx,ecx
    0xE9,       //jmp lCode+0X10
    0x00,0x00,0x00,0x00   //跳转回去的地址
};//后面需要计算对应的地址

填入对应的地址:

int jmpReturnAdd = 0x41FD67 - ((int)lCode+0x10) - 0x5;  //需要跳转回去的相对地址
/*写入到data中*/
int* nCode = (int *)(data + 0x11);
nCode[0] = jmpReturnAdd;
/*修改人物指针数据*/
int* userPointer = (int*)(data + 0x02);
userPointer[0] = 0x4CEF08;
WriteProcessMemory(hProcess, lCode, data, sizeof(data), NULL);

3.4.3 构建跳转代码

在本身的系统代码中,需要添加对应的跳转代码,同时由于该程序的空间只有执行权限,所以需要修改对应的权限。

/*修改源代码跳转位置*/
char E9Code[]{ 0xE9,0x00,0x00,0x00,0x00,0x90};//后面有一个nop
jmpReturnAdd = (int)lCode - 0x41FD61 - 0x5;
nCode = (int*)(E9Code + 0x01);
nCode[0] = jmpReturnAdd;
//可能代码属性不允许改写,所以需要改变内存属性
VirtualProtectEx(hProcess, (LPVOID)0x41FD61, 6, PAGE_EXECUTE_READWRITE,NULL);
WriteProcessMemory(hProcess,(LPVOID)0x41FD61, E9Code, sizeof(E9Code), NULL); 

需要注意的是,在加入jmp指令后会产生一个nop,所以需要在后面写入一个0x90

3.4.4 整体代码

#include <iostream>
#include <Windows.h>
using namespace std;
int main()
{
    DWORD Pid;
linput:
    cout << "请输入进程id:";
    cin >> Pid;
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, Pid);
    if (hProcess == NULL)
    {
        cout << "内存分配失败" << endl;
        goto linput;
    }
    else
    {
        LPVOID lCode = VirtualAllocEx(hProcess, NULL, 1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (lCode)
        {
            //内存分配成功
            char data[]
            {    0x81,0xFE,       //cmp esi
                0x00,0x00 ,0x00 ,0x00,    //人物指针
                0x75,0x02,     //jne xxx 
                0x31,0xFF,   //xor edi,edi
                0x8B,0xD0,   //mov edx,eax
                0x29,0xFA,   //sub edx,edi
                0x39,0xCA,   //cmp edx,ecx
                0xE9,       //jmp lCode+0X10
                0x00,0x00,0x00,0x00   //跳转回去的地址
            };//后面需要计算对应的地址
            int jmpReturnAdd = 0x41FD67 - ((int)lCode+0x10) - 0x5;  //需要跳转回去的相对地址
            /*写入到data中*/
            int* nCode = (int *)(data + 0x11);
            nCode[0] = jmpReturnAdd;
            /*修改人物指针数据*/
            int* userPointer = (int*)(data + 0x02);
            userPointer[0] = 0x4CEF08;
            WriteProcessMemory(hProcess, lCode, data, sizeof(data), NULL);
            /*修改源代码跳转位置*/
            char E9Code[]{ 0xE9,0x00,0x00,0x00,0x00,0x90};//后面有一个nop
            jmpReturnAdd = (int)lCode - 0x41FD61 - 0x5;
            nCode = (int*)(E9Code + 0x01);
            nCode[0] = jmpReturnAdd;
            //可能代码属性不允许改写,所以需要改变内存属性
            VirtualProtectEx(hProcess, (LPVOID)0x41FD61, 6, PAGE_EXECUTE_READWRITE,NULL);
            WriteProcessMemory(hProcess,(LPVOID)0x41FD61, E9Code, sizeof(E9Code), NULL);  
        }
        else
        {
            cout << "内存分配失败" << endl;
        }
    }
}


标签:暂无标签
版权属于:Jtripper 所有,转载请注明文章来源。

本文链接: https://www.jtripperbacaf.com/index.php/archives/58/

赞 (6)

评论区

发表评论

32+38=?

暂无评论,要不来一发?

回到顶部