三年三班三井寿 发表于 2019-3-7 16:45

连连看辅助的实现以及关键算法的逆向

论坛上已经有不少这个小游戏的分析了,我也试着用了差不多两个周日的时间来分析它(萌新,分析的很慢TAT),有了一些不同的思路。
先说一下大概的思路吧,辅助工具的编写其实很简单,甚至都不用去看反汇编,利用游戏里的道具(炸弹)去实现就可以了。通过CE找出道具栏的位置,炸弹的值,发送消息模拟点击炸弹就行。
实现后完成,又感觉太投机取巧了,就又自虐了一下,把它判断如何链接的算法给逆了出来。


游戏版本:百度qq连连看单机版第一个
               
             在分析游戏之前,我们先去一个广告。

            
            不对,应该是多个广告

            
            打开qqllk后直接会弹出第一个广告,然后第二个第三个,最后才会进入游戏。
            通过进程名的查看,真正游戏的进程名是kyodai.exe,我们直接去打开kyodai,会发现直接是打开不了的。
            观察后发现,游戏启动的过程中一共会有三个进程出现,一个就是一开始的qqllk.exe,然后会开启一个qqllk.ocx的进程,最后才会启动kyodai.exe
            也就是说kyodai.exe是qqllk.ocx这个进程启动的,而它肯定对游戏进程进行了解密,通过API断点,可以定位到解密的点
            
             仅仅是在0x43817A处修改了一个字节,qqllk.ocx在此之前已经创建了kyodai.exe进程,不过是以挂起的方式创建的,解密完成后再恢复运行。
             最后通过16进制编辑器手动修复一个字节就可以了,当然也可以脱壳后dump出来。去广告就到这结束了,具体细节也不细说了。
            

             接下来,进入主题
            
            从目标入手,想要实现这个小游戏的辅助。
            最直接的方法就是找到各个数据的基址(前提是它不会变)
            好吧,我们先找找看,假定它是不变的,至少这游戏没有随机基址
            
         Cheat Engine 启动
         首次搜索,再次搜索......
         等等,我们要找啥
          基址
      地图基址 和 道具基址
          假定空地就是0,其他有具体的值,然后搜几次,发现,没有符合要求的地址
          这里默认的四字节是搜索不到的,切换到单字节就能找到。
          几番确认后,我们能够确定地图数组的基地址为0x0012BB58,道具栏的基地址为0x0012AC5E
         
          有了基址了不起啊?
         
         你们知道我要说啥了吧
         
      下一步,我们需要找到炸弹
      
      Ollydbg启动,ctrl+G,0x0012AC5E

       观察道具基址的数值,我们猜测12AC5E处的值是道具(指南针)数量,下面6E处是第二个道具数量,7E,8E等还没有道具(初始会有3个指南针和3个重置道具)
      然后通过多次修改数据验证猜想,得出最后结论
      5E,6E,7E,8E,9E,AE,BE处为道具栏各个道具数量,
          每行末尾有一个标志位,通过F1,F2,F3...来标志下一格道具的种类,炸弹为F4(0表示下一个没有道具)
      还有一些其他的标志位,比如默认显示什么的,不过根我们这里没啥关系了
      

      我们已经找到了道具栏的位置,也找到了炸弹的标志
      接下来,就可以去写外挂了,实现起来也十分简单,将特定道具格子改成炸弹,然后模拟鼠标点击事件
      实现的方式也有很多种,我们在这里可以直接使用SetWindowLong来修改游戏本身的消息回调,来实现类似于HOOK的功能
      
      不过在测试的时候,发现即使已经修改了消息回调,也没法响应我们的键盘消息,可能是这游戏程序里本身就有钩子。
      不过好在,鼠标点击事件是能够获取到的,不然我们就得去找其他方法了。
      
         我们直接在道具栏指定位置点击,得到真实点击事件得wParam(1)与lParam(高位和地位对应着鼠标坐标)。
至此,基本就完成了,所有功能都围绕这个来实现就可以了。
修改道具栏道具后不会立马显示道具,需要鼠标移动到附近,所以也要模拟一个鼠标移动得消息
(将道具栏指定位置修改为炸弹(我是开启得时候直接将道具栏得道具顺序都给固定了),模拟鼠标点击炸弹)

        BYTE old_count;
        LPARAM lParam = (LPARAM)0x00c802d5;
        ReadProcessMemory(INVALID_HANDLE_VALUE, (LPVOID)0x12AC9E, &old_count, 1, &dwSize);
        WriteProcessMemory(INVALID_HANDLE_VALUE,(LPVOID)0x12AC9E, "\xFF", 1, &dwSize);
        ::SendMessage(g_hwnd,WM_LBUTTONDOWN, 1, (LPARAM)lParam);
        WriteProcessMemory(INVALID_HANDLE_VALUE, (LPVOID)0x12AC9E, &old_count, 1, &dwSize);
        ::SendMessage(g_hwnd, WM_MOUSEMOVE, 0, (LPARAM)lParam);

      
            










爽过之后,进入

自虐模式
       虽然这只是一个小游戏,并没有涉及很多数据结构和算法,但要去逆它得的关键算法对于我这种小白来说,还是很不友好的。
         含泪打开OD,
第一步,定位
         之前我们拿道具开刀,现在我们仍旧从道具下手,这次的主角是指南针,因为这个道具里面肯定会包含链接的算法,去判断两个点能否链接。
         有了前期的信息,定位其实很快。直接在指南针地址上下断,通过栈回溯去找调用的函数。
         


             待分析函数:
            该函数传入了两个参数,用于接受返回的两个点坐标
            该函数实现:   
            它的内部实际上是两层循环,逻辑其实很简单(但还是看了好久:'(weeqw),从地图数组的第一个点开始取,依次比较两个点类型(若是空地则跳过,若类型相同,则去比较判断能否链接)
         
            在下面一层的函数有判断类型的,检测类型的,还有判断两个点能否链接的,而最后一个其实就是整个游戏核心的算法,在这个函数内部仍有4个函数调用,不过后两个也都是基于前两个基础之上的

               判断能否链接的函数:
            首先它判断了两个点的y坐标是否相等,如果相等就进入到这个函数(其实其他情况也会进入这个函数,不过参数不一样),这种情况里面会判断两点中间有没有其他障碍物。
            


            接着返回上一层判断x坐标是否相等,然后进入另一个类似的函数。
            如果这两次判断都无效,则会进入到第三个算法,若第三个也无法判断正确,会进入到第四个算法
            其实后面两个算法也都是基于前两个上面的。
                        
             前两个一个是在横向的判断,一个是纵向

               
         整理后的源码大概如下:
BOOL IsXCheck(DWORD y, DWORD x1, DWORD x2, BOOL by1, BOOL by2)
{
        DWORD dwSmallx, dwBigx;
        dwSmallx = x2 < x1 ? (x2 + by2) : (x1 + by1);
        dwBigx = x2 < x1 ? (x1 - by1) : (x2 - by2);
        while (dwSmallx <= dwBigx)
        {
                if ((dwSmallx & 0x80000000) || dwSmallx >= 0x13 || y < 0 || y >= 0xB) return FALSE;
                BYTE dwAddr = *(PBYTE)((DWORD)dwAddrBase + y * 0x13 + dwSmallx);
                if (dwAddr > 0)return FALSE;
                ++dwSmallx;
        }
        return TRUE;
}
BOOL IsYCheck(DWORD x,DWORD y1,DWORD y2,BOOL b1,BOOL b2)
{
        DWORD dwSmally, dwBigy;
        dwSmally = y2 < y1 ? (y2 + b2) : (y1 + b1);
        dwBigy = y2 < y1 ? (y1 - b1) : (y2 - b2);
        if (dwSmally > dwBigy)return TRUE;
        DWORD temp = dwSmally;
        dwSmally *= 0x13;
        do
        {
                if ((x & 0x80000000) || x >= 0x13 ||
                        (dwSmally & 0x80000000) || dwSmally >= 0xD1)
                        return FALSE;
                BYTE dwAddr = *(PBYTE)((DWORD)dwAddrBase + dwSmally + x);
                if (dwAddr > 0)return FALSE;
                dwSmally += 0x13;        ++temp;
        } while (temp <= dwBigy);
        return TRUE;
}
后两个其实只是在调用前两个:
BOOL AlgorithmA(POINT a, POINT b)
{
        for (DWORD i = 0; i < 0xB; i++)
        {
                BOOL loc1 = (i == a.y) ? TRUE : FALSE;
                BOOL loc2 = (i == b.y) ? TRUE : FALSE;
                if (!IsXCheck(i, a.x, b.x, loc1, loc2)||
                        !IsYCheck(a.x, a.y, i, 1, loc1))
                        continue;
                if (IsYCheck(b.x, b.y, i, 1, loc2))
                        return TRUE;
        }
        return FALSE;
}
BOOL AlgorithmB(POINT a, POINT b)
{
        for (DWORD i = 0; i < 0x13; i++)
        {
                BOOL loc1 = (i == a.x) ? TRUE : FALSE;
                BOOL loc2 = (i == b.x) ? TRUE : FALSE;
                if (!IsYCheck(i, a.y, b.y, loc1, loc2) ||
                        !IsXCheck(a.y, a.x, i, 1, loc1))
                        continue;
                if (IsXCheck(b.y, b.x, i, 1, loc2))
                        return TRUE;
        }
        return FALSE;
}
四个算法都分析出来后,再往上一层判断能否链接
BOOL IsRemove(POINT a, POINT b)
{
        if (a.y == b.y&&IsXCheck(a.y, a.x, b.x, 1, 1)) return TRUE;
        if (a.x == b.x&&IsYCheck(a.x, a.y, b.y, 1, 1))return TRUE;
        if (AlgorithmA(a,b))return TRUE;
        if (AlgorithmB(a,b))return TRUE;
        return FALSE;
}
再往上就是那个循环遍历了
BOOL GetPoint(POINT&a, POINT&b)
{
        for (DWORD i = 1 ,j=1; i < 0xD1; i++)
        {
                j = i;
                obj.a1.x = (i - 1) % 0x13;
                obj.a1.y = (i - 1) / 0x13;
                do
                {
                        obj.b1.x = j % 0x13;
                        obj.b1.y = j / 0x13;
                        DWORD style1 = CheckStyle(obj.a1.x, obj.a1.y);
                        DWORD style2 = CheckStyle(obj.b1.x, obj.b1.y);
                        if (style1 == 0xff || style1==0|| style1==0x1)
                                break;
                        if (style2 == 0xff || style2 == 0 || style1 == 0x1)
                        {
                                j++;
                                continue;
                        }       
                        if (style1 == style2 && IsRemove(obj.a1, obj.b1))
                        {
                                a = obj.a1;
                                b = obj.b1;
                                return TRUE;
                        }
                        j++;
                } while (j<0xD1);
        }
        return FALSE;
}



主要还原的代码都在按照反汇编看的,应该还有很多地方可以优化。

最终测试结果,与游戏本身指南针找到的是同样的两个点。



涛之雨 发表于 2019-3-7 19:20

加精警告。。。
前排留名。
话说原来是这个原因一些小游戏打不开啊……
没分许过。。
话说既然知道原理了。回头试一下。。。估计原理差不多
大佬!收下一拜。先研究小游戏的去广告和启动还原。。。
算法回头再看。

zbr878458173 发表于 2019-4-9 12:27

JuncoJet 发表于 2019-3-8 10:52
大学的时候写过,三线程技能调用

你这个签名。。。。。。。。。。。是把所有的勋章都C下来了吗

viczc 发表于 2019-3-7 18:50

这就是大佬吗

涛之雨 发表于 2019-3-7 19:22

码了这么多字楼主辛苦了。
楼主年龄呀,表情包都放上来了。
{:301_1009:}
{:17_1067:}

龙飞飞龙 发表于 2019-3-7 19:25

真诚的感谢楼主的辛苦分享

三年三班三井寿 发表于 2019-3-7 19:35

涛之雨 发表于 2019-3-7 19:20
加精警告。。。
前排留名。
话说原来是这个原因一些小游戏打不开啊……


不是大佬,贼萌

三年三班三井寿 发表于 2019-3-7 19:37

viczc 发表于 2019-3-7 18:50
这就是大佬吗

你楼下那位是大佬,俺是萌新

学会爱自己 发表于 2019-3-7 19:58

学习一下

艾莉希雅 发表于 2019-3-7 20:00

自摩尔庄园后……又一款童年被……

遇日不归 发表于 2019-3-7 20:18

这个可以学习下,收藏了!!
页: [1] 2 3 4 5 6 7
查看完整版本: 连连看辅助的实现以及关键算法的逆向