专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
不跪的世界  ·  市场有了新的期待:周六的简报会 ·  4 天前  
不跪的世界  ·  市场有了新的期待:周六的简报会 ·  4 天前  
嘶吼专业版  ·  卡巴斯基在没有任何提示的情况下安装 ... ·  5 天前  
FreeBuf  ·  新型僵尸网络针对 100 个国家发起 30 ... ·  6 天前  
计算机与网络安全  ·  网络空间安全仿真术语 ·  6 天前  
51好读  ›  专栏  ›  看雪学苑

新手逆向学习--Win7 下 64 位扫雷逆向以及辅助制作

看雪学苑  · 公众号  · 互联网安全  · 2017-05-08 18:05

正文

之前逆过 XP 下的扫雷程序,感觉 XP 下的扫雷很简单,但是发现网上对于Win7下的扫雷逆向很少很少,于是就试着继续逆一下 Win7 下的扫雷。这一逆发现难度提升了不只一个等级啊,经过两天的努力,终于整个逆完了它的扫雷算法。

 首先在 Win7 下的扫雷不再是像 XP 一样在一开始就布置好雷区,这样我们就可以在一开始就读取雷区内存,比较坑的是 win7 下的扫雷是在你点击第一块儿方块时才开始布置雷区。这样我首先在 rand 函数下断点,发现有好多地方会调用 rand 函数,我把每个调用 rand 函数的地方下了断点,然后把一直在调用的 rand 函数的那几个函数断点给去掉,这样我们就找到了程序的突破口。

 不断退出当前调用,并在上层函数的 call 调用处下断点,直到找到了一个疑是算法入口的函数。

跟进函数,又发现一个 call,继续跟进

 我发现这个函数便是调用 rand 函数的地方,估计核心就在此了,开干。

我们要对每个 call 都倍加小心,需要都看一下,我们发现这里好像是一个申请数组空间并填充的操作。

经过多次循环后,发现数组填充完毕,之后观察一下申请的数组空间中存储的东西。

发现没有了 00 01 09 0A 四个值。因为我是点击的第一个方块,我们可以重新调试,点击其他方块试一下,发现这个数组会将点击方块周围的的 9 个值去掉(包括点击方块自己),这样我们就理解了,程序不会在第一次点击方块周围产生雷。

 这时候我们估计就对这个程序有了一点点理解了,在点击第一块儿方块的时候,程序开始申请内存。这里它会有一个结构体存储了随机雷数组已用大小和总空间,然后生成一个数组,并将各个雷进行编号存入数组中。之后rand函数产生的随机雷就在这些数组中产生。接下来验证我们的想法:

 跟进下一个 call,发现这里申请数组空间,并存储随机出来的雷值


 继续单步,发现有个小循环比较有意思

看一下 rax 存储了什么?

这好像是存储了多个数组的首地址啊,正好我们现在设定了 9x9 的雷区,这里正好 9 个地址,我们再跟进去这些地址看一下

这里 +10 处又存储了一个地址,继续观察,发现有个 byte 数组,存储了雷的状态,有雷就是 1,无雷就是 0。

这个时候我们就基本搞明白了这个 Win7 下扫雷是怎么布置的了。可是问题来了,最初的记录雷区各个数组的地址从哪得啊?我们逆着代码去溯源。我们发现这个值是rax+0x10 处的存储的值,而 rax 是 rsi+0x58 处存储的值,这个 rsi 是 rcx 作为上层调用函数传过来的参,我们走出这个函数看看这个参数从哪里得到。

我们找到了这样一个值,在 FFCFAA38 中存储了我们所想要的 rsi 的值。我们知道,这是一个全局变量,存储了 rsi 地址。但是这个值由于 RSLR 机制而导致每次地址不一样。我们有一种方法得到这个值,我们先看当前模块加载基地址,然后用FFCFAA38(全局变量地址)-FFC500000(当前模块加载地址)= AAA38(相对当前模块偏移)。这样我们可以用 GetModuleHandle 函数得到当前模块加载基地址,然后加上这个偏移 AAA38 就得到了全局变量地址。

这样我们就有了得到数组地址的方法:

Address = [[[[hModule+0xAAA38]+0x18]+0x58]+0x10]

这时 Address 就是存储雷区数组的首地址,每个雷区地址 +0x10 处就是雷区列状态数组(byte)地址。

其实,到这里我们也开始明白了,它所使用的应该是 C++ 的 vector,一个个 push 才产生这样的内存空间的,不得不说,这 C++ 功力已经炉火存青了,各种数据结构弄得头都大了。

找到了雷区布置数组就可以进行下一步动作了,我们通过计算鼠标坐标值来获得雷区格子,每个格子是 17*17 像素大小并加上1像素的边,所以每个格子大小为18像素,雷区边界为 30 像素。你问我这些怎么得到的?这些值肯定在某个内存存着,你可以下断点在 GetCursorPos 处,在你移动鼠标时会触发断点,然后跳出函数,发现下边有一个 GetWindowRect 函数,这个函数会传递窗口句柄,窗口句柄存储在一个全局内存中,我们可以得到这个窗口句柄。但是我用了更简单的方法,既然有窗口,我直接用工具测一下就知道每个格子大小了么。

这样我们就得到了鼠标坐标转换格子的公式:

int x = (xPos - 30) / 18;          //

  int y = (yPos - 30) / 18;          //行

最后,上辅助代码:

WNDPROC g_oldProc = NULL;

DWORD   dwArrOffset = 0xAAA38;   //这是重定向之前全局变量相对于模块基地址的位置

DWORD64 dwMineAddress = 0;       //雷区列数组位置

BYTE    *pMineMap;               //自定义一个数组存储雷的布局,便于访问

DWORD   rows = 0;                //行

DWORD   cols = 0;                //列

  

LRESULT CALLBACK WindowProc(

    _In_  HWND hwnd,

    _In_  UINT uMsg,

    _In_  WPARAM wParam,

    _In_  LPARAM lParam

)

{

  

    if (uMsg == WM_MOUSEMOVE)

    {

        int xPos = GET_X_LPARAM(lParam);

        int yPos = GET_Y_LPARAM(lParam);

  

        int x = (xPos - 30) / 18;          //列

        int y = (yPos - 30) / 18;          //行

         

        int nWidth = 30 + 18 * cols;

        int nHight = 30 + 18 * rows;

        if (x>=0&&y>=0&&x

        {

            if (pMineMap[x*rows + y] == (BYTE)0x01)

            {

                SetWindowText(hwnd, L"!!!有雷!!!");

            }

            else

            {

                SetWindowText(hwnd, L"扫雷");

            }

        }

        else

        {

            SetWindowText(hwnd, L"扫雷");

        }

         

    }

    //F12键一键扫雷

    if (wParam == VK_F12)

    {

        int i = 0;

        do

        {

            for (int j = 0; j 

            {

                if (pMineMap[i*rows + j] != (BYTE)0x01)

                {

  

                    int x = (i * 18) + 30 + 9;//定位到格子中心

                    int y = (j * 18) + 30 + 9;

                     

                    LPARAM point = MAKELPARAM(x, y);

                    //发送鼠标点击消息

                    PostMessage(hwnd, WM_LBUTTONDOWN, NULL, point);

                    PostMessage(hwnd, WM_LBUTTONUP, NULL, point);

                }

            }

            i++;

        } while (i!=cols);

    }

    return CallWindowProc(g_oldProc, hwnd, uMsg, wParam, lParam);

}

// CMineSweeperWaiguaApp 初始化

BOOL CMineSweeperWaiguaApp::InitInstance()

{

    CWinApp::InitInstance();

    OutputDebugString(L"已加载!\n");

    //获得雷区数组地址

    HMODULE hModule = GetModuleHandle(NULL);

    // 雷区列数组获得公式 [[[[hModule+0xAAA38]+0x18]+0x58]+0x10]

    DWORD64 v1 = *(DWORD64*)((DWORD64)hModule + dwArrOffset);

    //v2+0x8处存储了雷总数(DWORD),v2+0x0C处存储了行总数(DWORD),v2+0x10处存储了列总数(DWORD)

    DWORD64 v2 = *(DWORD64*)(v1 + 0x18);

    //申请一个存储雷区的数组空间

    rows = *(DWORD*)(v2+0x0C);     //行

    cols = *(DWORD*)(v2+0x10);     //列

    pMineMap = (BYTE*)VirtualAlloc(NULL,rows*cols, MEM_COMMIT, PAGE_READWRITE);

    if (pMineMap ==NULL)

    {

        OutputDebugString(L"申请内存失败!\n");

    }

    OutputDebugString(L"申请内存成功!\n");

    //v1处存储了列总数(DWORD)

    v1 = *(DWORD64*)(v2 + 0x58);

    dwMineAddress = *(DWORD64*)(v1 + 0x10);//存储雷区列数组地址,有多少列就有多少数组

  

    //数组地址首位显示的是行数

    for (int i=0;i

    {

        //v1处前4字节(DWORD)储了行总数  ,后边两个内容没搞懂(10 10)

        v1 = *(DWORD64*)(dwMineAddress + i * 8);

        BYTE *v5 = (BYTE*)(*(DWORD64*)(v1 + 0x10));

        for (int j = 0; j 

        {

            //将雷区状态赋值到数组中去 

            pMineMap[i*rows +j] = *v5;  //第i列第j行       

            v5++;

        }

    }

    OutputDebugString(L"雷区赋值成功!\n");

  

    HWND hWnd = FindWindow(NULL, L"扫雷");

    if (hWnd == NULL)

    {

        OutputDebugString(L"未找到目标窗口!\n");

        return FALSE;

    }

    //更改指定窗口的属性

    //返回值是之前的窗口函数

    g_oldProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)WindowProc);

  

    return TRUE;

}


到此,我们大功告成,需要注意的是每次要点击一下一个格子再注入动态库。不过,我的 F12 一键扫雷并没成功有人知道是怎么回事么?希望不吝赐教帮我解决一下,嘻嘻~~


作者:ixiaohuo ,原文发表于看雪论坛 —【软件逆向】版块



往期热门内容推荐



更多优秀文章,长按下方二维码,“关注看雪学院公众号”查看!

看雪论坛:http://bbs.pediy.com/

微信公众号 ID:ikanxue

微博:看雪安全

投稿、合作:www.kanxue.com