使用WinDbg —— .NET篇 (十一)

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”打印出参数和局部变量,这个命令确实可以,但是碰到优化过后的代码,能被调试器分析出来的变量和参数就少了许多了,就以这段代码为例,接下来我分别在DebugRelease的版本上通过“!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

 

是不是两者对变量和参数的显示完全不同,对于方法M1M2Debug版本全都显示出来了,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方法的父方法;然后找到MainMain父方法的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. 找出前两个参数的值:

在上一步骤中留了个小尾巴,还有两个参数的值没找到:xy。因为JIT编译器的优化,前两个参数普遍都会被存入寄存器中进行传递(32位,64位的CPU不止两个参数放入寄存器):

不幸的是这个时候这两个寄存器在后面调用M2和其他的方法的时候被其他的值覆盖了,所以不能通过读取寄存器中值来判断xy的值:

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方法怎么处理的ECXEDX,为了便于分析,我去掉机器码部分,并高亮语法:

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

从上面的汇编可以看到ECXEDX被用来计算后直接丢掉了,没在栈中留下任何痕迹,碰到这种情况确实没有任何办法找回xy的值。但是我们一般分析参数值的目的主要是用来推断它最后起了什么作用,所以虽然没能找到xy的值,但也是有可能推断出xy用来做了什么事情,在本实例中就是简单的用来加进一个局部变量中然后输出。从汇编中可以看到最终计算的结果也就是变量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了。

在这里说明一下,找出没有保存在栈中,也就是寄存器中的值是非常困难的,我写的示例已经够简单了,但转成优化的汇编后就变的晦涩难懂,在了解源码的情况可能找出临时的寄存器的值可能稍微简单一些,在没有源码的情况下,难度成指数级的增加,这要求操作人员有丰富的逆向经验。

由于篇幅比较长,具体分析过程见下一篇。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值