【翻译】R4ndom破解教程全文翻译(For新手):第十章
本帖最后由 yysniper 于 2015-5-17 17:16 编辑翻译说明:
1、本教程在52破解论坛及看雪论坛全球同步首发!
2、本教程翻译自国外的The Legend of Random的系列教程,英文原文地址:http://thelegendofrandom.com/blog/sample-page。本翻译教程只是为了给不愿看英文教程以及英文水平不好的人提供方便,同时也是自己的学习过程。该教程对英文水平要求不是很高,不过个人水平有限,有些地方翻译不准的请批评指正。
3、本翻译教程请勿用于商业用途。另,转载请注明!!!
4、感谢The Legend of Random!
发帖说明:
根据译者的时间安排,快的话2-3天会上传一章,慢的话就不造了。如果有段时间没更新请勿怪,因为译者的工作性质,有时是接触不到网络的。
其他章节:
全系列章节导航帖
第十章:打补丁的层次级别
一、简介本章我们会讨论给二进制文件打补丁的不同的层次级别。本章有点长而且详细,涵盖较多的背景知识,有些还不简单。我想给你展示一个深入分析二进制文件的例子,以及它需要什么。你可能大部分都不能够理解,不过它会给你一个非常好的总览逆向工程的一个好的机会。这样在将来的教程中,你会有一个参考框架。我们用上一章的那个crackme来研究,就是“TDC”写的Crackme6,相关下载中包括的有。你可以在教程页现在相关文件和本文的PDF版本。总之,从上一章我们就知道这个crackme不是一个硬骨头,不过这里我打算对它做高级分析,也为将来的教程做准备。现在坐好,准备一杯咖啡/香烟/巧克力棒/注射器,任何能让你坚持下去的东西都行,那咱们开始了......。二、破解的等级逆向工程领域(尤其是破解)中关于打补丁的不同级别有几个不成文的规则。基本上可以分为四级(我保证,至少有一半的逆向工程师会因为那个数字和我吵起来)。当然,因为缩写神马的听起来都不错,所以我给四个级别的每一个都想了一个缩写。事不宜迟,下面就是补丁级别的介绍及具体意义:级别一:LAMELAME方法,就是Localized Assembly Manipulation and Enhancing,这个方法目前我们已经学习过。意思是找到代码中的第一个魔术 比较/跳转 指令,然后将其NOP掉或强制它跳转。到目前为止,这个方法都很神奇的好用。当然,我们都是在简单的crackme上做实验(有一半都是我专门为教程写的)。不幸的是,外面的大部分应用都不会这么简单。用LAME方法,有许多东西都会出问题,包括:1、许多许多的应用会在程序的不同地方对程序是否已经注册进行检测,所以如果你仅仅打了一个补丁的话,并不意味着就没有其他的地方需要打补丁(我想我见过最多的分布检测点是19个)。并且有时候这些其他的检测点并不会起作用,除非某些特定的事件发生,所以你会发现自己又得回头对同一个程序进行搜索,以找到替代检测点并打补丁。2、许多程序也会采用多种特别的技巧以避免 比较/跳转 指令组合的暴露。无论是在DLL中执行、在另一个线程中执行还是以多态的方式修改,都有许多种方法来实现。3、有时候你将会修补大量的代码。你有可能会给七个检测点打补丁,将其他的检测点NOP掉等等。这会让你头昏脑涨的,而且对你来说也不是那么的优雅。4、使用该方法你不需要学习太多东西,如果你正在阅读本系列教程,很可能是因为你对相关主题感兴趣并有学习的欲望。尽管如此,有时候最优雅的解决方案,通常也是最简单的解决方案,仅仅是一个 比较/跳转 指令组合的补丁即可,所以别让我走错路并认为你不应该使用它。事实上,我逆向过的许多程序中,我猜大概有25-40%就是用像这样的一个简单补丁搞定的。所以它是一个强大的方法。级别二:NOOBNOOB方法,也就是Not Only Obvious Breakpoints方法,通常要比LAMP方法更深入一步。它通常涉及到要单步步入到 比较/跳转 指令组合的前面的那个CALL,以了解是什么让 比较/跳转 指令组合决定走这条路的。这样做的好处是,你将有更多机会捕获到调用相同方法进行注册验证的其他部分代码,所以给一处打补丁就可以真正的补好几处,也就是所有调用相同注册验证方法的那几处。当然,该方法也有几个缺点,比如:1、有时该方法用于超过一个注册验证的程序。例如,有一个用于比较两个字符串的通用函数,它返回真或假。在我们序列号匹配的案例中,这就是打补丁的地方,不过同样的方法被调用以比较两个不同的字符串,并且我们已经将其打过补丁以让它始终返回true(或者视情况也有可能是false)结果会怎样?2、该方法需要更多的时间和实验,以判定能够返回正确值的最好选择是什么。这个需要时间和技巧。这是本章中我们将会用到的第一个方法。级别三:SKILLEDSKILLED方法,也就是Some Knowledge In Lower Level Engineered Data方法,和NOOB方法有点像,除了它需要你仔细审查程序并且将其完全逆向以研究到底是什么情况。这样做有许多好处,比如理解所使用的任何技巧(像在内存中存储变量以便于后面获取),提供更多的打补丁的地方以更简单并且少侵入,从内部了解程序是如何工作的。它也给了你作为一个逆向工程师在将来会用到的许多知识,更不用说你的汇编语言技能。该方法的主要缺点是,它更难并且需要更多的时间。我建议你至少找几个程序试试这个方法,因为没有什么能够比花时间深挖代码、堆栈、寄存器以及内存能够让你成为更好的逆向工程师,尝试去感受下作者曾经试过的。本章的最后我们将会用到这个方法。级别四:SK1LL$思考下破解的圣杯,Serial Keygenning In Low-level Languages, Stupid 意味着你不仅要仔细研究并且准确找出注册进程是如何执行的,还要重建它。这就得能够让新用户随意输入任何用户名,然后keygen者的代码能够算出对于该二进制文件管用的序列号。制作一个keygen的通常的方法是用程序自身的代码来对付它,意思是拷贝作者用来解密序列号的代码并用它来进行加密。这些代码通常放置在某种专门用来接收被拆分的代码的程序中(它提供有GUI等类似功能)。skill$(译者注:小标题中的第三个字母为1,这里又变成i,我不知道是不是作者故意的)的最高境界是,如果不能够从应用中提取代码,就必须自己编写代码来提供可用的序列号。意识是你必须完全理解程序是如何解密序列号并将其与你输入的进行对比。你必须自己写程序来完成相同的功能,仅在逆向领域中,有很多次是用汇编语言写的。很明显,该方法的主要缺点是skill$的复杂难懂。那么,鉴于我们对逆向工程级别的新的理解......三、用级别二来研究应用程序重启应用并运行。给GetDlgItemTextA设置断点(参见上一章),输入密码(我输入的是“12121212”)然后点“Check”,Olly就断在了GetDlgItemTextA:(p1)咱们来看看GetDlgItemTextA:(p2)需要重点注意的是:其中一个参数是一个指向缓冲区的指针,该缓冲区是用来存储密码的(lpString)。返回值保存在EAX中,它保存的是字符串的长度:(p3)你在40125A处看到那个指向字符串缓冲区的指针,它是40205D(Olly加了一个注释“Buffer=”,因为它能够猜测参数。译者注:作者写的是40205D,不过看图片实际上是40305D,估计作者弄错了)。意思是该函数会拷贝我们的对话框文本到一个以40305D开始的buffer中,将返回的字符串的长度保存在EAX中。所以,在本例中,我们输入的密码“12121212”将被获取到,返回的密码长度保存在EAX中,这里是8。现在,如果你看接下来的两行,你会发现这个值与0x0B(十进制的11)进行比较,并且如果EAX比它小的话程序就会跳转。真正的意思是,如果我们的密码长度(EAX)小于0x0B(11个数字)就会跳转。注意如果我们不跳的话,我们就会直接到坏消息那,所以实际上,这就意味着我们的密码长度必须比11小:(p4)看吧!!咱们已经了解了一些东西了,我们的密码最多只能有11个数字(译者注:这里作者弄错了,上面一段已经分析了密码必须比11小,也就是说密码最多只能有10个数字)。因为我们的密码少于11个数字,所以咱们继续并让跳转实现。(如果你输入的密码大于11个数字,重启应用然后再输入一个小于11个数字的密码,再单步到我们所在的位置。)(p5)接下来你注意EAX的值,它仍然保存着密码的长度,并被测试是否为0,如果不是0的话,就跳过第二个坏消息。那么现在我们知道了第一个坏消息是当我们的密码长度小于11时显示,第二个坏消息是在密码为空的时候显示。注意,在跳转实现的接下来的两行,是从401282开始的,PUSH EAX(密码长度),地址40305D(存储密码的buffer)入栈。看看堆栈,可以看到确实如此:(p6)首先要注意的是(在地址18FAB0处)是长度(8)被压入栈,其次是在地址18FAAC处的地址40305D被压入栈,Olly也向我们显示了“12121212”也就是我们的密码。现在我们知道了,我们的密码是存储在内存的40305D处。这一点在后面会很重要。后面,Olly会将这两个值叫做ARG.1和ARG.2,因为它们是传递给函数的参数。这两个值入栈以后,我们就可以调用401298处的主要注册程序了(我们之所以知道这个,是因为所有重要的 比较/跳转 指令组合的前面都有个CALL,所以它的结果将决定我们是跳到好消息还是坏消息):(p7)让Olly就暂停在CALL那行,不过要注意CALL后面那行,40129D处指令对EAX自身做了OR操作(该操作会根据EAX是否为0来设置0标志位),如果EAX不是0的话就会跳过好消息。这就意味着将在401298处调用注册程序,并在某个时刻在EAX中保存一个值并将该值RETN。返回值将会被检查是否为0,如果不是就显示坏消息。所以我们必须保证在这个CALL中,当它返回时EAX等于0!!如果我们能够做到的话,它就是我们需要的唯一一个补丁(密码被限制在0到11个数字之间也算一个,不过那是一个简单的补丁)。咱们继续,单步步入到401298处的注册程序,总览一下:(p8)哇噢,看起来不少呢,尤其是你很可能对汇编语言只是个半吊子file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml\wps4D00.tmp.jpg。但也不是不可能。我常用的招是到程序的最后面,我们知道它返回时EAX必须等于0,看看是什么完成了这项工作以及是什么阻止了它发生,然后再回头用我们的方法。向下滚动直到你看到函数的RETN指令:(p9)这里,我们可以看到,我们肯定是想要在函数返回前避免401510处的指令将EAX的值设置为1。你可以看到有一个红色箭头指向该行(译者注:不是作者加的箭头,是那个细线的小箭头,在指令的边上),所以该跳转也需要被干掉。现在如果我们向上看看,我们能够看到EAX被设置为0的地方,也能看到函数底部将其返回的路径:(p10)如果我们看看4014FB那行,EAX将会被置0(对自身做XOR操作),跳转指令将会跳过401510处的坏消息指令,相关的执行流将会返回EAX值为0。现在我们跟一跟我们看到的第一个跳转(也就是会跳到401510处MOV EAX,1这个坏指令的跳转),看看从哪跳过来的:(p11)40147C就是那个坏跳转。我们想要阻止它跳,否则我们肯定会得到坏消息。好的,我们现在已经有了关于这段程序的基本的知识,对于级别二的破解我们就到这里,现在来打个补丁确保EAX总是返回0。你会怎么做呢?我准备将它留给你来做(这是本章结尾的作业)。放心好了,我会给你答案的...。不过你要明白这个级别的补丁已经开始时的补丁要好很多,一是我们只打了一个补丁,二是如果这段程序被应用中的其他部分调用的话,我们仍然能够获得好消息。现在,先停一会,考虑考虑你怎么来打这个补丁。记住,EAX必须返回0。我让你做的原因是,有很多很多NOOB补丁可以完成这个任务,我想让你开始像一个逆向工程师那样思考!如果你需要提示,那就看看结尾的作业那一块。如果你能够解决,那你就是一个真正的NOOB!!!当你完成时,就准备转到更详细的分析,继续阅读吧......。四、步入到级别三我知道你仍然是一个初学者,不过我还是想让你尝尝更深层次补丁的感觉。如果你还没有准备好,或者完全失败了,也别气馁。这里只是给你一个想法。我们会在将来的教程中学习这一块的所有东西。你可能会问,更加深入代码的目的是什么,应用程序中调用这段程序的每一个地方都会被打上补丁吗?好吧,对于新手来说,假如有不同程度的注册会怎么样,比如“Private”、“Corporate”, “Enterprise”...。程序可能会根据内部的逻辑来做决定。另一个你想要更深入学习的原因是,为它做一个keygen。你需要理解代码才能做。现在,咱们开始打一个SKILLED级别的补丁。回到上面程序(译者注:这里的程序是指那个验证函数,本章大部分都是这个意思,读者应自己做分别)的起始处,实验一下:(p12)首先,那有一些典型的寄存器的压栈操作以及在栈中为本地变量开辟空间的操作。ECX和EDX中的值被压栈,然后我们就可以在不用覆盖这些寄存器的情况下使用它们(函数返回时会将这些值出栈以将它们还原。译者注:这就是传说中的堆栈平衡,脱壳中ESP定律的原理)。然后我们就到了40142A,这里将栈中(我们输入的密码的地址)的本地变量拷贝到EAX中。如果你看寄存器窗口就会发现EAX的值是地址40305D,也就是我们密码所在的地址。下一行代码:
XOR DWORD PTR DS:, 1234567
该行的意思是,将ECX(它的值是0)和我们的密码首地址(密码存储在40305D,记不记得?)相加,然后从该位置取DWORD(4字节)数据与十六进制的1234567进行XOR操作。因为ECX是0,将其与我们的密码首地址进行相加不会有任何的影响,所以我们从密码的第一个数字的地址开始处理就行。简单点来说,这行代码的意思是“取密码的前四个字节与1234567进行XOR操作,将新值存储到内存中与我们密码同样的位置”。我们可以观察到这一过程。首先,要确保我们依然暂停在40142D那行,看数据窗口的上面那一块,它会告诉你地址ECX+EDX(40305D)是什么,以及它存储的值是什么(32313231),用ASCII码表示就是“2121”(要记得数据存储序列):(p13)现在选中“DS:=32313231”这行,右键选择“Follow in dump(数据窗口中跟随)”,然后我们就能看到我们密码当前存储的实际内存内容:(p14)现在,数据窗口显示的就是从40305D开始的内存内容。前面8个字节就是我们的密码。记住,我们当前所在的行正准备取该地址的前四个字节(31,32,31,32),然后与0x1234567进行XOR操作,之后再将结果存回到该内存区:(p15)咱们继续,单步步过一次,然后你就会看到我们密码的前四个字节已经变了,和0x1234567进行XOR的结果:(p16)好,继续下一行代码:(p17)该行是 AND BYTE PTR DS:, 0E。我们已经知道了ECX+EDX的结果是40305D,也就是我们以前密码的地址。现在,我们准备按BYTE与0x0E进行AND操作,并将结果存回该地址。这意味在,我们存储在40305D的以前的密码的第一个数字(译者注:这里我感觉作者的表述不太准确,因为存储在40305D的是XOR后的数据,早不是我们输入的12121212了,大家要注意,仔细观察数据区。)在与0E进行AND操作后再存回第一个位置。看看数据窗口的帮助区域已经显示出来了:(p18)它告诉我们将受到影响的地址是40305D,该地址的(当前)值是56。继续,单步执行一次,你会发现第一个数字又变了:(p19)现在咱们知道了0x56与0x0E进行AND操作的结果是0x06。咱们继续跋涉这段困难的代码:(p20)ECX增加了4(指向下面的四个字节),并和8进行比较。意思是这个循环会运行两次,第一次ECX等于4,第二次等于8,然后跳出循环。这意味着我们总共处理了8个字节。所以第二次循环时,我们将影响第二个4字节,也就是将它们与0x1234567进行AND操作。你在单步运行时,注意观察第二个4字节:(p21)它们也会被修改。第五字节也会被再次修改,因为它将与0x0E进行AND操作。循环结束后,下一条指令也就是401440处的指令仅仅是将ECX值置0:(p22)现在咱们来看看接下来的几条指令:(p23)首先,我们将我们(旧)密码的第一个(新的)字节拷贝到DL中(因为ECX再次被置0,所以我们正在处理的是第一个数字,也就是EAX当前指向的数字)。如果你看寄存器窗口,你会发现第一个字节(0x06)在EDX寄存器中:(p24)然后我们将DL中的数字与EAX+8中的内容相加,也就是EAX的第八个字节,并将结果存回第八个字节:(p25)这里,我们能够看到那个字节被修改了:(p26)它是将buffer中的第一个字节(6)与第八个字节(0)相加,结果得6(译者注:不知道读者注意到没有,上面的图片中箭头明明指的是第九个字节好不好,那是不是作者弄错了呢?我觉得是有问题的,因为如果按从0开始数,那么第一个字节就应该是77,如果不从0开始数,那么第八个字节就应该是33。那该怎么办呢?其实不用管第几个了,我们知道定位到该字节是按EAX+8的结果算的,因为EAX的值是40305D,所以40305D+8=403065。40305D就是06那个字节,我们往右边数,数到403065,刚好就是箭头指的那个06)。如果我们密码的长度大于8,这就会让我们密码的第一个字节与我们密码的后面的那个数字相加,不过因为我们的密码只有8位,所以这块内存被设为0。接下来我们给ECX加1(因此转移到下一个字节),将其与密码的长度进行比较。这只是查看我们是否已到达结尾。如果没有,就跳转到循环的开头再执行一次。这基本上意味在,我们将循环遍历密码的所有数字,每个数字相加然后将结果存储在第八个内存位置。现在我们明白了为什么密码只能是11个数字(译者注:这里用11个数字不太准确,字符最合适),所有空间加上0终止符一共只能接受11个字符。(p27)你单步执行这个循环,就可以观察到内存的变化:(p28)在循环结束后,我们再一次将ECX设置为0,并进入了与第一个循环相似的循环中,这次将每个四字节数据与0x89ABCDE进行XOR操作。(p29)它也将所有的字节相加,再将结果保存在第九字节。这个循环将会一直执行,直到ARG.2等于0为止。ARG.2是我们密码的长度(还记不记得它是调用该函数前第二个被压入堆栈的?)所以,这些指令将会执行8次,密码中的每个数字一次。在执行完这段代码后,你就会看到最终的结果:(p30)
运行程序并观察所有的情况是非常非常的重要,因为这能让整个过程更加清晰很多。花点时间来理解每一行,看看它将做什么,它准备将结果存储在什么地方。你会发现,它其实不像听起来的那样难:)。别忘了,我们将要在40147C处的跳转做出我们的选择。下面我们总结下我们所做的:1、我们将我们的密码的每一个四字节值与0x1234567进行XOR操作,再将结果覆盖回我们的密码。2、第一个字节与0x0E进行AND操作,第五个字节也是一样。3、然后我们将所有的字节值加起来,将结果存储在第八字节。4、然后,我们再将buffer中的每个四字节值与0x89ABCDEF进行XOR操作,再将结果存进这个buffer。5、我们再一次将buffer中的内容相加,将结果存储在第九个内存位置。我们已将执行了此crackme保护机制魔法的大部分(*啧啧*)。现在我们将载入这两个值(buffer内存内容的求和),一个在EAX+8,另一个在EAX+9,分别载入到DL、DH,本例中EDX的结果就是842C。然后,我们将这两个值与42DE进行比较:(p32)为啥是42DE呢?好吧,这很可能是一个硬编码的密码。你思考下,如果你有一个特殊密码,用它来进行整个的XOR和AND操作,将得到魔数42DE。我们的例子中,EDX等于842C:(p33)我们没有输入那个魔术密码,所以我将实现该跳转,跳到坏消息代码那里:(p34)当然,除非我们给Olly帮点小忙:(p35)因为我们不空降,所以EAX的值不会被置1,并且该函数会立即终止。下一步,我们将ECX值置为9,以便于我们访问buffer的第九个数字,将第九处内存位置的内容拷贝到DL中(这里是0x2C),对其自身做XOR操作(让其等于0),ECX减1以指向前一个位置,这样做9次:(p36)你可能有点疑惑,这没有改变buffer中的任何东西呀,那这个函数有什么意义呢?好吧,咱俩都被迷惑了。看起来它所做的一切都是让DL一次又一次的等于0,这看起来几乎就是代码中一个圈套(或者是一个错误)。总而言之,不管这个代码运行还是不允许,这都没有什么不同,所以它就是死代码。我们现在来到一组短点的代码,基本上是将EAX与30AC进行比较:(p37)首先,它将我们前面求的和存在ECX中(第九处内存位置是0x2C,第八处内存位置是0x84),将其与0xEEEE进行XOR操作,再与30AC进行比较。因为ECX不等于30AC,所以我们将跳转:(p38)跳转到那里,ECX再次被置为1:(p39)这基本上就是第二个密码检测点了。一个没有多少经验的逆向工程师(或刚好将 他/她 给难住了)很可能立即就将这个JNZ给打了补丁,原因是上面将我们转换的密码与0x42DE进行了比较。他们可能没有花时间来分析其他代码,认为这个补丁就够了。不幸的是,这个补丁明显不够,因为应用程序对我们的密码计算出来的值做了其他更多的操作,并且如果与新值不匹配的话就跳转。该方法多次被用来作为一种检测技术,检测是否有人尝试给应用程序打补丁:在没有任何补丁的情况下,如果我们的密码通过了检测并通过了第一个JNZ,那我们也应该能通过第二个。如果没有,那么我们就知道有人给第一个打了补丁,所以我们就知道有人修改了代码。许多情况下,第二个跳转会跳到完全不同的代码块,有些看起来令人难以置信的复杂,但是实际上又什么都没做,最后就终止了。这是企图让逆向工程师做一些徒劳无功的事,让攻克保护机制变的更难。这不是我们想要的,所以我们设置0标志位,然后继续,我们遇到了下面两行代码:(p40)这是将我们密码buffer的第一和第二个内存内容拷贝到CL和CH,本例中是让ECX等于CB08。与3592(十六进制)相加后再与E59A进行比较。如果它不等于该值就跳转:(p41)这个和上面做的事情是一样的。完成了另一个检测,以确保我们是合法的到达此处。我们明显不想让这个跳转实现,所以我们通过修改0标志位再次的帮了Olly的忙。然后我们顺利通过了另一个检测,这个是从4014A3到4014AD。通过修改0标志位,我们也跳过了这个JNZ,最终来到了这里:(p42)第一行代码CMP DWORD PTR DS:, 7A81B008做了另一个检测。在对密码做完了所有的操作以后 ,最后第一个四字节等于7A81B008。如果不是,我们就会跳到坏消息那:(p43)所以还是0标志位来帮Olly一把,然后我们就来到了另一个检测群(为什么不呢?),首先对接下来的几个字节做了一些操作,然后将其与388DBF02进行比较,并与各种内存中硬编码数字进行比较。这个在检测上有点矫枉过正了,不过我认为作者可能觉得检测越多就越能保护好crackme。绕过所有的跳转,我们最后来到了我们想要的地方,就是那个4014FB的JMP指令:(p44)如果我们单步通过RETN,我们将来到熟悉的地方,不过这次有点儿不同:(p45)注意这次我们来到了好消息这。这是因为我们组织应用程序将EAX设置为1。现在,你可能会认为“太棒了,我们在这个新的深入分析中,在级别二层面只用了一个补丁换来了9个(被设置0标志位的所有JNZ)”,不过这却不是真正的情况。我不仅理解了它是如何工作的(并且对于将来的逆向挑战也赢得了大量经验),现在还能够打非常牢固的补丁,因为我们知道这个补丁无论在什么情况下都会起作用。有一点没有提,那就是找到这个软件的真正的密码其实不是很难,这样就绕过了任何需要打补丁的地方!这就是真正的逆向工程,它只能靠大量的练习。并且应用程序越难破解,你就越能够从代码中获取更多的细节。再说一次,如果你失败了也不要担心。这次更多的只是提供一个相关方法的使用印象。我们将会再次的学习这些内容。同时呢,这里有一些...五、作业就是教程前面提到的,看你是否能够用NOOB技术给程序打补丁。这就意味着,找到一个方法步入对密码进行所有操作的CALL,并找到一个绕过所有操作的方法。你不需要理解对密码做的所有操作,仅仅是找到一个让程序跳过它并且仍然能够得到好消息的方法。如果你需要提示,请点击这里。超级吊的加分题:你能够找到硬编码密码吗?
本文PDF文件下载(已排版):本文相关附件下载地址(国外链接,不是一直好用):包括本章英文版PDF、crackme6.exe
PS:这章9000多字,真累呀!!! 又更新了,不错,值得学习 感谢楼主分享!!! 谢谢分享 赞,中英文都下载了,谢谢 大力支持楼主的翻译,下次补分
大力支持楼主的翻译,赞 继续支持!!!辛苦了英语好就是好! 感谢楼主辛苦劳动 老规矩,看之前养成习惯--回复一下!
页:
[1]
2