又是崭新的一年,大侠又来雷人了。
QQ版本更新以后,偶然翻看一下以前写的自动聊天机器人,居然不适用了!
于是重写了个动态库,为String传递所困扰,于是决定借假期搞翻这颗钉子。
之前也写过关于参数传递的文章,不过对字符串的讲述很少,原因是我没亲自测试过,呵呵
先说C/C++的两个函数:
EXPORT_API DWORD __stdcall fnHook(DWORD dwIndex) EXPORT_API DWORD __stdcall fnVarptr(void *dwArg,DWORD dwFlag)
在VB中调用应该是:
Private Declare Function fnHook Lib "Hook.dll" (ByVal dwIndex As Long) As Long Private Declare Function fnVarptr Lib "Hook.dll" (ByVal dwPtr As Long,ByVal dwFlag As Long) As Long Private Declare Function fnVarptrA Lib "Hook.dll" Alias "fnVarptr" (dwPtr As Any,ByVal dwFlag As Long) As Long Private Declare Function fnVarptrS Lib "Hook.dll" Alias "fnVarptr" (dwPtr As String,ByVal dwFlag As Long) As Long
后面两种其实是一样的,不过使用了别名调用,改变了参数的类型。
每声明一个API,vb就会自己写一个函数,这个函数会调用DllFunctionCall得到API函数地址,再次调用则直接转到地址去执行。
下面是VB调用的代码,和调用后在VC中DEBUG得到传递进来的参数值:
Private Sub Form_Load() ' Call fnHook(-1) Call Fuck End Sub Public Sub Fuck() Dim ret As Long,s As String s = "abc" '__vbastrcopy Call fnHook(0) ret = fnVarptr(StrPtr(s),6) '0x0014eb14 -> 97,98,99,0 Call fnHook(1) ret = fnVarptr(VarPtr(s),6) '0x0012fac0 -> 20,235,20,0 = 0x0014eb14 Call fnHook(2) ret = fnVarptrA(StrPtr(s),6) '0x0012fab8 -> 20,0 = 0x0014eb14 Call fnHook(3) ret = fnVarptrA(VarPtr(s),6) '0x0012fab8 -> 192,250,18,0 = 0x0012fac0 Call fnHook(4) ret = fnVarptrA(ByVal StrPtr(s),6) '0x0014eb14 -> 97,0 Call fnHook(5) ret = fnVarptrA(ByVal VarPtr(s),6) '0x0012fac0 -> 20,0 = 0x0014eb14 Call fnHook(6) ret = fnVarptrA(ByVal s,6) '0x0014eb3c -> 97,0 'ret = fnVarptrA(s,6) -> fucking no more Call fnHook(7) ret = fnVarptrS(ByVal s,0 Call fnHook(8) ret = fnVarptrS(s,6) '0x0012fabc -> 60,0 = 0x0014eb3c Call fnHook(9) End Sub
对于使用来说,到这里就足够了。你足以开发一些操作硬件之类的DLL来给VB或者其他程序调用。
因为上面很清楚,只有fnVarptr(StrPtr(s),6)和fnVarptrA(ByVal StrPtr(s),6)得到了abc的Unicode编码
而fnVarptrA(ByVal s,6)和fnVarptrS(ByVal s,6)得到的是ANSI编码,这两个根据实际情况使用
然而对于技术研究,还不够。我们要把它切开,把里面的东西挖出来。。。
Call fnHook(-1) 就是我们打响战斗的信号弹:
;Call fnHook(-1) 00001AE0 push -1 00001AE2 call 000018D0
注意,这里000018D0是vb自己写的函数,不是fnHook的地址,不过最终它会跳转到fnHook执行。
执行到返回就是它的主调函数,Form_Load的代码了。本来我想从调用Fuck开始看String的初始化和赋值,可惜啊。
00401AE7 call dword ptr ds:[401010h] ;段间调用(N行指令) 00401AED mov edx,dword ptr [esi] ;ESI=0014DEC0 未改变 00401AEF push esi 00401AF0 call dword ptr [edx+6F8h] ;EDX=7C99B178
这个00401AE7转来转去,1000多条指令还没返回这里。为了减少文章读者中引发精神失常的人数,我又把它们删掉了。
当然他们都是一些无关紧要的东西,完全可以忽略。而最后00401AF0的Call就是调用Fuck函数的。。。
004014D1 jmp 00401B40
这个00401B40就是函数的地址:
;FS寄存器指向当前活动线程的TEB结构(线程结构) ;偏移 说明 ;000 指向SEH链指针 ;下面是OllyIce的跟踪,内存从0x150000开始(VC的DLL模块是自身0x10000) 00401B40 push ebp 00401B41 mov ebp,esp 00401B43 sub esp,0Ch 00401B46 push 4010B6h 00401B4B mov eax,fs:[00000000] ;eax->0012FB00 00401B51 push eax 00401B52 mov dword ptr fs:[0],esp 00401B59 sub esp,20h 00401B5C push ebx 00401B5D push esi 00401B5E push edi 00401B5F mov dword ptr [ebp-0Ch],esp 00401B62 mov dword ptr [ebp-8],4010A0h ;s = 4010A0 00401B69 xor esi,esi 00401B6B mov dword ptr [ebp-4],esi ;ret = 0 00401B6E mov eax,dword ptr [ebp+8] 00401B71 push eax 00401B72 mov ecx,dword ptr [eax] 00401B74 call dword ptr [ecx+4] ;MSVBVM60.Zombie_AddRef 00401B77 mov edx,401948h ;UNICODE "abc" 00401B7C lea ecx,[ebp-1Ch] ;地址传送 00401B7F mov dword ptr [ebp-1Ch],esi ;*12fac0 = 0 00401B82 mov dword ptr [ebp-20h],esi ;*12fabc = 0 00401B85 mov dword ptr [ebp-24h],esi ;*12fab8 = 0 00401B88 call dword ptr ds:[401068h] ;MSVBVM60.__vbaStrCopy ;执行后EAX=15EB14(Unicode'abc'),EDX=6;ESI=0不变 ;Call fnHook(0) 00401B8E push esi 00401B8F call 004018D0
我们看到,vb自己又定义了三个指针。
EBP是堆栈指针,调用后因mov ebp,esp变为栈底,而sub esp,xx使esp仍然是栈顶。
加入定义的局部变量都是32位,如DWORD,或者vb的Long等,那么:
EBP - 4 是第一个局部变量
EBP - 8 是第二个局部变量
EBP 调用前的栈顶
EBP + 4 返回值地址
EBP + 8 第一个参数
; 调用约定 堆栈清除 参数传递
; __cdecl 调用者 从右到左,通过堆栈传递
; __stdcall 函数体 从右到左,通过堆栈传递
; __fastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
; thiscall 函数体 this指针默认通过ECX传递,其他参数从右到左入栈
那么,vb自己定义的这三个指针,等下就告诉你。我们先跟00401B8F这个Call进去看看它都干了什么坏事:
;api -> fnHook 004018D0 mov eax,[004032DC] 004018D5 or eax,eax ;if(eax == 0) goto 004018DB -> pointer != null 004018D7 je 004018DB 004018D9 jmp eax ;1000100F -> jumped 004018DB push 4018B8h ;*4018B8 = 004018A0,*004018A0 = "Hook.dll" 004018E0 mov eax,401140h ;*401140 = jmp dword ptr [401030],*401030=7339a0e5(MSvbVM60.DllFunctionCall) 004018E5 call eax ;调用DllFunctionCall,执行LoadLibray之类,而后得到DLL函数地址 004018E7 jmp eax ;跳到函数地址开始执行第一次
元芳,DllFunctionCall的真相居然是这样!这里1000100F就是vb给API函数分配的一个标签。
;@ILT+10(?fnHook@@YGKK@Z): 1000100F jmp fnHook (10001070) ; source code optimized
其实标签放的是一条Jump指令,哎,跟女人一样啰嗦。。。
执行到返回,自然是调用fnVarPtr函数,但是函数的调用是从准备参数开始的:
00401B94 mov esi,dword ptr ds:[401010h] ;MSvbVM60.__vbaSetSystemError 00401B9A call esi ; 00401B9C mov edx,dword ptr [ebp-1Ch] 00401B9F mov edi,dword ptr ds:[401058h] ;MSvbVM60.VarPtr 00401BA5 push edx 00401BA6 call edi ;Call VarPtr
前面两条指令是SetLastError的封装,不管它(它是为什么vb总是能报错,而C/C++直接崩溃的原因)。
这里它把[ebp-1Ch]这个地址(就是前面说的vb自己定义的变量),入栈,调用VarPtr,就是VarPtr([ebp-1Ch])!
7346DCE5 mov eax,dword ptr [esp+4] ;前面push x,*(esp + 4) = x 7346DCE9 ret 4
这才是VarPtr的庐山真面目!
在VC中,寄存器EAX和EDX是作为返回值的,所以执行后eax就是结果。紧接着自然是调用函数了:
;ret = fnVarptr(StrPtr(s),6) 00401BA8 push 6 00401BAA push eax 00401BAB call 00401914 ;api -> fnVarptr 类似fnHook的调用
这个00401BAB的Call与调用fnHook的过程是一样的,vb给每个声明的API都自己写了个函数:
;api -> fnVarptr 00401914 mov eax,[004032E8] 00401919 or eax,eax 0040191B je 0040191F 0040191D jmp eax 0040191F push 4018FCh 00401924 mov eax,401140h 00401929 call eax 0040192B jmp eax
在je那里就返回了,看完上面的上面的汇编,你懂的。
然后是取得返回值,接着vb有自己去检测异常了,汗!
;ret = fnVarptr(...)的返回值 00401BB0 mov dword ptr [ebp-24h],eax 00401BB3 call esi ;esi=7345C195,*7345C195 = ntdll.RtlGetLastWin32Error ;Call fnHook(1) 00401BB5 push 1 00401BB7 call 004018D0 00401BBC call esi
跟着是第二次调用:
;ret = fnVarptr(VarPtr(s),6) -> 因此,fnVarPtr得到的是s的地址的地址 00401BBE mov ebx,dword ptr ds:[401058h] 00401BC4 lea eax,[ebp-1Ch] ;对比StrPtr:mov edx,dword ptr [ebp-1Ch](__cdecl方式) 00401BC7 push eax 00401BC8 call ebx ;Call VarPtr 00401BCA push 6 00401BCC push eax 00401BCD call 00401914 00401BD2 mov dword ptr [ebp-24h],eax 00401BD5 call esi 00401BD7 push 2 00401BD9 call 004018D0
不难看出,StrPtr是把String的地址传递给VarPtr,相当于String是char* s,再VarPtr就是&s了。
当然,第二次调用是不能正常传递字符串(指针)的了,而是传递了字符串(指针)的地址。
从vb的源码可以看出:
ret = fnVarptr(VarPtr(s),6)
执行后fnVarPtr得到的是0x0012fac0,用它作为指针的地址得到四个字节 20,0就是 0x0014eb14 高高低低放置的结果。
而0x0014eb14是字符串的指针,也就是字符数据的内存地址,就是String啦!
注:用OD或OllyIce跟踪,因为加载是hModule不是0x10000而变为0x15eb14,当然这一行文字是我的观点,没有具体考究。
同理,对于ret = fnVarptrA(StrPtr(s),6),fnVarPtr的第一个参数自然也是mov到通用寄存器e[abcd]x
但是,由于APIfnVarPtr的函数声明有所不同(dwPtr As Any -> void*),导致了编译后的指令也有所不同:
00401BE0 mov ecx,dword ptr [ebp-1Ch] 00401BE3 push ecx 00401BE4 call edi ;Call VarPtr 00401BE6 lea edx,[ebp-24h] ;;EBP-0x24是第8个DWORD临时变量的地址 = 12FAB8 见00401B85指令 00401BE9 push 6 00401BEB push edx ;;第8个DWORD的地址入栈 00401BEC mov dword ptr [ebp-24h],eax ;;EAX=15EB14是__vbaStrCopy赋值字符串存放的地址 00401BEF call 00401914 ;执行Call的时候,得到dwPtr=12FAB8,而**dwPtr才是"abc" 00401BF4 call esi
对于ret = fnVarptrA(VarPtr(s),6)
00401BF6 push 3 00401BF8 call 004018D0 00401BFD call esi 00401BFF lea eax,[ebp-1Ch] 00401C02 push eax 00401C03 call ebx 00401C05 lea ecx,[ebp-24h] 00401C08 push 6 00401C0A push ecx ;相当于***dwPtr才是"abc" 00401C0B mov dword ptr [ebp-24h],eax 00401C0E call 00401914
OK,文章贵精不贵长(其实很多东西都不是越长越好,精才是最珍贵,你懂的)
那么至于传递了字符串,如何反过来设置其值,看两行代码
p = (PBYTE)dwArg; // if(p[0] == 0x61/*97*/) { if(p[1] == 0x00) { p[0] = 'd'; p[2] = 'e'; p[4] = 'f'; }else { p[0] = 'd'; p[1] = 'e'; p[2] = 'f'; } }
本例中,vb的字符串s其实也就是个指针,经过调用会变为0x0014eb14和0x0014eb3c以及其他不合法的指针值
给这两个有效的指针传送数据,都可以改变指针指向的内存,那么从vb的代码大家都知道该怎么做了。
阿弥陀佛,不知道有多少人精神失常了,反正我要。。去吃点东西,先这样了
梁侠
2013-01-02 01:43:00