• 【屌丝的逆袭系列】没有源代码也能加功能也能玩美化


    【使用工具】 Peid 0.94,OllyDbg(OllyIce),exeScope,Spy4Win   
    【运行平台】 WinXP 
    【软件名称】 IMMSG飞鸽传书 (下载)     
    【软件简介】 一个用于内网交流和传输文件的小工具   

      前些日子到同事的同学哪里去拷《康熙大帝》。可惜网连好后,就是访问不了。看来又是什么安全策略的问题了,本想重设一下。这时同事的同学就讲可以使用 飞鸽传书。

      这个软件用起来后感觉还不错,很方便,不过也有些不足。界面如下

      

      看上图可知。首先是窗体太窄,尤其是 显示用户名和主机名的那个LISTVIEW。看起来很不爽。第二个问题是ICON(图标)太难看。第三个呢在图上看不出来,就是双击运行后,界面并不先出来,而只是在拖盘上显示个图标,双击后,主界面才能出来,非常不方便。于是就想改一改。

      下面我们就一起来在没有源码的情况下把这些问题给改一改。

      1. 改变窗体的大小

      在未进行更改前,以为改变窗体大小应该是一件很容易的事情,不就是断一下CreateWindowExA,然后看传递的参数(参数里包含宽度和高度的值),之后顺藤摸瓜找到存放宽度和高度值的内存地址,更改成我想要的值不就行了嘛。如果幸运的话宽度和高度都是以立即数的形式给PUSH的话那就更美好了。当然事实往往和想象中的总是有差距的。动手之后才知道自己想的是那么简单,当然这也和自己的知识不牢是分不开的。不过,这也不是什么坏事,虽然分析了很多无关紧要的东西,但也通过这些分析学到了不少东西。也不能完全算坏事。好了。废话不多讲了。下面开始分析过程吧

      第一步当然是查壳,本来打算如果是猛壳(例如TMD之类)的话就直接放弃,不过一查之下,结果甚是让人高兴,不仅不是猛壳,甚至连壳都没加。后来才得知其实这是一款国外人写的免费软件。国外人的免费软件不加壳是很正常的,毕竟人家不象咱国人。把人家的源码或者软件拿来随便加点东西,然后就开源的闭源,没加壳的加壳,最后再把软件名字和作者一改,这就堂而惶之的宣称是××的作品了。用老罗的话说:就是真TM没见过这么流氓的。PEID切图如下。

     

      从图中可以看出这个软件是用Microsoft Visual C++ 6.0 写的。

      我用的这个算是比较新的userdb(存放各种开发工具生成的EXE文件的特征码的文本文件,PEID就是用他来识别开发工具的),而且这个软件的这个版本年代也比较久远了,理论上应该识别的准确率是很高的,当然我们还是不能完全相信PEID的,因为很多加壳的软件,都可以将其加壳后的EXE伪装成VC6编写的。所以还是用OD(OLLYDBG)载进来看看先。载入OD代码窗口显示如下:

    004183D7 >/$  55            push    ebp
    004183D8  |.  8BEC          mov     ebp, esp
    004183DA  |.  6A FF         push    -1
    004183DC  |.  68 18CB4100   push    0041CB18
    004183E1  |.  68 2C9D4100   push    00419D2C                         ;  SE 处理程序安装
    004183E6  |.  64:A1 0000000>mov     eax, dword ptr fs:[0]
    004183EC  |.  50            push    eax
    004183ED  |.  64:8925 00000>mov     dword ptr fs:[0], esp
    004183F4  |.  83EC 58       sub     esp, 58
    004183F7  |.  53            push    ebx
    004183F8  |.  56            push    esi
    004183F9  |.  57            push    edi
    004183FA  |.  8965 E8       mov     dword ptr [ebp-18], esp
    004183FD  |.  FF15 74B14100 call    dword ptr [<&KERNEL32.GetVersion>;  kernel32.GetVersion
    00418403  |.  33D2          xor     edx, edx
    00418405  |.  8AD4          mov     dl, ah
    00418407  |.  8915 DC224200 mov     dword ptr [4222DC], edx
    0041840D  |.  8BC8          mov     ecx, eax
    0041840F  |.  81E1 FF000000 and     ecx, 0FF
    00418415  |.  890D D8224200 mov     dword ptr [4222D8], ecx
    0041841B  |.  C1E1 08       shl     ecx, 8
    0041841E  |.  03CA          add     ecx, edx 

      看到注释里的SE 处理和那个GetVersion没有,有了这两个家伙基本可以确定PEID的识别是准确的了。当然光确定没加壳没用,我们还得继续前进,在OD的代码区右键,查找->所有模块中的名称。在弹出的窗口上随便选择一行后,输入 CreateWindowExA.会出现很多个,选择模块是USER32的那个,点右键->切换断点。当然可能有些人会问,为什么要断在USER32模块里,为什么不直接查找->本模块中的名称,这样不是能直接断在本程序里,而不用再从USER32模块里跟回本程序的领空了吗。当然这个问题对于大部分人来说,都是异常简单的,但对于新手还是有必要解释一些的。OD的反汇编能力虽然强,但对于有些动态调用(使用LoadLibary GetProcAddress)就不一定能分析出来他的API名称了,所以查找->本模块中的名称的时候,可能就会遗漏掉这些地方的调用。所以还是在断在USER32里比较安全,无论静态或动态调用,总得从我这里走,而且从API返回到程序领空也并不是一件困难的事情。懂点汇编的人就知道只要看栈顶就可以了(CALL一个函数的时候,在进行函数体之前,总要将代码的下一个地址压栈的,所以在函数的第一行的时候,栈顶就一定是调用函数下一个地址,当然这是在保护模式下,在实模式下,在栈顶和栈顶的下一位,分别存CS和偏移)。最重要的是OD里会直接提示你会返回到哪里,知道了返回的地址,那么他的上一行,一定就是调用的地方了,当然多重调用的话,可能第一次返回的并不你想要到的地方,不过不要紧,耐心慢慢一步步跟就是了。总能找到你想要的位置的:)。

      设断后F9,断在这里,代码区代码如下:

    77D2190B >  8BFF            mov     edi, edi
    77D2190D    55              push    ebp
    77D2190E    8BEC            mov     ebp, esp
    77D21910    68 01000040     push    40000001
    77D21915    FF75 34         push    dword ptr [ebp+34]
    77D21918    FF75 30         push    dword ptr [ebp+30]
    77D2191B    FF75 2C         push    dword ptr [ebp+2C]
    77D2191E    FF75 28         push    dword ptr [ebp+28]
    77D21921    FF75 24         push    dword ptr [ebp+24]
    77D21924    FF75 20         push    dword ptr [ebp+20]
    77D21927    FF75 1C         push    dword ptr [ebp+1C]
    77D2192A    FF75 18         push    dword ptr [ebp+18]
    77D2192D    FF75 14         push    dword ptr [ebp+14]
    77D21930    FF75 10         push    dword ptr [ebp+10]
    77D21933    FF75 0C         push    dword ptr [ebp+C]
    77D21936    FF75 08         push    dword ptr [ebp+8]
    77D21939    E8 B5FEFFFF     call    77D217F3
    77D2193E    5D              pop     ebp
    77D2193F    C2 3000         retn    30
    77D21942    FFB5 F0FBFFFF   push    dword ptr [ebp-410]
    77D21948    FF15 A810D177   call    dword ptr [<&ntdll.RtlReleaseAct>; ntdll.RtlReleaseActivationContext

      这个就是USER32里的CreateWindowExA 反汇编后的样子,其中上面代码的第一句就是,CreateWindowExA 的第一条语句了,当然上面的信息对我们来说是没有用的。不用管那么多,关键是看堆栈 。

    0012FCB0   00416876  /CALL 到 CreateWindowExA 来自 复件_IPM.00416870
    0012FCB4   00000000  |ExtStyle = 0
    0012FCB8   0012FDA0  |Class = "ipmsg_class"
    0012FCBC   004207CC  |WindowName = "IPMsg"
    0012FCC0   20CF0000  |Style = WS_OVERLAPPED|WS_MINIMIZEBOX|WS_MAXIMIZEBOX|WS_MINIMIZE|WS_SYSMENU|WS_THICKFRAME|WS_CAPTION
    0012FCC4   80000000  |X = 80000000 (-2147483648.)
    0012FCC8   80000000  |Y = 80000000 (-2147483648.)
    0012FCCC   80000000  |Width = 80000000 (-2147483648.)
    0012FCD0   80000000  |Height = 80000000 (-2147483648.)
    0012FCD4   00000000  |hParent = NULL
    0012FCD8   00000000  |hMenu = NULL
    0012FCDC   00400000  |hInst = 00400000
    0012FCE0   00000000  /lParam = NULL

      OD给出的解释已经很详细了,重要的是Class="ipmsg_class"(类名)和WindowName="ipmsg"(窗体名)。另外因为只要程序每次使用CreateWindowEx创建窗体的时候,这里都会被断下来,所以这个地方是会被多次断下来的,所以每次被断下来的时候,都要记录下Class和WindowsName,一会儿再拿来和用工具查出来的主窗体的类名和窗体名进行比较,如果类名和窗体名和主窗体的类名和窗体名能对应上的话,就可以确定当时被断下来的那个就是主窗体了。一旦确实了主窗体被断的位置,如此只要再运行一次程序,从那个地方开始返回到程序的领空,这样就可以找到创建主窗体的CreateWindowExA的地方了,然后在他PUSH 宽度和高度的地方,设断,删除其它断点,这样就可以断在我们想要的位置上,再顺藤摸瓜就可以找到它的宽度和高度的地址了。当然你也完全可以先运行程序,然后用工具查出来主窗体信息后,在CreateWindowExA上设条件断点,可能会更加的方便。不过在这里我们已经这样了,就继续顺着这个思路继续往下走吧。OKAY,记录了类和窗体名后,在目前看来已经够用了,就不用管其它了,继续F9。发现此时拖盘上的图标已经出来了。而且OD也显示程序已经处于运行状态。看来只截到这一个调用了,那么这个窗体是主窗体的可能性应该是比较大了。于是双击拖盘图标,打开主窗体,发现没有被截下来,想应该主窗体的确是已经被创建了,这里只是ShowWindow或者SetWindowPos一下而已.所以才不会被CreateWindowExA断点给截下来。当然还是刚才那句,想的和现实总是有段距离的。打开妖哥的SPY4WIN.查看主窗体信息

      头大的了,不是吧.和刚才截到的窗口信息简直风马牛 不相及嘛。如果只是标题名不一样,也许还能给点安慰,因为可以使用SetWindowText 重设下标题,而且也经常有人这样干,但如果窗口类名不一样,那就要另当别论了,虽说也可以使用GetClassInfo.重设类名。但一般情况下正常人类不会那样做。难道我们辛辛苦苦断下来的唯一一个窗口还是没用的???头都晕了,当然俺可也不是那种那么容易放弃的人,所以接着在刚才断下来的那个窗口上又下了一番工夫,包括跟踪窗口事件,因为我发现使用SPY4WIN将那个窗体(类名为img_class的那个)显示后,双击就会出现主窗体。于是跟了事件,发现在打开主窗体之前,他发了一个WM_ACTIVEAPP**.事件。于是用OD在那个窗口上下了事件断点,跟踪了半天,没发现什么东西,只好放弃了。看来还是得从头理思路了,既然不是在启动的时候创建的窗体。那么会不会是在双击拖盘图标的时候创建的呢,不过刚才在双击后,也的确没断下来,于是又思考是不是使用了CreateWindowExW.于是在它上面设断,的确断了几个下来,却也都是和主窗体风马牛不相及的。

      看来还得再理一次,而且得换个思路,刚才忽略了一个很大的问题,我们一直假设他是在启动的时候就创建了窗体,但这个却是不一定的,因为他是在双击托盘里的图标后,才显示的窗体,那么会不会是双击托盘图标后,才创建的窗体呢,这个得先确定一下。这个不难确认,先启动,使用刚才记录下来的主窗体信息在SPY4WIN中查找,没发现窗体,双击拖盘图标,查找,找到了。看来的确是在双击后才创建的,但为什么双击后,没有截住他呢,难到不是使用CreateWindowExA创建窗体,而是使用的是DIALOG相关函数。一想到DIALOG,忽然想到了主窗体的类名,#32770,好象记的在那里看到过介绍,是特殊窗口的类,于是百度了一下,果然。这是DIALOG(对话框)的类名,看来应该使用DIALOG相关函数了,不过此时又犯嘀咕了。因为貌似在什么地方又见到过。讲DIALOG相关函数最会还是会调用CreateWindowExA( W ).这样看来,CreateWindowExA断不下来。DIALOG相关函数应该也没戏了,所以当时并没有报多大希望。但事实又一次证明我错了。DIALOG相关函数当然首选CreateDialogParamA(W)了,于是在它上面设断,居然给断下来了。倒掉了。不过具体什么原因就不追究了,目的达到就行了。毕竟只是改软件,又不是分析软件。

      删除原来所有的断点,重新在CreateDialogParamA处设断,既然已经知道了是双击备拖盘图标后才创建的窗体,所以设断的时机当然是选在程序完全启动之后了。设断,然后双击拖盘图 标。断在了这里

    77D35EA0 >  8BFF            mov     edi, edi
    77D35EA2    55              push    ebp
    77D35EA3    8BEC            mov     ebp, esp
    77D35EA5    53              push    ebx
    77D35EA6    56              push    esi
    77D35EA7    8B75 08         mov     esi, dword ptr [ebp+8]
    77D35EAA    33DB            xor     ebx, ebx
    77D35EAC    53              push    ebx
    77D35EAD    FF75 0C         push    dword ptr [ebp+C]
    77D35EB0    6A 05           push    5
    77D35EB2    56              push    esi
    77D35EB3    FF15 2404D777   call    dword ptr [77D70424]             ; kernel32.FindResourceExA
    77D35EB9    3BC3            cmp     eax, ebx
    77D35EBB    74 40           je      short 77D35EFD
    77D35EBD    57              push    edi
    77D35EBE    50              push    eax
    77D35EBF    56              push    esi
    77D35EC0    FF15 C402D777   call    dword ptr [77D702C4]             ; kernel32.LoadResource 

      当然这些信息仍然不重要,重要的是 堆 栈(话外音:我日,不重要你老帖出来干嘛  回答:了解一下没有什么坏处)  

    0012FD5C   00415E68  /CALL 到 CreateDialogParamA 来自 复件_IPM.00415E62
    0012FD60   00400000  |hInst = 00400000
    0012FD64   00000065  |pTemplate = 65
    0012FD68   00000000  |hOwner = NULL
    0012FD6C   00415B5D  |pDlgProc = 复件_IPM.00415B5D
    0012FD70   00000000  /lParam = 0

      因为在双击后,主窗体很快出来了,所以最大的可能性就是:第一个断下来的地地方就是创建主窗体的地方。看堆 栈 信息第一排。那个 来自复件_IPM.00415E62。这个就是本程序调用此函数的地方了。再看看栈顶(这个第一行就是栈顶)。红字那个地址00415E68 ,这个就是执行完这个函数后要返回到的地址。00415E68 - 00415E62 =6  。 CALL指令两个字节,后面加一个地址(32位正好四字节)不正好是6吗。所以,如果遇到OD没有提示的情况下,可以使用这种方法确定调用地址,或者干脆不用算了,直接回到栈顶所指向的地址00415E68,然后往上看一行就行了,那个地方肯定是调用的地方。当然既然已经知道主窗体是使用CreateDialogParamA创建的,那其实已经很简单了,根本连程序领空也不用回了,使用CreateDialogParamA,必然会使用 窗体模版,这个在资源里一定能找到。看堆 栈 信息第三行,|pTemplate = 65 这个就是模版的资源号了。当然这个是16进制的。因为下面我们要使用EXESCOPE,所以得转成十进制101,因为EXESCOPE里的资源号是用十进制表示的。我勒个了去,原来啥都不懂的时候,还知道使用EXESCOPE打开文件,更改窗体的样式。现在居然一时没想起来,反而饶了这样一个大圈子,倒掉了。当然郁闷归郁闷。改还是要改。将程序载入EXESCOPE。找资源号为101的资源。肯定是个窗体,有了EXESCOPE,那就随你想怎么样就怎么样了。具体就不讲了。反正改就是了。到这里,调整窗体大小算是完成

    调整后界面:

     

       当然看到这里的朋友也不要觉得上当了,虽然上面做了N多无用功,但涉及的破解知识点以及逆向思路还是不少的。

      

      2. 改变图标

      改变图标是个很简单的工作。用EXESCOPE直接就可以修改。需要注意的原来的图标是32*32 16色的,你也必须找个同样大小,同样16色的图标,否则替换不了。EXESCOPE是不能改变资源的大小的。ICON的格式有点类似于BMP,点阵式的。图片文件的大小(占用的字节数)只和分辨率和位数(16色是4位)有关,和内容无关,不象JPEG和GIF等一些压缩格式的图片,文件的大小和图片的内容有很大的关系。

      使用EXESCOPE打开飞鸽传书,当然备份一份是必不可少的。这是一个好习惯。

     

      文件-》导入,选择你要替换的那个ICON,点打开。我选择的是自己处理的一个ICON,当然也是比较难看了。不过感觉还是比原来的好看一点。

     

      图标替换是完成了,但双击运行后。却发现了一个问题,就是这个图标颜色本身就显得有些暗,而且大部分又是透明的,所以显示在拖盘上就很不明显()而且如果已经习惯了以前的飞鸽图标,这突然一变,多少都会有些不习惯。所以我们有必要更改下拖盘图标,把他换成原来的图片。当然动手前还是要先确定思路,方法基本上和上篇一样,就是截API调用。先看上图,图标资源105 下面还有一个108 .打开后看和原来的图标差不多。所以就想可以把这个108 的图标换成原来的图标,然后将拖盘的图标改成使用108的。这样不就可以了嘛。

      先做准备工作,将108 的图标换成原来的105的图标。这个方法和前面一样,原来的那个图标可以从备份的那个文件里导出来。具体过程就不讲了。一切就绪后。就得开始 断AP I。

      提到拖盘图标。我们第一时间想到的当然是 Shell_NotifyIconA(W).下面我们来看看他的声明。

    Shell_NotifyIconA(
      DWORD dwMessage,
      PNOTIFYICONDATAA lpData
    )

      dwMessage 有三个值 NIM_MODIFY,NIM_ADD,NIM_DELETE分别指修改,添加,删除图标。

      LpData 是一个PNOTIFYICONDATAA。声明如下

    typedef struct _NOTIFYICONDATAA {
      DWORD cbSize;
       HWND hWnd;
       UINT uID;
       UINT uFlags;
       UINT uCallbackMessage;
       HICON hIcon;
       CHAR   szTip[64];
    } NOTIFYICONDATAA, *PNOTIFYICONDATAA; 

      cbSize指本结构的大小

      hWnd,指接收消息的窗体(当你左键单击,或者右键单击图标时,会自动发送一个消息(此消息由此结构后面的uCallbackMessage定义)给这个窗体))

      UID 图标的ID

      uFlags用来设置以下三个参数uCallbackMessage、hIcon、szTip是否有效.

      UCallbackMessage  当对拖盘图标进行操作时(左键双击WM_LBUTTONDBCLICK)时,向窗体发送的消息号。目标窗体只须处理这个消息号,并通过lapram判断所进行的操作(如左键单击,双击,右键单击等)然后做相应的处理就行了。

      HIcon 图标句柄,注意一下这个,这个是要重点分析的。

      SzTip 拖盘提示语句.

      具体怎么使用这个函数,在这里就不详述了,大家可以查查相关的资料。在这里我们唯一要注意的就是那个HICON hIcon。这个就是指拖盘的图标了,但不幸的是。HICON明显是个图标的句柄,而不是我们想要的图标的资源号。如果是资源号的话,直接就可以在这个地方改了,但如果是句柄的话,可能就得烦一些了。

      一开始的思路是。断下这个API后,返回到程序领空,在PUSH 参数的时候,得到上述的那个结构的地址,然后找到 HICON 的位置,下内存写入断点。这样就可以断在 给HICON 赋值的地方,这个地方一般离得到HICON的地方不会很远,最有可能的就是上一行。这样就可以跟进到获取HICON的函数里,这个函数里一定会使用到图标资源号,这样就可以找到图标资源号 并修改它了。但是实际运行中。却发现那块内存不止被这一个结构使用,写入的太过频繁,所以放弃了。于是另寻思路。现在我们的目的是找到获得HICON的函数,而且这个函数要和图标资源号相关,那么有几个,LoadIconA(W),LoadImageA(W )。最有可能的就是LOADICON。

    LoadIconA 声明如下

    LoadIconA(
        HINSTANCE hInstance,
        LPCSTR lpIconName
    );

      hInstance.是指出资源所在的模块(可能是EXE也可能是DLL)的实例。

      lpIconName是指向NULL字符结尾的字符串的指针,它包含图标名。当然在这里我们也可以传一个资源号(资源标识)进去。所以这个函数正是我们需要的。当然在VC里传资源标识

    的时候。要使用MAKEINTRESOURCE 转换一次。要不然是不可以通过的。当然这个和逆向没关系。 思路确定了,下面就是要确认了

      重新在OD里载入飞鸽传书。断在入口点。

      菜单栏->查看->断点 删除所有断点。在代码区(反汇编区)右键->查找->所有模块中的名称,键入 LoadIconA,选择模块是user32 的那个,右键 切换断点。

      查一查断点窗口,是不是多了一个断点,是的话。F9,断了下来,代码区就不看了,没什么用处,直接看 堆栈

    0012FCFC   004041A4  /CALL 到 LoadIconA 来自 IPMSG.0040419E
    0012FD00   00400000  |hInst = 00400000
    0012FD04   00000069  /RsrcName = 105.

      正好两个参数全在这了。HInst = 00400000 这个一眼就看出来了,明显是指本程序。WINDOWS用户级 的程序默认的加载地址就是这个。RsrcName = 105 。105 不就是原来的那个图标的资源号嘛。当然我们还不能高兴的那么早,因为这个时候加载的图标,不一定是给拖盘图标使用的,有可能是设置窗体图标的。所以还得走着看看。先回到程序领空里调用这个函数的地方。和原来一样,看第一行。“来自 IPMSG.0040419E“ 

    0040419E 就是调用此函数的地方了。在代码区 右键->转到->表达式,填入 0040419E。代码区第一行是

    0040419E  |.  FF15 8CB24100 call    dword ptr [<&USER32.LoadIconA>]  ; /LoadIconA

      向上翻几行。

    00404192  |.  6A 69         push    69                               ; /RsrcName = 105.
    00404194  |.  50            push    eax                              ; |hInst => 00400000
    00404195  |.  8975 C4       mov     dword ptr [ebp-3C], esi          ; |
    00404198  |.  8975 C8       mov     dword ptr [ebp-38], esi          ; |
    0040419B  |.  8945 CC       mov     dword ptr [ebp-34], eax          ; |
    0040419E  |.  FF15 8CB24100 call    dword ptr [<&USER32.LoadIconA>]  ; /LoadIconA

      其中最后一排就是 我们刚才找到的调用的地方。不管它,看第一行。Push 69. 十六进制的 69 不就是十进的105(6*16+9)嘛。好的。在这一行F2设断。CTRL+F2 重新载入程序。F9.断在了00404192 处。双击这一行,在弹出的窗口里将69(105)改成 6C(108)。F9。断在了LoadIconA处。看堆栈

    0012FCFC   004041A4  /CALL 到 LoadIconA 来自 IPMSG.0040419E
    0012FD00   00400000  |hInst = 00400000
    0012FD04   00000069  /RsrcName = 108.

      说明我们已经将图标的资源号改过了。

      先不用管它,继续F9。又被断了。看堆栈

    0012FCD0   00408823  /CALL 到 LoadIconA 来自 IPMSG.00408821
    0012FCD4   00400000  |hInst = 00400000
    0012FCD8   00000069  /RsrcName = 105.

      调用来自另外一个地方,传过来的图标资源号还是原来的105.再看拖盘,图标没有出来。看来我们第一次改的那个地方,八成是不对的了。先记下 00408821 这个地址。继续F9。又被断了。堆栈

    0012FCD0   0040885B  /CALL 到 LoadIconA 来自 IPMSG.00408859
    0012FCD4   00400000  |hInst = 00400000
    0012FCD8   0000006C  /RsrcName = 108.

      虽然这里RsrcName 是 108 但调用地址明显和第一次断的地方不一样。所以这个地方应该就是加载真正的图标108(现在的图标108已经在前面被我们改过了)的地方。所以这个地方对我们来说,用处不大。继续F9。再次被断。堆栈

    0012EA48   73658B1B  /CALL 到 LoadIconA 来自 73658B15
    0012EA4C   00000000  |hInst = NULL
    0012EA50   00007F00  /RsrcName = IDI_APPLICATION

      这个调用看地址就不在本程序的领空了,所以暂时先忽略掉它。继续F9。拖盘图标这时已经出来了。图标还是原来的。OD显示了程序处于运行状态。

      从上面来看,在程序处于运行前,有四个地方调用了LoadIconA.刚才第一个地方已经试过了。不是正确的地方。第三次传入的参数是原来的108,所以应该关系也不大。至于第四个,返回地址根本不在程序领空,所以也不用管,那么剩下的只有第二个调用了。

      Ctrl+F2重新载入,先不忙着F9。为了不让其它的断点影响到我们,先清除所有的断点。另外刚才我们也已经记下了第二个调用LoadIcon的地方的地址,就是 00408821。代码区,右键->转到->表达式,填入这个地址   点确定

    00408819  |> /6A 69         push    69
    0040881B  |.  FF35 AC224200 push    dword ptr [4222AC]
    00408821  |.  FFD3          call    ebx  

      最后一排就是刚才找到的那个调用的地方,当然也不用管它。看第一行。又是一个Push 69.好的。在这一行F2设置断点。F9。断在了这里。和刚才一样,改 65 为 6c. 没有再被断下来。程序也正常运行起来,图标也已经出来了,而且已经变成了108对应的图标,也就是我们先前导入到108里的那个原先程序的图标了。至此。算是差不多已经完成了。但还存在一个问题,就是我们目前还只是在OD里改了代码。此时改的也只是内存里的代码而已。重新载入后不又没有了嘛。不要紧。OD功能还是很强大的。

      在代码区 右键->复制到可执行程序->所有修改。弹出提示框。点 全部复制 ,弹出窗口,不用管,直接关闭,此时会提示你是否保存,点是,选择我们要替换的文件,此时问你是否覆盖。是。OK,一切搞定,关闭OD。双击我们修改后的飞鸽传书。再看一看。拖盘图标是不是又和原来一样了哈J.

      注:在实际当中,使用的分析方法和此略有不同。在改了第一个调用,发现不成功后。我就重新载入了  程序。然后在shell_notifyicon上设了断点,找到了调用它的地方,又设了断点。然后运行程序。记录每个调用LoadIconA的地方的返回地址,直到断点断在了调用shell_notifyicon的地方为止,那么我记录下来的最后一次调用LoadIconA的地方就是设置拖盘图标的那个LoadIcon 了。事后,发现,其实没这么麻烦。用上面的那个分析方法就已经能够确定了。故在这里将较为简单的方法作为主要介绍,而实际当中的分析方法只作为一个参考而已。

      好的。到此。第二步总算也完成

      3. 启动就显示窗体,而不是双击图标后才显示   

      下面我们来解决开篇里提到第三个问题:双击启动后不弹出窗体,而只是在拖盘上有个图标,需要双击拖盘图标才能打开主窗体 的问题。

      在正式开始之前,还得先回忆下在解决第一个问题时分析出来的“成果”:1. 主窗体是在双击拖盘图标时创建的。2.点主窗体上的叉是关闭主窗体,而不是隐藏它。3. 创建主窗体的函数是CreateDialogParamA。 本节将在这些分析的基础上继续。

      同样的先确定思路。本来的思路是在Shell_NotifyIcon 后,随便找个位置,将那个位置的代码改成jmp A。A就是我们写内存补丁的位置,在A位置调用CreateDialogParamA,然后显示窗体,最后,再跳回到原程序的相应 位置继续执行。这个方法是可行的,而且CreateDialogParamA本身就存在,不需要我们重建输入表,省了不少的事。但却也有些问题,例如CreateDialogParamA要指定窗体的处理函数,这个处理函数虽然很容易跟出来,在这里设置也不麻烦,但感觉上总有点不爽。另外CreateDialogParamA参数还有好几个,都要处理一番。还是有些麻烦(其实也不是特别麻烦)的。于是就想有没有更好一点的办法,这时忽然想起,主窗体是在双击拖盘图标后才创建的,而且每单击一次就会创建一个主窗体。那么极有可能的情况是,创建主窗体的过程已经在原程序被写在了一个方法里,这样我们就不需要直接调用CreateDialogParamA然后再去处理因为调用他而引起的一系列后续操作了。只须简单的调用下原程序里的那个创建窗体的函数即可。思路是确定了,剩下的就是证实和实现了。

      将飞鸽传书载入OD,代码区->右键->查找->所有模块里的名称,输入CreateDialogParamA,找到模块是user32.dll的那个F2设断,F9 直到OD显示 运行.双击 拖盘图标,断了下来。

    当然代码区不是我们关心的,看堆栈

    0012FD5C   00415E68  /CALL 到 CreateDialogParamA 来自 IPMSG.00415E62
    0012FD60   00400000  |hInst = 00400000
    0012FD64   00000065  |pTemplate = 65
    0012FD68   00000000  |hOwner = NULL
    0012FD6C   00415B5D  |pDlgProc = IPMSG.00415B5D
    0012FD70   00000000  /lParam = 0

      代码区->右键->转到->表达式,填入00415E68  (看堆栈第一排,如果不知道为什么 写它, 第一节有讲)。代码区来到这里。

    00415E68  |.  85C0          test    eax, eax
    00415E6A  |.  8946 20       mov     dword ptr [esi+20], eax
    00415E6D  |.  75 0B         jnz     short 00415E7A
    00415E6F  |.  56            push    esi                              ; /Arg1
    00415E70  |.  E8 CEFDFFFF   call    00415C43                         ; /IPMSG.00415C43
    00415E75  |.  59            pop     ecx
    00415E76  |.  33C0          xor     eax, eax
    00415E78  |.  EB 03         jmp     short 00415E7D
    00415E7A  |>  6A 01         push    1
    00415E7C  |.  58            pop     eax
    00415E7D  |>  5E            pop     esi
    00415E7E  /.  C2 0400       retn    4

      你可以向上翻一翻,这段代码的第一行也就是00415E68 的上一行就是调用CreateDialogParamA的语句,当然在这里我们不管这些,在这一行,F2.设断,F9运行到这里。此时窗体并没出来。F8(单步步过)几步走到00415E7E  处,此处是RETN。不用管,继续。来到这里。

    0040753F  |.  8B07          mov     eax, dword ptr [edi]             ;  IPMSG.0041C2A0
    00407541  |.  6A 0A         push    0A
    00407543  |.  8BCF          mov     ecx, edi
    00407545  |.  FF50 04       call    dword ptr [eax+4]
    00407548  |.  6A 01         push    1                                ; /Arg2 = 00000001
    0040754A  |.  57            push    edi                              ; |Arg1
    0040754B  |.  8BCE          mov     ecx, esi                         ; |
    0040754D  |.  E8 B8220000   call    0040980A                         ; /IPMSG.0040980A
    00407552  |.  395E 6C       cmp     dword ptr [esi+6C], ebx

      还是继续F8,当走过00407545  处的那个CALL之后,窗体出来了(任务栏图标已经出来) 看来再遇到一个retn就差不多要找到我们要找的函数了。继续。来到00407592.此处是一个retn。继续。来到这里。

    00405590   . /E9 CB000000   jmp     00405660
    00405595   > |8B06          mov     eax, dword ptr [esi]             ;  Cases A1 (WM_NCLBUTTONDOWN),201 (WM_LBUTTONDOWN) of switch 00405540
    00405597   . |8BCE          mov     ecx, esi

      向上翻一翻。

    00405587   .  55            push    ebp
    00405588   .  55            push    ebp
    00405589   .  8BCE          mov     ecx, esi
    0040558B   .  E8 E71E0000   call    00407477
    00405590   .  E9 CB000000   jmp     00405660
    00405595   >  8B06          mov     eax, dword ptr [esi]             ;  Cases A1 (WM_NCLBUTTONDOWN),201 (WM_LBUTTONDOWN) of switch 00405540

      上段中红字的那行,就是刚才来到的那一行。看一下他的上一行 call    00407477.这个就是我们要找的函数了。他有两个参数。PUSH EBP ,PUSH EBP。此时EBP是0.唯一感觉奇怪的是两个PUSH下面的那个mov ecx,esi. 因为这个软件是采用VC写的。所以这个函数可能采用的是thiscall的调用方式,使用  ECX传递this指针,当然这只是猜测,未进一步分析。只是对程序在不同的时间进行了一下跟踪,发现ESI的值一直保持不变,所以在补丁里加入这句也不会影响程序的正常运行。                               

      好了。确定了调用函数。下一步,要确定调用的地方了。具体怎么找调用的地方,和上面的步骤差不多,只要断一下Shell_NotifyIcon.然后不停的F8.找个合适的位置就是了。一开始的时候。找的是004080d5。后来发现在这里调用创建窗体的函数会在启动时创建两个主窗体。所以改在了004050AA处。当然在改跳转之前,我们还得在程序里找一块地方供我们写补丁。此时PEID再次出场。

      用PEID打开程序。找到 EP 段字样,跟在他后面的有一个按钮,按钮的文本是”>”.点一下。随便选择一个(.text .rdata……).右键搜索全0处。弹出如下对话框

     

      从图中可以看出来。.text 节的偏移 0001A72A处有大小为 8D6 的空白位置。

      8D6 = 2000多字节,差不多 2K了,绝对是够用了。所以就确定在这里面写了。当然我们不能使用RVA,而应该使用 基址+RVA 此程序的基址是 400000.所以我们写内存补丁的地方,应该在41A72A- (41A72A+8D6)之间,我们就确定在41A730处吧,这个地址好记哈。至于具体为什么这样确定,我想应该不要讲了吧。从RVA的名字也猜出来了。RVA的意思:相对偏移地址。相对于什么呢,当然是程序的基址了,而且一般EXE的基址都是400000.当然也有不一样的。不一样也不难确定。看看OD就知道了,OD里反汇编出来的代码的地址都是线性地址,也就是说都加了基址的,所以只须看看在OD里程序领空里的地址是多少就大致清楚了。当然这种方法确定的不是很准。准一点可以使用PE查看工具,查看NT头部分。具体就不讲了,可自已去查相关资料。

      好的。现在确定了 调用 函数,确定了内存补丁位置,确定了跳转至内存补丁的位置,那么下一步自然是实现从跑转位置跳到补丁位置,然后写内存补丁了。

      重新回到OD。来到 004050AA 处。

    004050AA处原代码如下:

    004050AA   . /75 15         jnz     short 004050C1 

    改成

    004050AA   . /75 15         jnz     41A730   

    F8 运行到 41A730 处。

    依次写入下列补丁程序。

    0041A730      60            pushad                // 保存现场
    0041A731      6A 00         push    0             // 参见前面的分析 原来是PUSH EBP
    0041A733      6A 00         push    0             // 同上
    0041A735      8BCE          mov     ecx, esi      // 这个可能是thiscall
    0041A737      E8 3BCDFEFF   call    00407477     // 调用创建窗体的函数
    0041A73C      61            popad                // 恢复现场。
    0041A73D    ^ 0F85 7EA9FEFF jnz     004050C1     // 回到正常轨道  

      以上的注释,是我加上去的,可不用写。

      好。做完这些,在代码区->右键->复制到可执行程序->所有修改。弹出对话框,选全部复制,弹出窗体,直接关闭,问是否保存,点是,选择原来的文件替换。好了。关闭,OD。双击原来的程序。看看是不是已经可以,双击就弹出 窗体了哈。 :)    

    ——

  • 相关阅读:
    2021年终总结
    uniapp开发小程序 使用@escook/requestminiprogram配置网络请求
    免费小图标(ico图标)制作工具网站
    Visual Studio Code怎么连接夜神(Android Studio 作者:锐琪视频 https://www.bilibili.com/read/cv2627730/ 出处:bilibili)
    Redis集群
    有感于携程的“混合办公模式”
    JavaSE基础day10多态、抽象类/方法、接口
    JavaSE基础day13异常处理
    JavaSE基础day17 IO操作01
    JavaSE基础day14集合
  • 原文地址:https://www.cnblogs.com/jivi/p/2989457.html
Copyright © 2020-2023  润新知