首页
社区
课程
招聘
[原创]扫雷分析总结 多种方式定位雷区 通过替换过程函数或Inlien Hook的方法实现作弊
2021-10-24 21:43 11469

[原创]扫雷分析总结 多种方式定位雷区 通过替换过程函数或Inlien Hook的方法实现作弊

2021-10-24 21:43
11469

武汉科锐逆向学习笔记

扫雷分析笔记 总结 多种方式定位雷区 通过替换窗口过程函数或Inlien Hook的方法实现作弊 + 自写DLL注入器


笔记来自:科锐40班某一天的作业

作业要求:当鼠标移动到雷上的时候修改窗口标题为 扫雪


预备知识

在分析一款软件的时候 必然先了解一下这款软件的功能 想要分析这款软件就要先学会使用这款软件 要对软件有一个大概的了解

笔者默认读者都是熟悉扫雷的


从开发者的角度思考问题

老师之前讲过 做逆向要从开发者的角度来思考问题

首先我们先想一下以下几个问题

如果是我们自己开发的话我们应该用什么数据结构存储雷区的数据呢

 

如果是我开发的话 肯定会定义一个二维数组来做为雷区并且把高度 宽度 雷数 倒计时 放到一起来存储(可能会定义为结构体


通过相关数据来定位雷区

上面我们讲到 如果是我开发的话 肯定会定义一个二维数组来做为雷区并且把高度 宽度 雷数 倒计时 放到一起来存储(可能会定义为结构体)  那么我们可以尝试一下搜索内存法 先搜索到已知的内存所存放的地址 然后在附近通过肉眼来找雷区

打开CE 附件进程选择扫雷

我们通过雷区的高度来搜索 看图 先搜索16

然后修改一下雷区的高度 改为10

然后在搜索10 很快就赛选出来啦

我们把这个地址给它记录起来 01005338  010056A8


然后使用OD附件 进行查看 我们发现有一块数据很整齐 只有10  0F  8F  三个值

猜测 少的数值应该是雷区 多的应该是安全的 那么我们可以点击一下8F的地方 看看是不是这样的 已确认完毕 确实是这样子的(具体请看确认数据的方法)

通过Command消息定位雷区

当我们调整游戏难度的时候 游戏会重新绘制窗口 在重新绘制窗口的时候是不是会重新初始化雷区呢 这时候我们对Command消息下断点(不知道Command消息是干嘛的 请百度)

先点击Winddows窗口

右键在ClsProc上设置消息断点 选择Command消息 点击确定

这时我们调整一下游戏的难度 这时OD 的断点会命中

接着我们想一下游戏初始化的时候 会不会使用循环 由于游戏是二维数组 它肯定会用到二层循环

 

F8往下跟 找两层的循环 一直F8到这个地方 我们发现前面并没有循环且这是我们遇到的第一个Call 也是最后一个Call 因为jmp 01001F4A 明显是个Break  这个Call在不跟进去就Break到Switch结尾啦 所以我们F7跟进去

由于我们要找循环 那么我们就关注箭头向上指的跳转(循环只会向上跳)


F8向下走会发现两个向上跳的跳转且位置相同 明显是嵌套的关系 说明这就是我们要找的循环 接着我们继续分析一下 既然是初始化那么是不是要对二维数组进行赋值呢

那么我们找对内存地址进行赋值的指令 右键 数据窗口跟随 选择立即数

发现窗口整整齐齐的 这就是雷区

通过随机数定位雷区

根据游戏经验我们知道 雷区是随机的生成的 应该会用到随机数的函数吧 那么通过随机数的函数 可不可以定位到雷区呢


点击Executable Modules 模块窗口 点击主程序 win.exe 也就算扫雷.exe

然后右键 查找 当前模块名称 或者按快捷键 CTRL+N键 上下看一看 有没有随机数相关的

我们发现有一个C库函数Rand 那么我对Rand 右键 在每个参考上设置断点

然后点击扫雷的笑脸或者更改游戏难度 让游戏进行初始化 接着断点已命中

关键代码应该就在Rand的返回地址处 我们F8跟出函数  发现直接中奖

这个还是跟上面Command消息的位置是一样的 右键 数据窗口跟随 选择立即数即可

这就是雷区


通过鼠标消息来定位雷区

当我们鼠标点击雷区的时候 它会对雷区进行访问来判断是不是雷 先打开Windows窗口 右键点击刷新 然后对扫雷主窗口 右键在ClassProc上设置消息断点

找到 鼠标左键点击的消息 设置断点

点击雷区 这时会断下来

然后我们看堆栈 000DFCEC的地址是我们的鼠标的X Y坐标 它肯定是拿我们的X Y坐标进行进行寻址的 所以我们下硬件访问断点

右键下一个硬件访问断点 F9运行 

然后断点来到这里 并把我们的坐标作为参数传进去了 所以这个Call 我们要进去看看

然后发现这里并没有做什么 只是判断了我们的坐标是否在矩形范围内然后就返回了 我们继续F9运行

然后断点来到这里 没有做什么事情 我们继续F9运行

然后断点来到这里 发现它拿我们的鼠标坐标 做了运算 并当作参数传递进去 猜想它应该是把鼠标的XY坐标转换为数组的行和列 这个Call是个关键Call 跟进去观察参数

进入Call之后发现参数1也就是ebp+8 赋值给edx 接下来也关注一下edx

继续向下 发现有一个地址把edx当作偏移来寻址 应该就算雷区了吧 然后我们到内存窗口看

这里就是雷区


通过双缓冲绘图来定位雷区

我们看一下扫雷在绘制的时候 是不是不一样 比如有的绘制数字 有的绘制旗子

那么它是通过什么来区分的呢 是不是要来读取雷区来区分要绘制什么呢

我们知道WIN32程序绝大数会用到双缓冲绘图


那么这时候我们就要对双缓冲绘图的相关API进行了解啦

http://tongxinmao.com/Article/Detail/id/341

 

我们知道 双缓冲绘图必然要将内存的DC复制到设备DC上 所以会使用BitBlt这个API 那么我们对BitBlt这个API进行下断点

点击模块窗口 双击主模块扫雷.exe

然后按CTRL + N 查找当前模块中的名称(标签)

找到BitBlt 右键在每个参考上设置断点


点击扫雷的雷区断点会直接断到这里 然后我们看一下参数 第五个是指向源设备环境的句柄 也就是它要绘制的图片是那个就要由这个参数来决定 这个参数是多少是由雷区的值来决定的 所以我们观察这个参数即可得到雷区的地址

这就是雷区

通过全局数据区定位雷区

我们知道扫雷是用二维数组来保存雷区的 那么决大数都会是存在全局数据区

我们让扫雷进行初始化 在数据区用肉眼观察有规律的数据

如何确定是否是雷区

当我们点击雷区的时候 它会对雷区进行访问 那么我们在疑似雷区的区域下一个内存访问断点 肯定会来的 如果不来就不是雷区


分析是否是雷

结合游戏进行分析 8F比0F少 应该是雷 0F比较多应该不是雷

这时我们可以把0F的地址改成8F 然后点击一下看看 是不是雷就可以确定了

我们找到雷区之后还有一个问题 怎么通过鼠标的坐标得到二维数组的行和列

扫雷是有一个简单的算法 接下来我们来定位算法


通过LBUTTONDOWN定位算法

我们鼠标点击的时候 肯定会通过我们的鼠标坐标来转换为二维数组的行和列

 

我们在Winddows窗口 对LButtonDown消息下断点 然后点击格子 会断到这里

因为转换的时候肯定会对我们的鼠标坐标进行访问 我们转到00DFCEC的地方

右键下一个硬件访问断点

F9运行 我们找赋值的地方 发现来到这里 我们通过上面分析得知这个Call

然后继续F9继续来到这里 发现这里对Eax进行赋值 进行分析

扫雷格子的大小是16 通过截图工具测试出来的

第一个X坐标格子的最小值是12   

12+4得到16 / 16   得到1    

 

第一个X坐标格子的最大值是24

27+4得到31/ 16   得到1    

 

第一个Y坐标最小值是55           

55-0x27得到16/16    得到1


第一个Y坐标最小值是70           

70-0x27得到31/16    得到1


得到数组的行和列之后 我们对雷区进行下一个内存访问断点 看看是不是这样

我们对雷区进行下内存访问断点 然后F9运行 断到这里 我们发现EDX是值是正常的 EDI左移5位 也就是乘32

接着我们要考虑怎么完成功能

当鼠标移动到雷上的时候修改窗口标题为 扫雪


我们需要通过替换窗口过程函数来接收扫雷的鼠标消息 然后在鼠标消息里面判断是不是雷 或者 使用Inline Hook消息处理函数Hook鼠标消息


替换窗口过程函数(全部代码)

HWND My_HWnd = 0;
WNDPROC My_Proc = 0;
char szBuff[100] = "";
LRESULT CALLBACK WindowProc(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)
{
  //鼠标移动
  if (Msg == WM_MOUSEMOVE)
    {
        int x,y;
        x = LOWORD(lParam); //客户区的Y坐标
        y = HIWORD(lParam); //客户区的Y坐标

        //0x1005340是数组首地址 通过算法得到鼠标在的雷区值
        if (*(PBYTE)((DWORD)0x1005340 + ((x + 4) >> 4) + ((y - 0x27) >> 4) * 32) == 0x8F)
        {
            //如果是雷就修改窗口标题为扫雪
            SetWindowText(hWnd,"扫雪");
        }
        else
        {
            //如果不是雷就修改窗口标题为扫雷
            SetWindowText(hWnd,"扫雷");
        }
    }
    return CallWindowProc(My_Proc, hWnd, Msg, wParam, lParam);
}

BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        //查找窗口
        My_HWnd = ::FindWindow("扫雷","扫雷");

        //替换窗口过程函数
        My_Proc = (WNDPROC)SetWindowLong(My_HWnd, GWL_WNDPROC,(LONG)WindowProc);
        break;
    }
    }
    return TRUE;
}

Inline Hook(全部代码)

int sign = 0;
HWND hWnd = ::FindWindow("扫雷", NULL);

void __declspec(naked)HOOKCODE()
{
	__asm
	{
	    pushad;
	    pushfd;

	    //判断是否为0x200消息(移动消息)
	    cmp dword ptr[esp + 0x8 + 0x24], 0x200;
	    jnz IF_END1

		//鼠标坐标转换到数组的行和列
		mov eax, [esp + 0x10 + 0x24];
		shr eax, 0x10;
		sub eax, 0x27;
		sar eax, 4;
		mov edx, eax;
		shl edx, 5;

		movzx eax, [esp + 0x10 + 0x24];
		add eax, 4;
		sar eax, 4;
		mov ecx, eax;

		//取值
		mov eax,0
		mov al, byte ptr[edx + ecx + 0x1005340];
		mov sign, eax;
	}

	if (sign == 0x8F)
	{
	    SetWindowText(hWnd, "扫雪");
	}
	else
	{
	    SetWindowText(hWnd, "扫雷");
	}

	__asm
	{
	    IF_END1:

	    popfd;
	    popad;

	    //跳回原来的位置
	    mov eax, 0x01001BC9;
	    add eax, 5;

	    mov edi, edi;
	    push ebp;
	    mov ebp, esp;
	    sub esp,40
	    jmp eax;
	}
}

void Hook()
{
	//修改内存属性
	DWORD dwOldProtect = 0;
	VirtualProtect((LPVOID)0x01001BC9, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect);

	//修改跳转
	char* bClassProc = (char*)0x01001BC9;
	bClassProc[0] = 0xE9;

	//计算偏移
	int offset = 0x01001BC9;
	offset = (int)HOOKCODE - (offset + 5);

	//修复跳转地址
	*(int*)&bClassProc[1] = offset;

	//多出来一个字节填NOP
	bClassProc[5] = 0x90;

	//恢复内存属性
	VirtualProtect((LPVOID)0x01001BC9, 1, dwOldProtect, &dwOldProtect);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
		Hook();
		break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}


注入器代码(全部代码 不包含MFC框架代码)

//注入代码
void CMFCApplication1Dlg::OnBnClickedButton1()
{
	//获取窗口句柄
	HWND hWnd = ::FindWindow("扫雷", NULL);

	DWORD dwPId = 0;
	//获得窗口所属进程ID和线程ID
	GetWindowThreadProcessId(hWnd, &dwPId);

	//用来打开一个已存在的进程对象,并返回进程的句柄
	HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPId);

	//给要创建线程的进程申请内存
	CString szPath;
	MyEdit.GetWindowTextA(szPath);
	LPVOID lpBuff = ::VirtualAllocEx(hProcess,NULL,0x1000,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);

	//写入内存
	DWORD dwBytes = 0;
	::WriteProcessMemory(hProcess,lpBuff,szPath.GetBuffer(),szPath.GetLength() + 1,&dwBytes);

	//创建远程线程
	HANDLE hThread = CreateRemoteThread(hProcess, 
		                            NULL, 
		                            0,(LPTHREAD_START_ROUTINE)LoadLibraryA,
		                            (LPVOID)lpBuff,0,NULL);

	//等待线程结束
	WaitForSingleObject(hThread,INFINITE);
	return;
}

//拖拽代码
void CMFCApplication1Dlg::OnDropFiles(HDROP hDropInfo)
{
	CString strFilePath;
	DragQueryFile(hDropInfo,0,strFilePath.GetBuffer(MAX_PATH),MAX_PATH);
	MyEdit.SetWindowTextA(strFilePath);
	CDialogEx::OnDropFiles(hDropInfo);
}


注入器

笔者是一个刚刚学习汇编不久的小白 如果本文中哪里有错误 请各位前辈指导 

源码和已经编译好的软件都已经放到附件里面了如果有需要可以下载


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-10-24 22:25 被旺仔_小可爱编辑 ,原因:
上传的附件:
收藏
点赞6
打赏
分享
最新回复 (6)
雪    币: 2863
活跃值: (1602)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chixiaojie 2021-10-24 21:47
2
0
为什么人世间尽然会有如此美妙精华的好文章。
雪    币: 500
活跃值: (865)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
sky灬饮冰 2021-10-24 22:21
3
1
666,跟着大佬学会了
雪    币: 182
活跃值: (576)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
柒雪天尚 2021-10-24 22:37
4
0
为什么人世间尽然会有如此美妙精华的好文章
雪    币: 2863
活跃值: (1602)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chixiaojie 2021-10-25 15:47
5
0
为什么人世间尽然会有如此美妙精华绝伦的好文章。
雪    币: 2
活跃值: (925)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Hell02W0rld 2021-11-22 21:31
6
0
为什么人世间尽然会有如此美妙精华的好文章
游客
登录 | 注册 方可回帖
返回