2. 如何找到参数值
还是先看代码:
using System;
namespace TestStackMethod { class Program { static void M3() { Console.Write("Press any key to continue..."); Console.ReadKey(); }
static void M2(string x, string y, string z, string o, string p, string q) { string a = x.ToUpper(); string b = y.ToUpper(); string c = z.ToUpper(); string d = o.ToUpper(); string e = p.ToUpper(); string f = q.ToUpper(); Console.WriteLine("{0},{1},{2},{3},{4},{5}", a, b, c, d, e, f); M3();
} static void M1(int x, int y, int z, int o, int p, int q) { int a = x + y; int b = y + z; int c = z + o; int d = o + p; int e = p + q; int f = q + x; int result = a + b + c + d + e + f; Console.WriteLine(result); M2("a", "b", "c", "d", "e", "f"); }
static void Main(string[] args) { M1(0, 1, 2, 3, 4, 5); } } } |
像上一小节一样,采取Release的编译输出。这一节的目的主要是找出方法中参数的位置和值,可能读者看到这里会奇怪,因为我在4.4 查看线程栈这一节已经讲了可以通过“!clrstack -a”打印出参数和局部变量,这个命令确实可以,但是碰到优化过后的代码,能被调试器分析出来的变量和参数就少了许多了,就以这段代码为例,接下来我分别在Debug和Release的版本上通过“!clrstack -a”打印出主线程的线程栈:
Release Version |
0:000> !clrstack -a OS Thread Id: 0x1b5c (0) Child SP IP Call Site … 0115f1bc 015a0693 TestStackMethod.Program.M3()
0115f1d0 015a065a TestStackMethod.Program.M2(System.String, System.String, System.String, System.String, System.String, System.String) PARAMETERS: x = <no data> y = <no data> z = <no data> o = <no data> p = <no data> q = <no data> LOCALS: <no data> <no data> <no data> <no data> <no data> <no data>
0115f204 015a0506 TestStackMethod.Program.M1(Int32, Int32, Int32, Int32, Int32, Int32) PARAMETERS: x = <no data> y = <no data> z = <no data> o = <no data> p = <no data> q = <no data> LOCALS: <no data> <no data> <no data> <no data> <no data> 0x0115f204 = 0x000002a6
0115f230 015a0478 TestStackMethod.Program.Main(System.String[]) PARAMETERS: args = <no data> LOCALS: <no data>
0115f3b0 62251376 [GCFrame: 0115f3b0] |
Debug Version |
0:000> !clrstack -a OS Thread Id: 0x166c (0) Child SP IP Call Site …
010ff1c4 01460789 TestStackMethod.Program.M3()
010ff1d8 0146073f TestStackMethod.Program.M2(System.String, System.String, System.String, System.String, System.String, System.String) PARAMETERS: x (0x010ff214) = 0x03103144 y (0x010ff210) = 0x03103154 z (0x010ff234) = 0x03103164 o (0x010ff230) = 0x03103174 p (0x010ff22c) = 0x03103184 q (0x010ff228) = 0x03105854 LOCALS: 0x010ff20c = 0x031058dc 0x010ff208 = 0x031058ec 0x010ff204 = 0x031058fc 0x010ff200 = 0x0310590c 0x010ff1fc = 0x0310591c 0x010ff1f8 = 0x0310592c
010ff238 014605d7 TestStackMethod.Program.M1(Int32, Int32, Int32, Int32, Int32, Int32) PARAMETERS: x (0x010ff270) = 0x00000000 y (0x010ff26c) = 0x00000001 z (0x010ff290) = 0x00000002 o (0x010ff28c) = 0x00000003 p (0x010ff288) = 0x00000004 q (0x010ff284) = 0x0000030b LOCALS: 0x010ff268 = 0x00000001 0x010ff264 = 0x00000003 0x010ff260 = 0x00000005 0x010ff25c = 0x00000007 0x010ff258 = 0x0000030f 0x010ff254 = 0x0000030b 0x010ff250 = 0x0000062a
… |
是不是两者对变量和参数的显示完全不同,对于方法M1和M2,Debug版本全都显示出来了,Release版本的一个参数和局部变量都没显示。因为在实际工作中经常会碰到这种找不到参数或局部变量的情况,按道理说方法还没返回,这些值不至于全失效了。
在我开始长篇讨论怎么找参数之前,先授人与鱼,先快速的说一下怎么找到参数的值:
假设我需要找到传入M1方法中的各个参数值:
1. 通过“!dumpstack”打印出该线程的栈回溯信息:
DumpStack会打印出托管和非托管的栈帧信息,在这里由于篇幅的原因我删掉部分非托管的栈帧信息
0:000> !dumpstack OS Thread Id: 0x1b5c (0) Current frame: ntdll!NtDeviceIoControlFile+0xc ChildEBP RetAddr Caller, Callee … 0115f024 76d89f58 KERNELBASE!GetConsoleInput+0x74, calling KERNELBASE!ConsoleCallServerGeneric 0115f078 76d8a09a KERNELBASE!ReadConsoleInputW+0x1a, calling KERNELBASE!GetConsoleInput 0115f090 612777bb (MethodDesc 60a72ae4 +0x6b DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr, InputRecord ByRef, Int32, Int32 ByRef)) 0115f0b8 612777bb (MethodDesc 60a72ae4 +0x6b DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr, InputRecord ByRef, Int32, Int32 ByRef)) 0115f100 62251831 clr!ThePreStub+0x11, calling clr!PreStubWorker 0115f124 6131000a (MethodDesc 609732dc +0xe6 System.Console.ReadKey(Boolean)), calling 60bbbddc 0115f1b4 015a0693 (MethodDesc 01554ce0 +0x1b TestStackMethod.Program.M3()), calling (MethodDesc 609732dc +0 System.Console.ReadKey(Boolean)) 0115f1c8 015a065a (MethodDesc 01554cec +0x13a TestStackMethod.Program.M2(System.String, System.String, System.String, System.String, System.String, System.String)), calling (MethodDesc 01554ce0 +0 TestStackMethod.Program.M3()) 0115f1ec 015a0506 (MethodDesc 01554cf8 +0x76 TestStackMethod.Program.M1(Int32, Int32, Int32, Int32, Int32, Int32)), calling (MethodDesc 01554cec +0 TestStackMethod.Program.M2(System.String, System.String, System.String, System.String, System.String, System.String)) 0115f218 015a0478 (MethodDesc 01554d04 +0x30 TestStackMethod.Program.Main(System.String[])), calling (MethodDesc 01554cf8 +0 TestStackMethod.Program.M1(Int32, Int32, Int32, Int32, Int32, Int32)) 0115f238 62251376 clr!CallDescrWorkerInternal+0x34 … |
2. 打印出当前栈帧中相关内存信息:
通过上表找到M1的父方法,也就是Main方法,并找到Main方法的父方法;然后找到Main和Main父方法的ChildEBP的值,这样可以通过dd命令打印出Main方法的在栈中的内存信息(暂时不解释):
0:000> dd 0115f218 0115f238 0115f218 0115f238 015a0478 00000149 00000004 0115f228 00000003 00000002 51f3e4ab 88d3ad50 0115f238 0115f244 |
从这一段内存里面找到Main方法的栈帧中的IP值:“015a0478”,可以看到我标粗的一段,那么自这段内存之后的几个字节的值就是参数相关的值,那么这些值怎么分别对应哪个参数呢,这时候只能通过分析汇编代码看到了。
3. 反汇编M1的父方法:
也就是说反汇编Main方法:
0:000> !u 015a0478 Normal JIT generated code TestStackMethod.Program.Main(System.String[]) Begin 015a0448, size 34 015a0448 55 push ebp 015a0449 8bec mov ebp,esp 015a044b 83ec08 sub esp,8 015a044e 33c0 xor eax,eax 015a0450 8945f8 mov dword ptr [ebp-8],eax 015a0453 8945fc mov dword ptr [ebp-4],eax 015a0456 8d4df8 lea ecx,[ebp-8] 015a0459 e8b2a0655f call mscorlib_ni+0x31a510 (60bfa510) (System.DateTime.get_Now(), mdToken: 06000523) 015a045e 6a02 push 2 015a0460 6a03 push 3 015a0462 6a04 push 4 015a0464 8d4df8 lea ecx,[ebp-8] 015a0467 e89493655f call mscorlib_ni+0x319800 (60bf9800) (System.DateTime.get_Millisecond(), mdToken: 06000520) 015a046c 50 push eax 015a046d 33c9 xor ecx,ecx 015a046f 8d5101 lea edx,[ecx+1] 015a0472 ff15004d5501 call dword ptr ds:[1554D00h] (TestStackMethod.Program.M1(Int32, Int32, Int32, Int32, Int32, Int32), mdToken: 06000003) >>> 015a0478 8be5 mov esp,ebp 015a047a 5d pop ebp 015a047b c3 ret |
从这段汇编可以看到参数的设置如下顺序是:
push z push o push p push q set ECX =x set EDX=y call M1 |
最后面call M1做了两件事情:一是把从M1返回回来需要执行的地址push到栈中,也就是把值“015a0478”存到栈中;二是调到M1方法(这句话也要打个折扣,因为在托管代码中没有直接跳到M1中,而是先执行一段代码后再调到M1,但是在这里不管细节)。
所以根据这些信息可以知道,上述汇编还能再看成如下效果:
push z push o push p push q set ECX =x set EDX=y push 0x015a0478 |
所以结合上一小节找到的栈中的内存信息:
0:000> dd 0115f218 0115f238 0115f218 0115f238 015a0478 00000149 00000004 0115f228 00000003 00000002 51f3e4ab 88d3ad50 0115f238 0115f244 |
|
可以推断出:
q = 00000149 p = 00000004 o = 00000003 z=00000002 |
我们可以看到q的值符合我们的结果((0x149+0x4+0x3+0x2+0x1)*2):
4. 找出前两个参数的值:
在上一步骤中留了个小尾巴,还有两个参数的值没找到:x,y。因为JIT编译器的优化,前两个参数普遍都会被存入寄存器中进行传递(32位,64位的CPU不止两个参数放入寄存器):
不幸的是这个时候这两个寄存器在后面调用M2和其他的方法的时候被其他的值覆盖了,所以不能通过读取寄存器中值来判断x和y的值:
0:000> r eax=00000000 ebx=0115f050 ecx=00000000 edx=00000000 esi=0115efa0 edi=00000001 eip=772f6c2c esp=0115ef04 ebp=0115f024 iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 ntdll!NtDeviceIoControlFile+0xc: 772f6c2c c22800 ret 28h |
分析这个情况还是得通过反汇编,只是这一次反汇编M1方法,看看M1方法怎么处理的ECX和EDX,为了便于分析,我去掉机器码部分,并高亮语法:
0:000> .asm no_code_bytes Assembly options: no_code_bytes 0:000> !u 015a0506 Normal JIT generated code TestStackMethod.Program.M1(Int32, Int32, Int32, Int32, Int32, Int32) Begin 015a0490, size 80 015a0490 push ebp 015a0491 mov ebp,esp 015a0493 push edi 015a0494 push esi 015a0495 push ebx 015a0496 sub esp,8 015a0499 xor eax,eax 015a049b mov dword ptr [ebp-14h],eax 015a049e mov edi,dword ptr [ebp+14h] 015a04a1 mov esi,dword ptr [ebp+10h] 015a04a4 mov ebx,dword ptr [ebp+8] 015a04a7 mov eax,edx 015a04a9 add eax,edi 015a04ab mov dword ptr [ebp-10h],eax 015a04ae add edi,esi 015a04b0 mov eax,dword ptr [ebp+0Ch] 015a04b3 add esi,eax 015a04b5 add eax,ebx 015a04b7 add ebx,ecx 015a04b9 add ecx,edx 015a04bb add ecx,dword ptr [ebp-10h] 015a04be add ecx,edi 015a04c0 add ecx,esi 015a04c2 add ecx,eax 015a04c4 add ecx,ebx 015a04c6 mov dword ptr [ebp-14h],ecx 015a04c9 mov ecx,dword ptr [ebp-14h] 015a04cc call mscorlib_ni+0xa30adc (61310adc) 015a04d1 push dword ptr ds:[43022ACh] 015a04d7 push dword ptr ds:[43022B0h] 015a04dd push dword ptr ds:[43022B4h] 015a04e3 call mscorlib_ni+0x3ab470 (60c8b470) 015a04e8 push eax 015a04e9 mov ecx,dword ptr [ebp-14h] 015a04ec xor edx,edx 015a04ee call clr!COMNumber::FormatInt32 (62261b9c) 015a04f3 push eax 015a04f4 mov edx,dword ptr ds:[43022A8h] 015a04fa mov ecx,dword ptr ds:[43022A4h] 015a0500 call dword ptr ds:[1554CF4h] >>> 015a0506 lea esp,[ebp-0Ch] 015a0509 pop ebx 015a050a pop esi 015a050b pop edi 015a050c pop ebp 015a050d ret 10h |
从上面的汇编可以看到ECX和EDX被用来计算后直接丢掉了,没在栈中留下任何痕迹,碰到这种情况确实没有任何办法找回x和y的值。但是我们一般分析参数值的目的主要是用来推断它最后起了什么作用,所以虽然没能找到x和y的值,但也是有可能推断出x和y用来做了什么事情,在本实例中就是简单的用来加进一个局部变量中然后输出。从汇编中可以看到最终计算的结果也就是变量result的值存放在EBX-14h的位置,那我们就能找到这个结果了:
0:000> dd 0115f218-14h l1 0115f204 000002a6 0:000> ? 000002a6 Evaluate expression: 678 = 000002a6 |
如果我们推的更细致一点可以分析出处在EBX-10h位置的值为:
[EBX-10h] = [ebp+14h] + EDX |
找出这两处的值:
0:000> dd 0115f218+14h l1 0115f22c 00000002 0:000> dd 0115f218-10h l1 0115f208 00000003 |
从这里可以看出EDX也就是参数y的值等于1。然后就能推断x的值为0了。
在这里说明一下,找出没有保存在栈中,也就是寄存器中的值是非常困难的,我写的示例已经够简单了,但转成优化的汇编后就变的晦涩难懂,在了解源码的情况可能找出临时的寄存器的值可能稍微简单一些,在没有源码的情况下,难度成指数级的增加,这要求操作人员有丰富的逆向经验。
由于篇幅比较长,具体分析过程见下一篇。