5.1 x86: 3个参数

5.1.1 MSVC

在我们用MSVC 2010 Express编译后可以看到:

  1. $SG3830 DB a=%d; b=%d; c=%d’, 00H
  2. ...
  3. push 3
  4. push 2
  5. push 1
  6. push OFFSET $SG3830
  7. call _printf
  8. add esp, 16 ; 00000010H

这和之前的代码几乎一样,但是我们现在可以看到printf()的参数被反序压入了栈中。第一个参数被最后压入。

另外,在32bit的环境下int类型变量占4 bytes。

那么,这里有4个参数 4*4=16 —— 恰好在栈中占据了16bytes:一个32bit字符串指针,和3个int类型变量。

当函数执行完后,执行"ADD ESP, X"指令恢复栈指针寄存器(ESP 寄存器)。通常可以在这里推断函数参数的个数:用 X除以4。

当然,这只涉及__cdecl函数调用方式。

也可以在最后一个函数调用后,把几个ADD ESP, X指令合并成一个。

  1. push a1
  2. push a2
  3. call ...
  4. ...
  5. push a1
  6. call ...
  7. ...
  8. push a1
  9. push a2
  10. push a3
  11. call ...
  12. add esp, 24

5.1.2 MSVC 与 ollyDbg

现在我们来在OllyDbg中加载这个范例。我们可以尝试在MSVC 2012 加 /MD 参数编译这个示例,也就是链接MSVCR*.dll,那么我们就可以在debugger中清楚的看到调用的函数。

在OllyDbg中载入程序,最开始的断点在ntdll.dll中,接着按F9(run),然后第二个断点在CRT-code中。现在我们来找main()函数。

往下滚动屏幕,找到下图这段代码(MSVC把main()函数分配在代码段开始处) 见图5.3

点击 PUSH EBP指令,按下F2(设置断点)然后按下F9(run),通过这些操作来跳过CRT-code,因为我们现在还不必关注这部分。

按6次F8(step over)。见图5.4 现在EIP 指向了CALL printf的指令。和其他调试器一样,OllyDbg高亮了有值改变的寄存器。所以每次你按下F8,EIP都在改变然后它看起来便是红色的。ESP同时也在改变,因为它是指向栈的

栈中的数据又在哪?那么看一下调试器右下方的窗口:

5.1 x86: 3个参数 - 图1

图 5.1

然后我们可以看到有三列,栈的地址,元组数据,以及一些OllyDbg的注释,OllyDbg可以识别像printf()这样的字符串,以及后面的三个值。

右击选中字符串,然后点击”follow in dump”,然后字符串就会出现在左侧显示内存数据的地方,这些内存的数据可以被编辑。我们可以修改这些字符串,之后这个例子的结果就会变的不同,现在可能并不是很实用。但是作为练习却非常好,可以体会每部分是如何工作的。

再按一次F8(step over)

然后我们就可以看到输出

5.1 x86: 3个参数 - 图2

图5.2 执行printf()函数

让我们看看寄存器和栈是怎样变化的 见图5.5

EAX寄存器现在是0xD(13).这是正确的,printf()返回打印的字符,EIP也变了——

事实上现在指向CALL printf之后下一条指令的地址.ECX和EDX的值也改变了。显然,printf()函数的内部机制对它们进行了使用。

很重要的一点ESP的值并没有发生变化,栈的状态也是!我们可以清楚地看到字符串和相应的3个值还是在那里,实际上这就是cdecl调用方式。被调用的函数并不清楚栈中参数,因为这是调用体的任务。

再按一下F8执行ADD ESP, 0见图5.6

ESP改变了,但是值还是在栈中!当然 没有必要用0或者别的数据填充这些值。

因为在栈指针寄存器之上的数据都是无用的。

5.1 x86: 3个参数 - 图3

图5.3 OllyDbg:main()初始处

5.1 x86: 3个参数 - 图4

图5.4 OllyDbg:printf()执行时

5.1 x86: 3个参数 - 图5

图5.5 Ollydbg:printf()执行后

5.1 x86: 3个参数 - 图6

图5.6 OllyDbg ADD ESP, 10执行完后

5.1.3 GCC

现在我们将同样的程序在linux下用GCC4.4.1编译后放入IDA看一下:

  1. main proc near
  2. var_10 = dword ptr -10h
  3. var_C = dword ptr -0Ch
  4. var_8 = dword ptr -8
  5. var_4 = dword ptr -4
  6. push ebp
  7. mov ebp, esp
  8. and esp, 0FFFFFFF0h
  9. sub esp, 10h
  10. mov eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
  11. mov [esp+10h+var_4], 3
  12. mov [esp+10h+var_8], 2
  13. mov [esp+10h+var_C], 1
  14. mov [esp+10h+var_10], eax
  15. call _printf
  16. mov eax, 0
  17. leave
  18. retn
  19. main endp

MSVC与GCC编译后代码的不同点只是参数入栈的方法不同,这里GCC不用PUSH/POP而是直接对栈操作。

5.1.4 GCC与GDB

接着我们尝试在linux中用GDB运行下这个示例程序。

-g 表示将debug信息插入可执行文件中

$ gcc 1.c -g -o 1

反编译:

  1. $ gdb 1
  2. GNU gdb (GDB) 7.6.1-ubuntu
  3. Copyright (C) 2013 Free Software Foundation, Inc.
  4. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
  5. This is free software: you are free to change and redistribute it.
  6. There is NO WARRANTY, to the extent permitted by law. Type "show copying"
  7. and "show warranty" for details.
  8. This GDB was configured as "i686-linux-gnu".
  9. For bug reporting instructions, please see:
  10. <http://www.gnu.org/software/gdb/bugs/>...
  11. Reading symbols from /home/dennis/polygon/1...done.

表5.1 在printf()处设置断点

  1. (gdb) b printf
  2. Breakpoint 1 at 0x80482f0

Run 这里没有printf()函数的源码,所以GDB没法显示出源码,但是却可以这样做

  1. (gdb) run
  2. Starting program: /home/dennis/polygon/1
  3. Breakpoint 1, __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
  4. 29 printf.c: No such file or directory.

打印10组栈中的元组数据,左边是栈中的地址

  1. (gdb) x/10w $esp
  2. 0xbffff11c: 0x0804844a 0x080484f0 0x00000001 0x00000002
  3. 0xbffff12c: 0x00000003 0x08048460 0x00000000 0x00000000
  4. 0xbffff13c: 0xb7e29905 0x00000001

最开始的是返回地址(0x0804844a),我们可以确定在这里,于是可以反汇编这里的代码

  1. (gdb) x/5i 0x0804844a
  2. 0x804844a <main+45>: mov $0x0,%eax
  3. 0x804844f <main+50>: leave
  4. 0x8048450 <main+51>: ret
  5. 0x8048451: xchg %ax,%ax
  6. 0x8048453: xchg %ax,%ax

两个XCHG指令,明显是一些垃圾数据,可以忽略 第二个(0x080484f0)是一处格式化字符串

  1. (gdb) x/s 0x080484f0
  2. 0x80484f0: "a=%d; b=%d; c=%d"

而其他三个则是printf()函数的参数,另外的可能只是栈中的垃圾数据,但是也可能是其他函数的数据,例如它们的本地变量。这里可以忽略。 执行 finish ,表示执行到函数结束。在这里是执行到printf()完。

  1. (gdb) finish
  2. Run till exit from #0 __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
  3. main () at 1.c:6
  4. 6 return 0;
  5. Value returned is $2 = 13

GDB显示了printf()函数在eax中的返回值,这是打印字符的数量,就像在OllyDbg中一样。

我们同样看到了return 0;及这在1.c文件中第6行所代表的含义。1.c文件就在当前目录下,GDB就在那找到了字符串。但是GDB又是怎么知道当前执行到了哪一行?

事实上这和编译器有关,当生成调试信息时,同样也保存了一张代码行号与指令地址的关系表。

查看EAX中储存的13:

  1. (gdb) info registers
  2. eax 0xd 13
  3. ecx 0x0 0
  4. edx 0x0 0
  5. ebx 0xb7fc0000 -1208221696
  6. esp 0xbffff120 0xbffff120
  7. ebp 0xbffff138 0xbffff138
  8. esi 0x0 0
  9. edi 0x0 0
  10. eip 0x804844a 0x804844a <main+45>
  11. ...

反汇编当前的指令

  1. (gdb) disas
  2. Dump of assembler code for function main:
  3. 0x0804841d <+0>: push %ebp
  4. 0x0804841e <+1>: mov %esp,%ebp
  5. 0x08048420 <+3>: and $0xfffffff0,%esp
  6. 0x08048423 <+6>: sub $0x10,%esp
  7. 0x08048426 <+9>: movl $0x3,0xc(%esp)
  8. 0x0804842e <+17>: movl $0x2,0x8(%esp)
  9. 0x08048436 <+25>: movl $0x1,0x4(%esp)
  10. 0x0804843e <+33>: movl $0x80484f0,(%esp)
  11. 0x08048445 <+40>: call 0x80482f0 <printf@plt>
  12. => 0x0804844a <+45>: mov $0x0,%eax
  13. 0x0804844f <+50>: leave
  14. 0x08048450 <+51>: ret
  15. End of assembler dump.

GDB默认使用AT&T语法显示,当然也可以转换至intel:

  1. (gdb) set disassembly-flavor intel
  2. (gdb) disas
  3. Dump of assembler code for function main:
  4. 0x0804841d <+0>: push ebp
  5. 0x0804841e <+1>: mov ebp,esp
  6. 0x08048420 <+3>: and esp,0xfffffff0
  7. 0x08048423 <+6>: sub esp,0x10
  8. 0x08048426 <+9>: mov DWORD PTR [esp+0xc],0x3
  9. 0x0804842e <+17>: mov DWORD PTR [esp+0x8],0x2
  10. 0x08048436 <+25>: mov DWORD PTR [esp+0x4],0x1
  11. 0x0804843e <+33>: mov DWORD PTR [esp],0x80484f0
  12. 0x08048445 <+40>: call 0x80482f0 <printf@plt>
  13. => 0x0804844a <+45>: mov eax,0x0
  14. 0x0804844f <+50>: leave
  15. 0x08048450 <+51>: ret
  16. End of assembler dump.

执行下一条指令,GDB显示了结束大括号,代表着这里是函数结束部分。

  1. (gdb) step
  2. 7 };

在执行完MOV EAX, 0后我们可以看到EAX就已经变为0了。

  1. (gdb) info registers
  2. eax 0x0 0
  3. ecx 0x0 0
  4. edx 0x0 0
  5. ebx 0xb7fc0000 -1208221696
  6. esp 0xbffff120 0xbffff120
  7. ebp 0xbffff138 0xbffff138
  8. esi 0x0 0
  9. edi 0x0 0
  10. eip 0x804844f 0x804844f <main+50>
  11. ...