好友
阅读权限25
听众
最后登录1970-1-1
|
本帖最后由 cdj68765 于 2015-6-2 13:20 编辑
这次我处理的文件是64位的MikuMikuDance,这是由一位日本职人为了新手舞蹈制作而编写的3D软件,由于是在日语环境下编写的VC程式,因此在中文系统上面运行会出现各种乱码,而我需要做的,便是通过编写汇编代码,植入到该程式里面,来让乱码变成原本应该显示的日文,并以此来提升自己在64位反汇编上面的技术。
首先是使用工具:
由于支持64位的OD作者还没有发布,而能找到的,功能强大的动态反汇编工具也屈指可数,于是我姑且找了个x64dbg来用(用过以后发现,意外的好用,并不比OD差多少)
然后准备了一个Stud_PE以及我最喜欢的EmEditor来充当笔记本以及16位编辑器来使用。
好,万事具备,让我们开始吧。
载入程式到x64dbg,然后在代码最后的位置找一片空白区域,并随便键入汇编代码,比如push rax好了,然后实验性保存一下
阿勒,2个修改中0个修改被应用?然后修改一下原有的程式保存下呢,
3个修改中只有1个修改被应用,
看来修改原有的程序代码范围内的可以保存出来,此外的空白区域算是保存不了了,这在使用OD的时候是完全想不到的问题,于是应该怎么办呢。
首先,我想到的是写一个Dll,然后添加到该程式的输入表里面,于是我确实这么做了,用fasmw汇编编辑器,并用汇编语言编写了个64位的dll,然后使用stud_PE添加我写的dll到输入表里
然后打开修改后的程序,
然后再也没有然后了
经过很长一段时间后我才知道,Stud_PE并不是在原有的输入表上添加新的函数地址,而是新建了一个区块,并把原有的输入表加上新添加的函数都放到了新的区块,再把输入表的偏移地址指向了新的区块,这一切本来是好的,不过这货这样做以后,不知道为啥会导致载入表找不到了,同样,这个问题只在64位程序上面出现,32位的程式上我并没有碰到过这种情况,所以,我现在还不知道64位该怎么添加dll到输入表,还望知道的大神能教教我这个菜鸟小生。
好了,加载dll到主程序无望,我只能另辟他经,于是呢,我只能先在x64dbg里面打好汇编代码,调试好以后,保存成二进制,使用EmEditor的16进制编辑功能16个字节16个字节地修改,这过程太痛苦,至今不敢回忆,幸好X64dbg有保存/载入历史操作记录的功能不然我骂娘的心情都有了。
上面那个麻烦的方法我用了一段时间后(其实大多数时间我都在使用X64dbg的载入历史修改的功能,实际用16进制直接对源程序修改的过程并不多)终于忍无可忍,于是我在想,它原来的程序空间不让保存修改,那么我自己建一个区块来保存我自己编的代码不就行了?于是我用Stud_PE添加了一个新区间,设置区块的属性和.text的一样,并且填充数据00
请必须设置新区块的属性和.text的一模一样,也就是右边那些勾选的flags一样就行
载入主程序到X64dbg(载入这么多次怎么还没开始动态调试,233),然后我们怎么知道我添加的区块在哪个位置呢,其实,你点下任何一行汇编,在
在这里就会显示你所指的那行汇编在哪个区块里面
然后你找到主程序汇编代码的第一行,
看到没,后面是虚拟地址,前面那个1000是汇编代码的偏移地址(其实随便找一行就行,并不一定非要选择第一行),我们用计算器算一下,怎么算呢,就是把后面的地址 也就是13F0C1000-1000(用16进制计算),得到一个虚拟基址13F0C0000,然后我们看
上面写着,我们添加的区块的虚拟偏移地址是1A8000,于是我们把13F0C0000+1A8000=13F268000,非常好,这个地址就是我们新添加区块在内存的虚拟地址,我们按下Ctrl+G,输入这个地址后确认,就跳到我们新添加区块的位置了,
可以随便点个地方确认下,这里会显示你自己添加区块的名字的。
于是我随便往下几行找了个空白处,打了几行代码,保存,
很好,可以保存了。
让我们开始正题吧。
开始正题之前。让我讲一个64位的一个基础知识吧。
大家应该知道,32位汇编在调用一个call之前,通常需要把几个参数push到堆栈,但64位不同,至少64位的call在输入前4个参数前不同,这前四个参数只需要填到r8,r9,rcx,rdx这四个寄存器里面就行。虽然这样做对于刚接触64位汇编的人员可能会很不习惯,不过接触过一段时间以后,会发现这样做其实比32位那种模式要好,因为这样做有种高级语言里面给参数赋值的感觉,只不过赋值对象这里换成了寄存器,而对堆栈操作我总感觉不是很放心,因为一不小心,你就不知道堆栈跑哪里去了。
说起堆栈,我想就以刚接触汇编的新手角度,来讲讲堆栈,
我相信,刚接触汇编的同学看到 qword ptr ss:[rsp+20],这种语句就不明所以,不知道表达的是什么意思,其实以前我也是如此,但现在我可以直接告诉你们,但凡看到这样的语句,请直接把目光移到右下角的堆栈框,所有这样的语句都是跟堆栈有关的操作,我们不用管 qword ptr ss:这里到底写的什么,只要看到这个样子的东西,就意识到是要对堆栈进行操作就行了,我们重点看的是后面的rsp+20
双击堆栈框的地址部分,让地址变成
酱紫子,看到没,长长的地址就变成了简单的数字加减,那么,rsp+20就可以简单的理解成相对于当前地址(反白的那条),+20的堆栈在哪了,再简单点讲,找到$+20这几个字便是rsp+20了,(如遇到rbp+20的情况,请右键堆栈框,选择Follow RBP)
然后注意下,地址栏反白的表示当前rsp(堆栈指针)所在位置,请保证指示箭头和当前堆栈位置统一后再读取,比如
双击其他地址以后,指示指针就换到了你双击地址的位置,但当前堆栈指针在反白的那里,也就是$-10,这种情况下面,读取rsp+20的话,就是不对的,嘛,反正你只要知道,指示指针所在的位置就是+ -的中心,你只要保证堆栈位置跟指示指针在同一位置就行了,不用管那么多
好,我们终于把前戏讲完了,于是开始讲调试方面的事情吧,首先,我们要明确一下处理的目标
看到没,这些下拉或收起组合框里面的文字,都是乱码,我们就需要把这些乱码给转换成原来应该显示的日文
然后让我们想想,是哪个Windows 的API来让这些文字显示出来的,查下MSDN,显然跟下拉窗口文字有关的API是Sendmessage,我们查下sendmessage的输入参数,
SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam),而我们需要处理的文字字符串显然在Msg这个参数里面
知道我们想要处理的参数在哪里后,接下来该怎么做呢,让我们再找找Windows的API里面,哪个API能够改变字符串的编码?翻来翻去,找到两个,MultiByteToWideChar和WideCharToMultiByte,一个是字符串转宽字符,另外一个是宽字符转字符串,要用哪个其实是看源程序里,这一块是调用SendmessageA还是SendmessageW的,好,这个先缓一下,我们从程序里面入手吧
载入程序到X64dbg,然后下断点,在X64bdg里面,怎么下断点?跟OD一样就行,命令行里面打bp SendMessageA
,不过我不喜欢打指令,X64bdg里面,还有更加轻松的办法,
Symbols,然后选中user32.dll这个系统动态链接库里面的SendMessage,按下F2就能在该API上面下断了,不过,不管是bp SendMessageA还是在这个地方下断,都只是在user32.dll里面下断,并不是在源程序的call上面下断,这点请注意,因此,这样下断以后,对源程序的任何操作,都会无意中触发这个断点,但这些断点都不是我想要的,因此我先在
断点选项卡里面,把该断点禁用,然后等载入模型以后,再启用该断点,
因为是在user32.dll里面设的断点,所以每断一次,都会在堆栈上面留下一句话
,我们鼠标点下这个return条,按下回车,就能回到主程序里面,调用该API的call了,于是,我每断一次,就返回到主程序的call,然后做个标记,很枯燥的反复这么做很多次,直到载入的模型显示出来并且不再有断点发生
以上过程是不是觉得很繁琐?因为OD的话,有更简单的方法,就是Ctrl+N,然后找到该API,点下在每个参考点上设置断点就行了,
那么X64dbg有没有类似OD里面Ctrl+N的功能?我说,有,只要右键汇编指令,选择
搜索,从calls搜索,该工具就会在
这个选项卡里面,把所有的call都显示出来,然后只要在下面的搜索栏搜索你需要的API就行了,不过这里也有X64dbg设计不完善的地方,也就是没有一键全部下断的功能,只能一个个下断,并且下的断点越多,程序就越卡,所以这个功能在call的API不多的地方蛮实用的,但是一旦遇到用了成百上千的call的时候(比如SendMessage),还是在系统dll里面下断比较好。当然,这个功能 用来寻找API调用还是很省事的,右键,然后
跟随到汇编,就自动跳转到call当前指令的汇编代码处了,也算是常用功能了吧
我们讲到哪里了,对了我们费尽千辛万苦,终于找到我们想要处理的函数了,是SendMessageA,
来,我们抓紧把这个call改成我们自己写的地址把
等等,先不急,对于X64的汇编,我觉得我们有必要先研究下,它原本的API调用机制再下手也不急,首先,我们让我们想一想,64位的汇编在call之前需要干嘛,对,之前就提到了,需要四个寄存器里面填入对应的参数,哪四个?就是r8,r9,rcx,rdx,好,然后SendMessage这个API需要几个参数?也是四个 hWnd,Msg,wParam, IParam,,于是我们运行一下程序,看看这四个寄存器里面,都装了些啥,好让我们判断对于该API来讲,如果是我们自己来写这个call,该怎么输入寄存器的值
这里需要注意一下,由于64位是根据你上述寄存器里面的值来决定你输入给call的参数是多少的,跟32位时代,
从上到下push相关的参数完全不一样,简单来说,并不是你第一行把参数mov给rcx寄存器,就代表rcx里面存的是IParam参数,所以,你赋值给寄存器的先后顺序并不重要,重要的是你call的时候,寄存器上面数值的多少,再明确的讲,必须要知道,call这个API的时候,r8,r9,rcx,rdx这四个寄存器对于该API到底对应的是哪四个参数你才能下手
首先判断第一个参数句柄吧
那么对于该API,哪个寄存器里面存的是句柄的地址呢,RDX和R8首先pass,因为句柄不可能是简单的赋值数字以及0,那么可能的就是rcx和r9这两个寄存器了,我们先看下R9吧,右键R9,然后
跟随到内存,一看,
,数据太有规律了,简直像是一段段待加工的零件一样,十有八九就是SendMessage需要的msg参数,而不是句柄,
因此我们就当做rcx存的是句柄吧,然后r9存的是msg,那么rdx呢,给了个常量143.十有八九是wParam,最后还剩下个r8不用猜就是IParam无误,
确认最重要的几个线索以后,让我们回到之前讲的MultiByteToWideChar和WideCharToMultiByte问题上面,这里源程序调用的是SendMessageA,因此很明显我们应该用MultiByteToWideChar把字符串转换成宽字符,要转换的目标是r9寄存器里面存的地址当中的数据,于是可喜可贺,我们还要研究一下MultiByteToWideChar的函数调用问题
嘛,学习是在模仿中进行的,让我们看下源程序是怎么处理MultiByteToWideChar这个函数的再下手吧,随便跳到源程序的某个典型call MultiByteToWideChar当中我们如法炮制的研究下各个寄存器里面应该填写什么参数[Asm] 纯文本查看 复制代码 $ ==> | mov r8,qword ptr ss:[rsp+60] |
$+5 | mov r9d,ebp |
$+8 | xor edx,edx |
$+A | mov ecx,r14d |
$+D |mov qword ptr ss:[rsp+20],rbx |
$+11 |mov dword ptr ss:[rsp+28],ebp |
$+16 | call qword ptr ds:[<&MultiByteToWideChar>] |
好了够典型了,在这里我就要讲一下其他的东西了,你看哦,如果call的API只有四个参数,我们只需要在四个寄存器里面存就行了,那要是跟MultiByteToWideChar一样,需要第5个,第6个参数呢,那怎么办?,如你所见,超过4个参数的时候,第5个参数开始,就要往堆栈里面塞了,怎么塞?就是像这样,用mov dword ptr ss:[rsp+28],ebp mov qword ptr ss:[rsp+20],rbx往堆栈塞,从rsp+20开始,每添加一个参数,rsp+20就增加一个8位(64位下,call API预留20h,也就是4*8个堆栈空间,用来给四个寄存器用?笑)
然后你也看到了,这种情况下面,就是什么准备都没有地,直接往堆栈塞数据了,而且并不是32位下使用push那种,每push一次就申请一层堆栈来放数据,而是直接往原有的堆栈里面,不做任何工作直接修改原有的数据,这么做有多么危险,我想不用我多说,万一把有用的堆栈数据给覆盖掉了,变成你用来调用API的参数,等你call完API以后,程序因为找不到原来有用的堆栈数据而崩溃你哭都来不及,因此,请务必在准备填入你需要的参数给API之前,先用sub rsp,28h(堆栈地址-28h,简称申请28h堆栈空间,(注:所有对rsp的操作都是对堆栈的操作)来申请一段安全的堆栈空间,至于申请多少,自己看着办,多点少点无所谓,按照公式来讲,是8+你需要参数个数*8,比如4个参数的话,需要申请40的空间,转换成16进制,也就是28h, 就是说,你怎么着也要来个sub rsp,28吧。
至此64位的fcall我们已经知道了,我们来研究下,MultiByteToWideChar需要哪些参数,各对应什么寄存器
首先还是去看看MultiByteToWideChar的定义吧,int MultiByteToWideChar(UINT CodePage,DWORD dwFlags,LPCSTR lpMultiByteStr,int cchMultiByte,LPWSTR lpWideCharStr,int cchWideChar);
哇好长啊,我研究一下32位下面是肿么样的再说
[Asm] 纯文本查看 复制代码 push eax ; /WideBufSize
push [arg.2] ; |WideCharBuf
push ebx ; |StringSize
push [local.3] ; |StringToMap
push 0x0 ; |Options = 0
push 0xFDE9 ; |CodePage = 0xFDE9
call dword ptr ds:[<&MultiByteToWideChar>]
还是看汇编语言上面的注释好理解意思,定义那长长一串到底是毛玩意啊,好我们首先看到了CodePage,让我想想,我现在因为用的中文系统,所以程序以中文的编码打开了日语的字符串,这是我所不希望的,于是我打算把乱码通过MultiByteToWideChar转换成日语文字再输出显示,因此,CodePage应该填写3A4(日语编码,中文的是3A8,关于这个编码问题,网上有表可以查)
于是根据寄存器,
可以很轻松知道,rcx里面存的是Codepage参数,
那么从上往下看,rdx呢,被异或处理了,数值全变成了0,那十有八九是Options ,可以不管
然后看青色的两个框,rbp和r9,在源代码里面rbp寄存器同时赋值给了r9寄存器和dword ptr ss:[rsp+28]堆栈,于是我们可以理解这两个寄存器里面存的是WideBufSize以及StringSize,至于哪里是哪个并不重要,只要保证这两个地方的数值是一样的就行了,最后还剩下两个参数,一个是原字符所在地址,还有个是MultiByteToWideChar处理时所需要的缓存地址,这里也可以理解成该API处理完源来的字符串后,把结果保存在哪个地方给我们用,要分辨哪个是哪个也很轻松,你想哦,源字符串所在的地方,肯定是一串规律的字符串,而将来用来保存处理后的地址,肯定是一堆意义不明的东西,我们只要分别看下这两个地址所指向内存里面,显示的东西就行,
看这个是R8所指向内存里面的东西
看,红框内的英文字(这里你们可能会奇怪,你要处理的不是日语乱码问题么,怎么冒出来的却是英文?其实这是一句日文伴随英文的语句,x64bdg的内存内容显示窗口用的是Ascii,所以英文能正常显示,而这段英文前面的那些%$..之类的字符,其实就是日文字),是不是很清楚就能理解?然后我们看下rbx (mov qword ptr ss:[rsp+20],rbx ,这里把rbx的数据给了rsp+20的堆栈位置,所以我们这里就姑且把qword ptr ss:[rsp+20]当做rbx来看了)地址所指向的内存空间,
是不是一堆意义不明的东西?基本上都是00,看来这里就是软件申请到的用来缓存数据的空间了
好,我们来总结一下MultiByteToWideChar在64位下的参数问题
r8,用来存你想要处理的字符串的内存地址
r9,指定字符串中字节的个数
rdx,指定Options,不多说,给个0吧
rcx,指定Codepage,也就是指定代码页,我这里给3A4,根据情况给3A8,或者给1(系统默认代码)也行
mov qword ptr ss:[rsp+20],某个寄存器 指定用来保存结果的缓存地址,这个需要好好弄,我后面会讲
mov dword ptr ss:[rsp+28],某个数字 指定的缓冲区的宽字符个数,不知道怎么设置的话,就和r9寄存器的数值一样就行,
好至此,我们需要处理的两个API需要调用参数,以及各个寄存器对API的意义都搞清楚了,于是我们就可以开始写代码了
终于可以写代码了,这个过程太漫长了啊
讲一下代码的思路,其实很简单,从寄存器获得当前要处理字符串的内存地址给MultiByteToWideChar,然后用MultiByteToWideChar把字符串处理成我想要的内容并输入到我指定的缓存地址,最后调用SendMessageW(因为已经把字符串变成了宽字符,所以这里使用SendMessageW来显示宽字符,而不是原来的SendMessageA来显示字符串)让该API把我指定缓存地址的内容显示出来就完事。
首先,就是push各种寄存器了,我们熟称保护现场,但我不喜欢这种让人听了云里雾里的话,我相信新手听到保护现场这个词一定不知道到底是什么意思,为什么要保护现场。
那我也简单的讲一下吧,所谓的push eax,push ebx,之类的,就是把各种寄存器里的数据,保存到堆栈,是的,你就把堆栈当成前面有序号的记事本来用好了,平时有啥怕忘记又有用的东西尽管往里面塞,这里我们就需要把各个寄存器里面的数据塞进去
有存就有取,pop就是把当前堆栈的数据取出来,放到你指定的寄存器里面
那,你看,有这么多寄存器呢,32位就那么多了,64位又增加了r8~r15,这么多寄存器,我总不至于,每个寄存器都push一遍吧,那还不得烦死
其实不需要push这么多的,我跟你们讲哦,断点断到你要修改的call之前,
然后给寄存器来张截图,这张截图代表的是,在call SendMessageA之前,寄存器的状态,然后我们按一下F8,单步步过
这张图是程序经过call SendMessageA后,寄存器的状态,红色的就是代表改变过了的,而黑色的,就表示,经过这个API以后,数值并没有改变的寄存器
那么,这些寄存器就是我们需要push的,虽然还是很多,不过你挑几个你可能会修改的来push就行了,比如rbx,rbp,rsp,r10,r11,好了,差不多就行了,具体还是看经过你写的代码以后,有多少原本不应该变的寄存器变了,专挑那些进行push就行了,没啥其他的
[Asm] 纯文本查看 复制代码 $ ==> | push rbx |
$+1 | push rbp |
$+2 | push rsp |
$+3 | push rsi |
$+4 | push rdi |
$+5 | push rcx |
$+6 | push rdx |
$+7 | sub rsp,200 |
$+E | call mikumikudance.13F918099 |
看,我push了一大堆寄存器(保存了一大堆寄存器的数据到堆栈)
跟了个sub rsp,200,来申请了200h大小的栈空间,这个没什么好讲的,之前已经提到过了
然后后面跟了个call,这个call的是我写的另外一个功能函数,
看看这个call干了什么事情
[Asm] 纯文本查看 复制代码 $ ==> | push rcx |
$+1 | push rdx |
$+2 | push r8 |
$+4 | push r9 |
$+6 | push r10 |
$+8 | push r11 |
$+B | xor rcx,rcx |
$+E | mov rcx,200 |
$+15 | call qword ptr ds:[<&void * __ptr64 __cdecl operator new(unsigned |
$+1B | pop r11 |
$+1D | pop r10 |
$+1F | pop r9 |
$+21 | pop r8 |
$+23 | pop rdx |
$+24 | pop rcx |
$+25 | ret |
再次push一大堆寄存器以后,call了个operator new的系统API,也就是说,这个功能函数唯一要实现的,只有call一个operator new,关于这个API是用来干嘛的,其实就是用来申请一段空间的,rcx里面用来存你想要申请的空间大小,然后经过这个API以后,系统会为你申请一个你希望大小的内存空间,并返回一个该空间的所在地址给rax,那为什么要用这个呢?还记不记得MultiByteToWideChar需要一个缓冲空间来存处理以后的字符串?在你自己无法想办法找到一个相对安全的空间来保存你处理后的数据的情况下,让系统给你分配一块安全的空间不失为一个好办法。
然后从这个功能函数返回以后,
[Asm] 纯文本查看 复制代码 $ ==> | mov r8,r9 |
$+3 | mov r9,100 |
$+A | mov edx,1 |
$+F | mov ecx,3A4 |
$+14 | mov dword ptr ss:[rsp+28],100 |
$+1C | mov qword ptr ss:[rsp+20],rax |
$+21 | call qword ptr ds:[<&MultiByteToWideChar>] |
没什么好讲的,r8用来存等待处理的字符串地址,只不过现在这个地址还存在r9寄存器里面,然后rax存的是你刚申请到的空间,把rax的地址存到rsp+20这个堆栈里面也没啥好讲的,
紧接着
[Asm] 纯文本查看 复制代码 $ ==> | mov r9,qword ptr ss:[rsp+18] |
$+5 | xor r8,r8 |
$+8 | mov rdx,qword ptr ss:[rsp+210] |
$+10 | mov ecx,dword ptr ss:[rsp+218] |
$+17 | call qword ptr ds:[<&SendMessageW>] |
r9里面存的是你经过MultiByteToWideChar处理后,你申请的缓存地址,在rsp+18这个地方(注意观察堆栈的变化情况,虽然你之前把缓存地址mov给了rsp+20,不过你从ret回来了一次,堆栈加了一个8位,所以缓存地址从rsp+20变到了rsp+18,这段话不用刻意去理解,只要注意观察你所关注的数据,在执行当前指令的时候,处在堆栈哪个位置就行了,
这种++ --,我想应该很容易就能找到的,再重申一次,双击堆栈的地址那块,OD也一样,把地址变成这种样子显示以后,就很容易能理解指令里面各种rsp+x,rsp-x到底代表什么意思了)
好吧,我希望处理的代码在经过SendMessageW这个API以后,就已经完成了,下面我们要还原现场,
[Asm] 纯文本查看 复制代码 $-6 | call qword ptr ds:[<&SendMessageW>] |
$ ==> | add rsp,218 |
$+7 | pop rcx |
$+8 | pop rsi |
$+9 | pop rbp |
$+A | pop rbx |
$+B | ret |
首先用add rsp,218 把堆栈返回到,你用push rcx把寄存器保存到堆栈的位置,有人会问,你这个218是怎么得出来的?你之前不是sub rsp,200,申请了200的空间么,那这里不应该是add rsp,200么,为何变成了add rsp,218,其实吧,你看,我之前多申请了两个寄存器rdi和rdx,因为这两个在我写的代码里面要用到,但又不是需要还原到call之前的寄存器,所以我这里就跳过两个pop,直接从rcx开始pop,也就是rsp+218的位置还原pop,当然这么讲太容易让人不能理解,甚至误解,那我就在这里讲另外一个办法吧,
在call进你自己写的代码之前,先给寄存器来张截图,然后明确需要保存并且还原的寄存器,根据堆栈先进后出,后进先出的原则,在经过call SendMessageW这个API以后,找到你给寄存器截图的那张
图片,看下你最后push的寄存器数值是多少,我这里最后是rdx,不过我不需要还原rdx寄存器的数值,那看看倒数第二个push 呢,是rcx,rcx的数值是多少?
好,是120B6C,那相对于当前堆栈地址,该地址所在的堆栈位置是?$+218,那么很好,add rsp,218,然后依次pop就行了,反正不管怎么样,你至少要保证,到达ret的时候,堆栈指针必须到达
能够返回的地址就行,程序在处理过你写的编码以后,连返回都办不到,还怎么继续处理接下去的指令呢。
好了,至此基本上该做的都已经做完了,期间各种动态调试我就不讲了,蛮枯燥无味的,代码是改了又改,毕竟是底层汇编层面编写的,并没有高级语言那么方便,寻找自己编写的汇编代码的Bug也不是很容易,关键是,网络上关于64位反汇编的知识太少了,都是靠自己不断尝试不断测试,不停的程序出错中走出来的,我希望我这篇文章,能对业界的64位反汇编提供一定的帮助,我就心满意足了
最后的最后,只需要做一步大家都会的工作,
右键你编写代码的第一行,选择复制,地址,然后跑到你想要修改的源代码处
双击
改成,
call 你自己写的代码的第一行地址,点OK,然后右键
随便找个地方保存下就万事大吉了,测试一下效果
恩非常满意,就是这个味道
最后的最后,楼主放着自己的毕业设计不做,这几天都在搞这个东西,再怎么样也意思意思吧,233,光速匿
PS:有任何不懂的,亦或者文章中出错的地方,请都在楼下回复
===========================================================================================================================
喔,没想到大家会这么热情的回复我这篇不咋样的经验分享文章,其实我自己很清楚我的文笔实在不行,很多地方都讲不清楚,嘛,承蒙大家的照顾,我再为这篇文章添油加醋吧 233
其实,上面那个地方的乱码,仅仅只是这个来自日本的软件里面的一小部分,在其他地方,还充斥着各种因为中文系统带来的bug和乱码,接下来,我会为你们一一讲解我是怎么解决的
首先吧,信息对话框,就是Windows经典的,确认/取消对话框窗口,来张截图就是
你看,这个对话框的标题和里面的内容,都是乱码,怎么样也不能这样子吧,于是我们着手修改吧
像这种对话框,不用说,我想聪明的大家都知道是调用Windows API——MessageBox出来的,那么,事情就简单了
在命令栏打bp MessageBoxA,然后找个能让对话框出现的操作,很好,断下来了,
由于我们在user32.dll上下的断,因此我们往堆栈看去
看到那鲜红的return没,不用想,按下回车,回到软件call这个系统API的地方
[Asm] 纯文本查看 复制代码 $ ==> | lea r8,qword ptr ds:[13FF50050] |
$+7 | lea rdx,qword ptr ds:[rbx+2324] |
$+E | mov r9d,40001 | ;r9d:"ctx ", 40001:"ctx "
$+14 | call qword ptr ds:[<&MessageBoxA>] |
于是我们再例行公事,查看MessageBox在MSDN上面的定义,没办法,64位么,在32位上面的所有经验都无法使用,只有微软的白皮书可靠,而且还只能作为参考,实际还要参照调试工具里面寄存器的数值才是真,嘛,这次我就最后再分析一次吧,之后的函数调用,我都直接明说各个寄存器对于该API意味着什么,不再拐弯抹角了
[C] 纯文本查看 复制代码 int WINAPI MessageBox(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType)
如上所示,调用一个MessageBox,需要一个句柄,一个窗体标题,一个文本信息,最后一个窗体类型,一共四个参数,也就是说,call这个API只需要r8,r9,rcx,rdx这四个寄存器
那么我们回到调试器,先断在MessageBox后,再看看各个寄存器吧
不需要管其他的寄存器,在64位下,任何调用都只要看这四个寄存器,以及堆栈从rsp+20开始的数值,然后从上到下依次观察,怎么观察?
从rcx开始,右键rcx后面的数值,右键,跟随到内存
然后我们的目光就应该转向左下角的内存里面的内容了
看不懂怎么办?看不懂就看下一个寄存器,也就是rdx所指向的内容,操作和上面相同
唔,明眼人一下子就应该看到了,ASCII里面几个很明显就是英语单词,而且文本含量这么大,十有八九就是文本信息了,我们暂时先这么确认吧,再看下r8所指向的内容
前面是不明所以的字符,后面却跟着英语,我估计应该是日文后面跟着英语,而且字串有点短,那十有八九是窗口的标题了
最后还有个r9,是个常量40001,各位再怎么样也不会觉得这个常量会是句柄吧,因此rcx里面装的是MessageBox这个系统API的句柄,rdx装的是文本,r8装的是标题,r9就是窗体类型了,我想大家应该不会对我这个分析有意见吧,233
好了,我们理解了这个API的调用后,接下来就要写自己的东西了
要写什么呢,我们来明确一下目标,这个窗体有两个地方出现了字符,标题和文本,按照我在sendmessage上面的处理过程,就是用MultiByteToWideChar这个API把字符串转换成宽字符,转换成宽字符的过程中,顺便转换下字符编码,最后再用MessageBoxW,只不过这里出现了两次文本,因此需要用两次MultiByteToWideChar
明确目标以后,开始编写吧,注释我就直接写在程序里面了
[Asm] 纯文本查看 复制代码 $ ==> | push rbx |
$+1 | push rbp |
$+2 | push rsp |
$+3 | push rsi |
$+4 | push rdi |以上都是将各种寄存器的数据保存到堆栈,因为源程序里面,在我没改之前,程序经过MessageBoxA这个API,这些寄存器里面的东西并没有变,因此我需要保存这些寄存器的数据
$+5 | push rcx |
$+6 | push rdx |至于为什么要保存rcx和rdx,是因为我需要用到这两个寄存器里面的数值,但是在我用到之前,这两个寄存器里面的数值就已经因为我的操作变了,因此我也要保存
$+7 | sub rsp,80 |申请80h的堆栈空间,原因我上面有讲
$+E | call mikumikudance.13FEE8099 |还是跟SendMessage一样,调用我写的这个子程序,申请空间用来给MultiByteToWideChar缓存用
$+13 | push rax |申请到的缓存空间地址保存在rax寄存器,我之后也需要用到这个缓存空间,所以也保存到堆栈
$+14 | mov r9,100 |该寄存器指定读取字节数
$+1B | mov edx,1 |该寄存器指定Options
$+20 | mov ecx,3A4 |该寄存器指定Codpage,也就是代码页,转换编码全靠这个参数
$+25 | mov dword ptr ss:[rsp+28],100 |多余的参数压入堆栈,这个地址存的是缓存字符串个数,和r9保持一致就行
$+2D | mov qword ptr ss:[rsp+20],rax |这个堆栈地址存的是,缓存的地址,记得是缓存地址,也就是这个函数处理过后的结果,保存在哪里
$+32 | call qword ptr ds:[<&MultiByteToWideCha |你们也注意到了,r8本来用来指定要处理的文本地址,但是我之前的操作并没有使r8寄存器里面的数据有任何改变,所以这里我不动
$+38 | call mikumikudance.13FEE8099 |再次申请一个新的空间,为了第二次调用MultiByteToWideChar做准备
$+3D | push rax |再次把得到的新空间地址 保存到堆栈
$+3E | mov r8,qword ptr ss:[rsp+78] |这次r8我手动赋值了,rsp+78从哪里来的?看到我开头那一大串push里面,最后一个是push rdx没,就是从这里来的
$+43 | mov r9,100 |同上一次调用
$+4A | mov edx,1 |同上一次调用
$+4F | mov ecx,3A4 |同上一次调用
$+54 | mov dword ptr ss:[rsp+28],100 |同上一次调用
$+5C | mov qword ptr ss:[rsp+20],rax |同上一次调用
$+61 | call qword ptr ds:[<&MultiByteToWideCha |同上一次调用
$+67 | mov rcx,qword ptr ss:[rsp+98] |根据我之前的分析,rcx存的是句柄,那这个rcx的句柄从哪里来?还是看我之前的push,倒数第二个push,我把原来的句柄保存到堆栈了,就是为了这里来调用的
$+6F | mov r8,qword ptr ss:[rsp+8] |记不记得我第一次使用子函数后那个push rax,那个申请到的地址被我用在这了
$+74 | mov rdx,qword ptr ss:[rsp] |第二次使用子函数后push rax,申请到的地址用在这
$+78 | mov r9d,40001 | ;40001:"ctx "常数40001,没啥好解释
$+7E | call qword ptr ds:[<&MessageBoxW>] |因为我用MultiByteToWideChar把字符串转换成宽字符,因此调用的信息窗口也应该是能处理宽字符的MessageBoxW而不是MessageBoxA
$+84 | add rsp,A0 |处理完后,交还堆栈空间,这个40啊,是看你要从堆栈的哪里开始恢复数据到寄存器的,简单来讲,最开始先给寄存器来张截图,然后根据截图的数据来观察堆栈选择?
$+8B | pop rdi |
$+8C | pop rsi |
$+8D | pop rsp |
$+8E | pop rbp |
$+8F | pop rbx |以上都是从堆栈恢复数据到寄存器,每恢复一个就交还一层堆栈,没啥好讲的
$+90 | ret |最后一个pop后,堆栈应该指向的是一个return地址,不是如此的话,就应该检查检查了
好,这段代码写完以后,我们来检查一下效果
恩,非常好,没问题了,剩下的就是把程序里面,几百个MessageBox都替换成指向我写的这个代码就行了,你们一定会吐槽,全部替换?傻么,干啥不用Hook,哎,不提了,多说都是泪,技术不过关
你们以为,上面那个信息窗口处理完,就完结了么?NONO,事实完全没有这么简单,我们来看下一个问题
先阐述一下问题吧,
有个功能叫做附件,用来给模型人物添加各种物品,如今呢,这个功能哦
我把这货弄成正常显示的日文以后,反而不能用了
让他保持原来的乱码呢,
却能正常的使用,你说气不气人?
好吧,我们着手解决这个问题吧,首先,我们想,既然乱码能用,日文却不能用,想必有一套文字对比系统,怎么对比的?乱码跟乱码对比,废话,其实也不完全是废话,这里其实我在问的是乱码的来源问题,我认为一部分乱码来自外部加载的模型文件里面,然后还有一部分乱码来自我们在下拉框里面选择的字符串,从我们选择的字符串中,提取字符串,然后跟原文件里面的字符串进行对比,对比一致就通过执行,不一致就免谈,显然两边都是乱码当然能对比通过
那么我们该怎么解决这个问题?
无非就两个途径,一个是把源文件里面的乱码变成日文,还有个就是把我们好不容易弄成正常日文字的字符再弄成乱码,只不过表面上显示的是正常的字,后台处理用乱码
前者明显不可行,动源文件就是作死行为(其实也不是源文件,而是源文件映射到内存的映射文件,不过即便如此,也不能这么做,很危险,会导致各种意想不到的后果,甚至程序崩溃)
好,我们选择后面的解决方案,那剩下一个问题就是,软件通过什么API获得我们选择的下拉框字符串?
我想当然地认为是Getwindowstext,因为这个API就是用来干这个事情的啊,而且事实上,这个程序的输入表里面,也确实有这个API啊,然后在我给所有的Getwindowstext下断以后,发现怎么也断不下来,我知道我错了,获得下拉框字符串另有API,是啥?GetWindowLongPtr?GetClientRect?试着给许许多多API下断都无结果以后,万籁俱灰的我,给GetDlgLtem下断(一个仅仅用来返回句柄的函数,你们可能要问,之前为什么不直接给这个函数下断,非要等到现在?因为如果直接bp GetDlgLtem的话,那几乎是不管什么操作都会断下,用到这个函数的地方实在是太多,所以只能给程序里面的call下断,但是程序里面call这个函数的地方也是以百来记的,所以是吧,实在无计可施才出此下策),我倒是要看看,到底是何方神圣用来获得下拉框里的字符串的,结果你知道怎么样?
是的你没看错,经过反反复复的确认以后,已经可以肯定,就是SendMessage这个本应该用来将指定的消息发送出去的函数,在这里却用来负责接收下拉框里选择的文字,send这几个字是如此的富有欺骗性,要不是事实就摆在我面前,我怎么能够想到,前面背负send这个单词的函数名,竟然还负责接收这个差事啊,微软你坑爹啊
我含着眼泪分析到底是哪个参数的改变,导致这个本应该用来发送信息的函数变成接收信息的,其实也没啥好看的,edx后面跟着的那个大大的148已经说明问题了,当付与edx数值148后(对应的定义是wParam)该API就能从句柄指定的下拉框获得指定的字符串到r9(定义是MSG,也就是本来用来存信息的地方,只不过现在拿来当缓存用),下拉框有这么多条,那又是哪个参数用来指定获得的文字呢,唯一剩下的r8寄存器挺身而出(IParam),r8为0就代表是下拉框第一条,以此类推
知道是哪个API作怪以后,事情就好办了,写代码呗,怎么写?
我们的目的变了,我获得的是正常的日语文字,但是我在这里要让之变成乱码,并要用字符串,而不是宽字符返回,也就是说,我需要用两个API来变换,哪两个?一个是经常用的MultiByteToWideChar,还有个便是提到过一次,但一次都没有用过的WideCharToMultiByte(宽字符转变成字符串)。好,我们开始写自己的代码吧
这次代码在申请缓存空间上面,没有调用子函数,请注意观察
[Asm] 纯文本查看 复制代码 $ ==> | push rbx |
$+1 | push rbp |
$+2 | push rsi |以上是经过观察以后,发现源程序里面,经过SendMessageA并没有变的寄存器,所以保存下,没啥好讲的
$+3 | push rcx |
$+4 | push rdx |
$+5 | push r9 |
$+7 | push r8 |以上是需要用到的寄存器数值,相应的也保存下
$+9 | sub rsp,40 |例行公事,申请40h的堆栈空间
$+D | call qword ptr ds:[<&SendMessageA>] |让程序先调用一次SendMessageA来获得下拉框选择的字符串,并输出获得的字符串到r9,简单的讲,就是让本应该执行的操作先执行一次
$+13 | sub rsp,400 |这里开始请注意,我申请了400h的堆栈空间,这是很大的一块空间了,表明,我打算把堆栈空间当内存空间来给MultiByteToWideChar当缓存用
$+1A | lea rax,qword ptr ss:[rsp+40] |汇编指令lea指令,功能就和C语言里面的指针指令“&”一样,取地址,我这里就取的是rsp+40这个堆栈空间的地址,说明我打算把从这里开始的堆栈空间当做给MultiByteToWideChar的缓存用
$+1F | mov r8,qword ptr ss:[rsp+448] |r8用来存输入的文本地址,该地址在哪?看到开头我Push r9没,就在经过SendMessageA处理后,之前r9寄存器的数值也就是rsp+448(我申请了400h的堆栈空间才导致该地址这么偏远的)里面就存着我想要处理的文件的地址
$+27 | mov r9,100 |读取字符串数,没啥好讲
$+2F | mov edx,1 |Options
$+34 | mov ecx,3A8 |代码页Codepage,我要把日语变成中文系统下面才显示的乱码,当然应该用中文编码,也就是3A8
$+39 | mov qword ptr ss:[rsp+28],100 |缓存字符串数
$+42 | mov qword ptr ss:[rsp+20],rax |我们刚才把从堆栈得到的地址给了rax,因此rax就保存了缓存空间的地址
$+47 | call qword ptr ds:[<&MultiByteToWideCha |这里就把字符串变成了宽字符,紧接着就要把宽字符变成字符串了
$+4D | lea r8,qword ptr ss:[rsp+40] |再次取得地址,经过上面那个API后,有没有发现堆栈突然大变了下?那就是处理后的宽字符被保存到了堆栈,于是我们再次用lea从堆栈取得宽字符串的首地址,保存给r8,用来进行下一阶段的字符串处理
$+55 | mov r9,100 |用来指定从r8宽字符读取的字符串数
$+5C | xor rdx,rdx |options
$+5F | mov rcx,1 |还是指定代码页,上面一次转换,已经把日文字变成了乱码,这里用默认,也就是1,所谓的默认,就是系统默认语言
$+66 | mov qword ptr ss:[rsp+38],0 |pDefaultCharUsed不用管,给0就行
$+6F | mov qword ptr ss:[rsp+30],0 |pDefaultChar同上
$+78 | mov qword ptr ss:[rsp+28],12 |指定保存多少个字符串(我之前转换了100个宽字符,总不能这100个全部转换成100个字符串吧,挑有用的前几个就行了,我仔细观察了下最多需要12个就够了
$+81 | mov rax,qword ptr ss:[rsp+448] |有始有终,我从r9获得字符串,当然应该回到r9里面去,不然接下来程序要对比字符串从哪里找呢?
$+89 | mov qword ptr ss:[rsp+20],rax |输入缓存地址,也就是用来保存结果的,保存到r9里面,你们应该看得懂吧,开头我push r9把r9数值压入栈,然后程序到这一步,r9的数据在堆栈rsp+448里面,因为mov的指令长度不能直接从一个堆栈传到另一个堆栈,因此我用rax寄存器做过渡
$+8E | call qword ptr ds:[<&WideCharToMultiByt |
$+94 | add rsp,458 |开始还原寄存器
$+9B | pop rcx |
$+9C | pop rsi |
$+9D | pop rbp |
$+9E | pop rbx |
$+9F | ret |返回,没啥好讲
大家也注意到了,之前我申请缓存空间用的是call系统API——operator new,让系统为我申请一段空间,而这里我用的是申请一段很大的堆栈空间,然后就直接用堆栈空间来当数据缓存使用了,难说哪种方法好,哪种方法不好吧,前面那种毕竟安全,系统为你申请的空间,你再怎么操作都不会影响到其它地方的,不过如果需要call的次数比较多的话,很容易出现程序效率过低的情况,毕竟每Call一次都要重新申请一次,如果程序瞬间call上百次,每次都申请,想必运行效率绝对会不理想
而后者呢,用堆栈空间充当缓存,虽然可能会不保险(对堆栈操作我一向来很小心翼翼的,因为一旦搞砸,程序必将崩溃,只能重头来过),但是执行效率上面绝对比上一种方法要高,这里也引出一个老生常谈的问题——优化,其实不管是游戏优化还是软件优化,都是从这些小事情入手的,因为不管多么小的事情,被运行千百次以后,都会变成大事情,比如对我这里来讲,用前一种方法,虽然保证了程序的稳定性,但是在打开该程序某些窗口的时候,明显有卡顿的感觉,用后一种在平常使用的时候没问题,但是在加载粒子特效后,会导致程序假死(当然我这里夸大其词了,使用堆栈空间来充当缓存只要操作过之后,有好好的还原堆栈空间,就不会导致程序不稳定啥的,这里只是举个例子)
恩,很好,显示的是日语,功能也能正常实现,非常满意,你们以为酱紫就真的完事了?其实这个程序不简单
来让我阐述一下,最后一个问题吧,这个问题结束以后,该程序的修改工作也就告一段落了
还是下拉框的问题
在下拉框里面,选择一个选项以后,点下范围选择,对应的地方就应该被选中
但是,现在不管是日语字,还是乱码字,点了都没反应
解决这个问题,发生在上个问题之前,也就是说,我在解决这个问题的时候,并不知道SendMessage也能获得下拉框的字符,因此我在下断的时候,下在Getwindowstext,很容易就断下来了,我就说么,Getwindowstext就是用来干这种获得下拉框文字的活的,用SendMessage获得字符串都是邪道,哼。
跟上面那个问题一样,获得的是正常的汉字,对比的却是乱码,因此编写的代码也跟上面的一样,不需要改,唯一需要想的,只有Getwindowstext这个API的在64位下各个寄存器的参数了,例行公事,查定义呗
Int GetWindowText(HWND hWnd,LPTSTR lpString,Int nMaxCount);
吼,就三个参数,句柄,接受文本的地址,以及最大接受字符数,对应的寄存器呢
[Asm] 纯文本查看 复制代码 $ ==> | mov rcx,rax |
$+3 | mov r8d,14 |
$+9 | lea rdx,qword ptr ss:[rbp+540] |
$+10 | call qword ptr ds:[<&GetWindowTextA>] | ;
有了经验以后,这种只有三个参数的可以直接判断了,rcx存的是句柄,上面多少API都是拿rcx存句柄我就不讲了,以后都可以直接说,rcx就是用来存句柄用的
r8用来存最大接受字符串数的,毕竟给了个常数14,剩下rdx存的是用来保存字符串的缓存地址了,用的lea取得堆栈地址,我上面写的代码才刚刚用过这个方法233
[Asm] 纯文本查看 复制代码 $ ==> | push rbx | ;
$+1 | push rbp |
$+2 | push rsp |
$+3 | push rsi |
$+4 | push rdi |
$+5 | push rdx |
$+6 | sub rsp,40 |
$+A | call qword ptr ds:[<&GetWindowTextA>] |
$+10 | sub rsp,400 |
$+17 | lea rax,qword ptr ss:[rsp+40] |
$+1C | mov r8,qword ptr ss:[rsp+440] |
$+24 | mov r9,100 |
$+2B | mov edx,1 |
$+30 | mov ecx,3A8 |
$+35 | mov dword ptr ss:[rsp+28],100 |
$+3D | mov qword ptr ss:[rsp+20],rax |
$+42 | call qword ptr ds:[<&MultiByteToWideChar>] |
$+48 | mov r8,qword ptr ss:[rsp+20] |
$+4D | mov r9,100 |
$+54 | xor rdx,rdx |
$+57 | mov rcx,1 |
$+5E | mov qword ptr ss:[rsp+38],0 |
$+67 | mov qword ptr ss:[rsp+30],0 |
$+70 | mov dword ptr ss:[rsp+28],8 |
$+78 | mov rax,qword ptr ss:[rsp+440] |
$+80 | mov qword ptr ss:[rsp+20],rax |
$+85 | call qword ptr ds:[<&WideCharToMultiByte>] |
$+8B | add rsp,448 |
$+92 | pop rdi |
$+93 | pop rsi |
$+94 | pop rsp |
$+95 | pop rbp |
$+96 | pop rbx |
$+97 | ret |
从上面照抄下来的代码段,只有SendMessageA换成了GetWindowTextA,我在这里就不再重复讲一遍各个指令是什么意思了,快速进行下一个阶段吧。
满怀欣喜的去测试该问题解决了没有,发现这个问题就解决了一半?为啥这么说呢
请看
刚才那个下拉条里面选择项目的话,红色框里面是程序自带的,红色框下面是模型文件载入后才显示的
现在的情况是,我把上面的代码写完,并且替换原本的call以后,红色框以内的项目,也就是程序自带的这些字,没有效果,而其他的,却能正常使用,于是我就愁了
这是闹哪样啊喂
我认认真真的反复调试,看内存里面的数值变化,字符变化等等,半天以后,终于发现了问题所在
对于红框以内的字符,在经过GetWindowTextA以后,只有中文汉字被正确读取了,也就是说第一个选项,GetWindowTextA这个API只读取了“全“,后面的那些日语全部变成了“?”号
之后的那些字符也是,经过GetWindowTextA以后,都只有汉字得到了识别,日语字符全部消失(注,是经过GetWindowTextA后变成这样的,并没有经过我写的代码,后来我仿照之前用SendMessage获得下拉框文字的方法,自己再写了个call来获得字符串,发现依旧不能获得日语字)
奇怪的是,红框之外的日语字,GetWindowTextA却能正常识别,我实在是对这个问题没辙,但是考虑到,起码前面还有汉字能够识别,我能不能从这些能够识别的汉字入手,让程序就认这些汉字,不管后面的日文字就知道我进行的是什么操作?(也就是说,让程序只对比前面一小部分的字)
于是,问题的关键就变成了,程序在得到这些字以后,是在哪里进行的对比工作?
于是我给这个call下了断,然后按F8步过跟着。不久就看到了
[Asm] 纯文本查看 复制代码 $-13 | lea rsi,qword ptr ss:[rbp+540] |rbp+540,这个堆栈地址眼熟不,就是经过GetWindowTextA之后,获得字符串的保存地址
$-C | lea rdi,qword ptr ds:[13F66D9C4] |从程序本体当中,获得用来做对比参照的字符串
$-5 | mov ecx,6 |给ecx赋值6
$ ==> | rep cmps byte ptr ds:[rsi],byte ptr es:[rdi] | ;没怎么见过的汇编指令,不过看到rsi和rdi,再联系之前的两条lea指令,也猜得到
$+2 | mov eax,r12d |把r12寄存器的数值给了eax
$+5 | sete al |又是一个奇怪的指令,用处就是根据flags寄存器的状态来设置al(rax的最低8位)寄存器的值
$+8 | or edx,eax |按位或操作
$+A | je mikumikudance.13F585166 |跳转就不执行接下去的操作
别看rep cmps byte ptr ds:[rsi],byte ptr es:[rdi] 这么长一条,其实输入的汇编指令只有rep cmpsb这几个字
功能是循环对比rsi地址里面的数值和rdi地址里面的数值是否相同,全部相同则标志寄存器ZF置1,
你说循环,那循环几次呢,总不能rsi和rdi之后的全部数据都循环对比吧
看到这条指令之前的mov ecx,6 没,ecx里面的数值就是循环的次数,这里给了6,也就是进行6次对比,按照我之前分析,我自然不希望这里循环对比太多次,于是我对比了经过我写的代码之后的字符和程序要对比字符的区别
[Asm] 纯文本查看 复制代码 rsi|91 53 3F 3F 3F 3F 00 33 33 5A 8B 44 66 96 E4 43
rdi|91 53 CC DA B0 D1 00 00 00 00 00 00 41 6C 6C 20
发现2个字节之后就完全不一样了,所以我这里只要把6改成2就行了
说实话,这是一条很强大的汇编指令,用这个简单的指令甚至直接取代了某些系统API的功能你们发现没,在我发现这些个神奇的指令之前,我一度以为这里的字符串对比用的是VC的字符串对比函数,也就是strncmp之类的,我都做好了,重写一个类strncmp代码的准备了,没想到,到最后却发现完全不是这样的,后面也依次使用了好几次这种功能指令,直到程序自带的这几个选项过后,才跳转到其它位置去对比模型文件的字符串,我跟着修改了几个mov ecx,5后(就是循环次数),这个问题也就跟着解决了。
呼,不得不追加一个新问题了,我是没想到这个程序这么棘手什么问题呢
按功能上来说,应该是我在下拉框选了什么,下面的输入框也应该显示什么,但是现在输入框里面却是乱码字,我首先想到,这里显示乱码很可能是我某个SendMessage忘记修改了,但是在我给每个SendMessage下断以后,依旧没有断下来,怎么回事
嘛,断不下来,我就换个方法吧,既然是下拉框选择后,那应该调用的是GetWindowsText,从下拉框获得字符串,然后再在输入框显示的,于是我给GetWindowsTextA下了断点,果真,不久就断下来了
于是我满怀欣喜把这个GetWindowsText改成调用我写的函数,然后一看,
两个字符变成两个??了,什么鬼?,这里我就意识到了,事情没我想的这么简单,大家看到上图,红色框里面,调用了strcpy,我相信学过VC编程的大家一定很熟悉,这是一个字符串对比函数,什么跟什么对比?首先可以肯定,对比之一是GetWindowsText获得的字符串,那对比的参照呢?我观察了下寄存器数据,了解到对于该函数来说R8存的是对比字符串之一,RCX存的是另一个对比字符串,R8是从GetWindowsText获得,没有疑问,但是RCX却是从一个很遥远的地方获得的内存地址,我怀疑是模型文件在内存里面的映射数据
然后我把目光转向GetWindowsText,看经过这个API后,获得数据是什么
这个字符串代表的是?号,等等?号,上面哪里出现了?号,难道GetWindowsText并不是从下拉框获得的字符串,而是从输入框获得字符串啊,好吧,这样子事情就清楚了,我猜测这里的设计意图是,不从下拉框获得字符串,而是从下拉框获得选择条目的序号,然后是用GetWindowsText获得输入框的字符串,跟模型本身的字符串进行对比,如果一致就不进行修改,如果不一致就按照序号重新设置输入框的名称,这样子,大致的思路清楚了,虽然我并不知道为啥软件作者要这么拐弯抹角设置成酱紫的
但是还有个问题要解决,到底是什么API改变输入框里面的字符串的,没有办法,我们只能单步跟踪下去
[Asm] 纯文本查看 复制代码 $ ==> | mov rdx,qword ptr ds:[rcx+A1BD0] |
$+7 | cmp dword ptr ds:[rdx+rbx+60],ebp |对比跟下拉框中选择项目的序号
$+B | jnz mikumikudance.13F9CE407 |对比通过就不跳转,否则一直循坏
$+D | mov r8d,edi |
$+10 | mov rdx,rsi |
$+13 | call mikumikudance.13F9D25C0 |改变窗体内容显示
$+18 | mov rcx,qword ptr ds:[13FA845F8] |
$+1F | mov dword ptr ds:[rcx+A1C98],edi |
$+25 | inc edi |
$+27 | add rbx,C0 |
$+2E | cmp edi,186A0 |
$+34 | jl mikumikudance.13F9CE3E2 |全部序号都对比过,依旧没有找到,跳出循坏
跟着跟着,就来到了这个地方,一个循环,循环的目的是,如果上面调用strcpy字符串对比不通过的话,根据你之前下拉框选择项目的序号用循环找到对应的序号的内容,如果找到了,就Call循环体中间那个函数,来修改窗体内部的文字显示,那么这个call就是关键了我想,修改输入框字的函数也在该Call里面了
一进这个CALL,满眼都充斥着SetWindoTextA这个系统API,我想不用多说了,这个API肯定就是用来设置输入框里面文字的API不跑了
没啥好讲的,开始写代码吧,等等,各个寄存器对于该API来说的意义呢?什么都不说,先给寄存器来张截图
轻车熟路的我们,经过前几次的洗礼以后,就默认rcx里面装的是句柄了,R8?一串意义不明的东西,肯定和文本搭不着边,不管了,R9呢
很抱歉,右键以后,我连Follow in dump(跟随到内存)都没看到,十有八九没有字符串在里面,因此我们只要记得等会让自己写代码的时候,调用R9就行了,不用管里面是啥
那么,Rdx里面,就存着我们需要处理的文字了,好,开始写吧,
跟我之前写的SendMessage差不多
[Asm] 纯文本查看 复制代码 $ ==> | push rbx |
$+1 | push rbp |
$+2 | push rsp |
$+3 | push rsi |
$+4 | push rdi |以上就是将各个经过call以后不应该改变的寄存器数据保存到堆栈里面
$+5 | push rcx |
$+6 | push rdx |
$+7 | push r8 |
$+9 | push r9 |这是个寄存器里面的数据,在之后的call要用到,所以也保存一份到堆栈
$+B | sub rsp,200 |申请200的堆栈空间(记得在保存寄存器的数据后再申请,切勿申请堆栈后再保存寄存器数据,不然就白保存了)
$+12 | lea rax,qword ptr ss:[rsp+40] |获得堆栈rsp+40的地址,用来给之后的字符串转换API当缓存用
$+17 | mov r8,rdx |之前分析rdx里面装的是我们要处理的字符串,因此把rdx里面的地址给r8
$+1A | mov r9,50 |读取50个字节的字符串
$+21 | mov edx,1 |Options,不用管
$+26 | mov ecx,3A4 |Codepage,字是日语字,当然用日语编码
$+2B | mov dword ptr ss:[rsp+28],100 |存100字节的宽字符
$+33 | mov qword ptr ss:[rsp+20],rax |将缓存地址存入堆栈的这个位置
$+38 | call qword ptr ds:[<&MultiByteToWideCha |
$+3E | mov r8,qword ptr ss:[rsp+208] |从开头保存到堆栈的数据当中重新获得r8的数值,以下类同
$+46 | mov r9,qword ptr ss:[rsp+200] |
$+4E | mov rcx,qword ptr ss:[rsp+218] |
$+56 | mov rdx,qword ptr ss:[rsp+20] |将处理过的字符串地址赋值给rdx
$+5B | call qword ptr ds:[<&SetWindowTextW>] |因为已经变成了宽字符,所以这里调用SetWindowTextW,而不是SetWindowTextA,我想这里没啥好解释
$+61 | add rsp,220 |认真根据call之前寄存器的截图,以及知道自己接下来需要从堆栈还原哪些寄存器的数据,来选择rsp的地址
$+68 | pop rdi |
$+69 | pop rsi |
$+6A | pop rsp |
$+6B | pop rbp |
$+6C | pop rbx |还原现场
$+6D | ret |返回
很好,最后看下效果
很好,又一个问题得到了解决
在此说一个让我很无语的X64bdg上面的Bug,怎么一个Bug呢,就是说哦,我打一个call系统函数的汇编指令,比如call qword ptr ds:[13FD687B8]是吧,然后输入后自动变成了call qword ptr ds:[13FD687B7],于是,如果你想得到自己想要的call,必须要输入call qword ptr ds:[13FD687B9]你说,这个Bug无语吧
终于,这个仅仅只有1.64MB的64位软件总算修改完毕了,别看我轻描淡写,写了几行汇编,然后替换到源程序里面的call就完事了,但是找到程序实现各种字符串处理的call,以及想办法处理这些异常字符串,以及之后的想解决方案,测试自己编写的汇编代码错误,等等工作都是很费时费力的,尤其是在完全不懂的64位平台上面更是如此了,嘛,我在文章的一开头也说了【并以此来提升自己在64位反汇编上面的技术】
因此虽然这一整周一直都在处理这个软件,但其实我是很开心的,一点都不觉得腻,这就像是一个小孩纸找到了自己心爱的玩具一样充满好奇心和兴趣,因此我在保存完程序以后,就迫不及待的来这里分享经验,说难听点,就像是小孩纸在炫耀自己的玩具一般233,
嘛,未来必将步入64位的年代,64位在底层汇编上面的效率,尤其是大数据下面的处理效率不是32位能比的,仅此希望,我的这篇充满瑕疵的文章,能为今后的64位汇编以及反汇编带来贡献。
以上嘛,最后我把修改后的程序给出来吧
链接: http://pan.baidu.com/s/1ntOgSlj 密码: 6ux9
|
免费评分
-
查看全部评分
|