头一次发帖,希望能对新手有用。文章中涉及了一点程序运行栈的知识,以及一点点VB的数据类型在内存中的存在形式的介绍。 原来从来没有接触过VB,最近工作原因需要对一个没有源代码的VB60程序进行一点儿汇编级别的小更改,发现VB是一个很有意思的语言。 工作的需求是这样的: 原来的软件里,调用了getObject函数(在msvbvm60.dll里对应rtcGetObject)。通过看别人的调用范例,发现这个函数有两个参数,都是string类型(后来发现其实是variant,这是后话,后面会提到。)。我的工作就是把调用的参数改掉。 问题很简单,我觉得很快就能够做完,可是做起来有些小地方确实耗费了不少时间。 首先,原来软件的作者加了个小壳,简单来说,就是对所有text段的内容都XOR了一个常量,然后跳转到OEP。由于这壳过于简单,去掉小壳不费力,这里不多表述。 去壳之后,根据反汇编的结果,在call msvbvm60.rtcGetObject的地方设置断点,查看寄存器和堆栈的情况。到目前为止,so far so good,心情非常哇哈哈。可是,程序停在断点之后,傻了眼,堆栈和寄存器的值比较杂乱,根本看不出来参数是怎么传的。 既然,是新手区的文章,这里对于程序的调用传统就多说两句。大体上常见的调用传统(calling convention)有以下几种: 1,cdecl (c default call)c的默认调用方式,好处是能比较好的支持可变参数函数,比如printf;缺点是编译结果体积会稍微大一点 a,参数通过堆栈传递,从右向左(也就是最后一个参数先进栈)压栈 b,调用者负责清空堆栈中的参数 c,在object文件中,cdecl函数的名字前缀一个下划线'_'。 2,stdcall (standard call)pascal的默认调用方式 a,参数通过堆栈传递,从右向左(也就是最后一个参数先进栈)压栈 b,被调用者负责清空堆栈中的参数 c,在object文件中,stdcall函数的名字前缀一个下划线'_',后缀一个'@'以及堆栈需要的空间字节数 3,fastcall (fast call)因为使用了寄存器传参数,所以调用速度会快一些 a,前两个参数放进ECX,EDX里面,两个以上的参数从右向左压栈 b,被调用者负责清空堆栈中的参数 c,在object文件中,stdcall函数的名字前缀一个'@',后缀一个'@'以及堆栈需要的空间字节数 4,thiscall (this call)面向对象语言传递this指针的调用方式 a,参数通过堆栈传递,从右向左(也就是最后一个参数先进栈)压栈,this指针通过ECX传递 b,被调用者负责清空堆栈中的参数 感兴趣者可以参考这片鸟文,写得很清楚 http://www.codeproject.com/Articles/1388/Calling-Conventions-Demystified 好了,学习了calling convention之后,我们来看一下调用rtcGetObject之际的堆栈和寄存器现场。 为了让运行时现场和程序对应起来,我采用的程序是自己用VB60写的一个范例。这个例子中GetObject的调用如下: Dim mytest As Object mytest = GetObject("d:\abc.ljs", "mohaha") 首先使用W32dasm反汇编目标exe,在import function里面找到rtcGetObject函数,查看它被call的地方,设置上断点。 程序如约,跑到了断点处,此时现场如下: 【寄存器】 EAX 0012FA7C ECX 0012FA5C EDX 0012FA6C EBX 00000008 ESP 0012F9DC EBP 0012FAA4 ESI 00000000 EDI 72A46DF6 【堆栈】: (从ESP到EBP的内容) 0012F9DC 0012FA5C 0012F9E0 0012FA7C 0012F9E4 0012FA6C 0012F9E8 0012FAE8 0012F9EC 00271C18 0012F9F0 00000001 0012F9F4 FFFFFFFF 0012F9F8 FFFFFFFF 0012F9FC 75F5881F RETURN to USER32.75F5881F from ntdll.RtlActivateActivationContextUnsafeFast 0012FA00 75F588C9 RETURN to USER32.75F588C9 from ntdll.RtlDeactivateActivationContextUnsafeFast 0012FA04 0012FA14 0012FA08 00000001 0012FA0C 00000000 0012FA10 00000000 0012FA14 0012F9D0 0012FA18 0012FB98 0012FA1C 0012FF70 0012FA20 75FA62E3 USER32.75FA62E3 0012FA24 0E3DB391 0012FA28 FFFFFFFE 0012FA2C 00000008 0012FA30 75F57631 RETURN to USER32.75F57631 from USER32.75F587C3 0012FA34 00401654 UNICODE "mohaha" 0012FA38 7299F74E MSVBVM60.7299F74E 0012FA3C 00000008 0012FA40 00000080 0012FA44 00401638 UNICODE "d:\abc.ljs" 0012FA48 729A0962 RETURN to MSVBVM60.729A0962 from MSVBVM60.72972B59 0012FA4C 00000000 0012FA50 00001001 0012FA54 0012FAB4 0012FA58 01AEF71C 0012FA5C 00000000 0012FA60 01AEF71C 0012FA64 75F566E3 USER32.IsIconic 0012FA68 01AEF71C 0012FA6C 00000008 0012FA70 75F4CC71 RETURN to USER32.75F4CC71 from USER32.75F57543 0012FA74 00272D3C UNICODE "mohaha" 0012FA78 0066FDD0 0012FA7C 00000008 0012FA80 0ABB0367 0012FA84 00272D74 UNICODE "d:\abc.ljs" 0012FA88 01AEF71C 0012FA8C 00000000 0012FA90 0012FAC8 Pointer to next SEH record 0012FA94 004010B6 SE handler 0012FA98 0012F9E8 0012FA9C 00401098 12.00401098 0012FAA0 00000000 0012FAA4 0012FADC 下面我们根据刚刚学习的calling convention,但是由于我对VB不熟悉,不知道它在这里到底采用哪种调用传统,我们把和调用参数相关的值都单拿出来看。 CX:0012FA5C DX:0012FA6C 栈顶 + 0:0012FA5C 栈顶 + 4:0012FA7C 栈顶 + 8:0012FA6C CX和DX的值和栈里面的值有重复,应该是准备参数时候的中间结果,最后push到栈里面的。所以我估计,参数是全用栈传递的。因为VB是部分面向对象的语言,第一个参数有可能是某些this指针类型的玩意儿,所以栈顶第一个参数可能不是我们需要的。后两个参数可能就应该是字符串参数的实体了。 现在,让我们来看看这些值都对应着什么东西。假设他们是地址指针,我把对应空间的值列在了后面: 栈顶 + 0: 0012FA5C:00000000 栈顶 + 4: 0012FA7C:00000008 栈顶 + 8: 0012FA6C:00000008 我当时的幻想是,字符串的首地址应该出现在这三个值中,就好像C语言一样。但是,回到现实,这三个值......都不是字符串起始地址。 难道是VB下,STRING的内存存储方式不同?于是开始调研VB下的STRING。发现VB采用COM那套BSTR字符串方案,即,字符串前缀整个字符串的字节长度(不算结束符),字符串的每一个字符都是UNICODE双字节表示,字符串结尾以UNICODE的0000表示结束。 OK。回到我们的例子程序,第一个参数"d:\abc.ljs",长度为10,字节应该是20个;第二个参数"mohaha",长度为6,应该是12个字节。 于是,在内存中字符串的开头找这两个数,依然未果。此时,感觉这一切好奇妙......完全摸不着头脑。 又经过了几番思考,几番网上搜索,发现有一个重要的问题被忽略了。VB的官方Document里面定义的GetObject原型是这样的: GetObject( [ pathname ] [ , class ] ) 其中 pathname 和 class 都是Variant类型,而不是String。 那好,咱们来再看一下variant类型变量在内存中的组织形式。 |--------------------------------| | 偏移量 | 域大小 | 域描述 | |--------|---------|-------------| | 0 | 2 | 数据类型 | | 2 | 6 | 保留字段 | | 8 | 最大 8 | 实际数据内容| |--------------------------------| Variant可以存各种类型的数据,他的前两个字节储存着它当前数据的数据类型,随后6字节是保留字,没有任何意义(有的文档上说,这6个字节必须是0,但是我实际观测发现,这6个字节可以是任何值)。 数据类型,有一个大表,可以参考Wiki页http://en.wikipedia.org/wiki/Variant_type。我们可以看到String类型的数据类型码是:0x0008,实际数据内容是一个wchar_t的字符串指针。那好,咱们再回头看看我们的运行现场。 栈内0012FA6C和0012FA7C的内容都是0x08,之后6个字节的随意值,然后跟随了一个字符串的指针,指向我们的参数字符串。 现在,一切都对上了。调试过程中,我对这两个variant进行修改,程序也作出了对应的行为变化。这下,我就可以通过对对应字符常量进行修改、push命令顺序的调整,随意改变调用GetObject时候的参数了。 文毕。关于Variant有几篇网文值得参考:http://en.wikipedia.org/wiki/Variant_typehttp://www.codeguru.com/vb/gen/vb_misc/algorithms/article.php/c7495/How-Visual-Basic-6-Stores-Data.htmhttp://www.canaimasoft.com/f90vb/onlinemanuals/usermanual/th_34.htm
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)