简单的栈回溯Trackback文件简析.docx
- 文档编号:10756886
- 上传时间:2023-02-22
- 格式:DOCX
- 页数:18
- 大小:669.66KB
简单的栈回溯Trackback文件简析.docx
《简单的栈回溯Trackback文件简析.docx》由会员分享,可在线阅读,更多相关《简单的栈回溯Trackback文件简析.docx(18页珍藏版)》请在冰豆网上搜索。
简单的栈回溯Trackback文件简析
简单的栈回溯&简单的栈回溯欺骗--简单分析
1.#include
2.#include
3.#include
4.
5.#pragma warning(disable:
4311 4312 4313)
6.
7.int fake_ebp_1, fake_ebp_2;
8.
9.void __stdcall _StackTrace(int StackBase, int ebp, int esp)
10.{
11. int limit = 30, retaddr, calladdr;
12. printf("ebp ret call\n");
13. while ((ebp > esp) && (ebp < StackBase) && (limit--))
14. {
15. retaddr = *(int *)(ebp + 4);
16. calladdr = 0;
17. __try
18. {
19. if (*(unsigned char *)(retaddr - 5) == 0xe8)
20. {
21. calladdr = *(int *)(retaddr - 4) + retaddr;
22. }
23. } __except (EXCEPTION_EXECUTE_HANDLER) {}
24. printf("%08x %08x %08x\n", ebp, retaddr, calladdr);
25. ebp = *(int *)ebp;
26. }
27. printf("trace completed.\n");
28.}
29.
30.__declspec(naked) void __stdcall StackTrace()
31.{
32. // iceboy's stack trace
33. __asm {
34. push esp
35. push ebp
36. push fs:
[0x4] //; StackBase
37. call _StackTrace
38. retn
39. }
40.}
41.
42.void b(int, int)
43.{
44. StackTrace();
45.}
46.
47.void a(int, int, int)
48.{
49. b(0, 0);
50.}
51.
52.int search_call(int fn1, int fn2)
53.{
54. while (true)
55. {
56. if (*(unsigned char *)(fn1++) == 0xe8)
57. {
58. if ((*(int *)fn1 + fn1 + 4) == fn2)
59. {
60. return fn1 + 4;
61. }
62. }
63. }
64.}
65.
66.// fake call
67.__declspec(naked) void __stdcall d(int, int)
68.{
69. __asm {
70. push fake_ebp_1
71. push ebp
72. mov ebp, esp
73. push fake_ebp_2
74. push ebp
75. mov ebp, esp
76. call StackTrace
77. pop esp
78. pop ebp
79. pop eax
80. retn 8
81. }
82.}
83.
84.// fake call & hide self
85.__declspec(naked) void __stdcall e(int, int)
86.{
87. __asm {
88. push ebp
89. mov ebp, [ebp]
90. push 0
91. push 0
92. call d
93. pop ebp
94. retn 8
95. }
96.}
97.
98.void c(int, int, bool hideself)
99.{
100. if (!
hideself)
101. {
102. d(0, 0);
103. } else
104. {
105. e(0, 0);
106. }
107.}
108.
109.int main()
110.{
111. fake_ebp_1 = search_call((int)main, (int)a);
112. fake_ebp_2 = search_call((int)a, (int)b);
113.
114.
115. printf("address of function a:
0x%08x\n", a);
116. printf("address of function b:
0x%08x\n", b);
117. printf("address of function c:
0x%08x\n", c);
118. printf("address of function main:
0x%08x\n", main);
119.
120. printf("\ntest 1:
standard call\n");
121. a(0, 0, 0);
122.
123. printf("\ntest 2:
fake call\n");
124. c(0, 0, false);
125.
126. printf("\ntest 3:
fake call & hide self\n");
127. c(0, 0, true);
128.
129. printf("\npress any key to continue...");
130. _getch();
131. printf("\n");
132. return 0;
133.}
在我的机器上输出如下:
我们就顺着程序来一步一 步分析,需要什么知识点时就说明什么知识点。
首先
1.fake_ebp_1 = search_call((int)main, (int)a);
2.fake_ebp_2 = search_call((int)a, (int)b);
search_call的代码如下:
1.int search_call(int fn1, int fn2)
2.{
3. while (true)
4. {
5. if (*(unsigned char *)(fn1++) == 0xe8)
6. {
7. if ((*(int *)fn1 + fn1 + 4) == fn2)
8. {
9. return fn1 + 4;
10. }
11. }
12. }
13.}
14.首先要知道的是x86平台call指令有很多种,有e8call,ff15call,还有regcall.对于e8call,我们可以计算出call的地址,也就是被调函数首地址,其它的call就困难了许多.那么e8call具体是如何计算机被调函数的首地址的呢?
15.e8是call的指令机器码,这点大家都很清楚。
e8后面的四字节机器码是此时指令指针(EIP)的值与目的地址(被调函数首地址)的差值,也即两地址间的相对偏移。
指令指针在这里可以叫做返回地址,意思就是从被调函数返回后应执行的第一条指令的地址。
16.明白了这些,就很容易计算出目的地址(被调函数首地址):
目的地址= 返回地址+ 相对偏移 举个例子吧:
17.0045F761 > \8B8D D8000000 MOV ECX,DWORD PTR SS:
[EBP+D8]
18.0045F767 . 53 PUSH EBX
19.0045F768 . 03CA ADD ECX,EDX
20.0045F76A . 51 PUSH ECX
21.0045F76B . 50 PUSH EAX
22.0045F76C . E8 0FB14800 CALL SRO_Clie.008EA880
23.看最后一行,E8后的偏移值是0048B10F(十六进制),返回地址应该是0045F76C+5=0045F771,所以目的地址计算出应该是0048B10F+0045F771=008EA880。
看SRO_Clie.008EA880 就知道结果是正确的。
24.上面的代码fn1+4就是返回地址,fn2就是目的地址,*(int*)fn1就是偏移量。
25.综上所述,很容易得出fake_ebp_1是main函数调用a函数的后的返回地址,fake_ebp_2是a函数调用b函数后的返回地址。
接下来
1.printf("address of function a:
0x%08x\n", a);
2.printf("address of function b:
0x%08x\n", b);
3.printf("address of function c:
0x%08x\n", c);
4.printf("address of function main:
0x%08x\n", main);
这几行很好理解,不过反汇编后发下结果输出的并不是这几个函数的真正首地址,而是它们在静态函数跳转表中的地址,如下:
1.00401004 CC int 3
2.@ILT+0(?
d@@YGXHH@Z):
3.00401005 E9 D6 02 00 00 jmp d (004012e0)
4.@ILT+5(?
search_call@@YAHHH@Z):
5.0040100A E9 51 02 00 00 jmp search_call (00401260)
6.@ILT+10(?
c@@YAXHH_N@Z):
7.0040100F E9 1C 03 00 00 jmp c (00401330)
8.@ILT+15(?
a@@YAXHHH@Z):
9.00401014 E9 F7 01 00 00 jmp a (00401210)
10.@ILT+20(?
e@@YGXHH@Z):
11.00401019 E9 F2 02 00 00 jmp e (00401310)
12.@ILT+25(?
_StackTrace@@YGXHHH@Z):
13.0040101E E9 3D 00 00 00 jmp _StackTrace (00401060)
14.@ILT+30(?
StackTrace@@YGXXZ):
15.00401023 E9 48 01 00 00 jmp StackTrace (00401170)
16.@ILT+35(_main):
17.00401028 E9 63 03 00 00 jmp main (00401390)
18.@ILT+40(?
b@@YAXHH@Z):
19.0040102D E9 9E 01 00 00 jmp b (004011d0)
20.00401032 CC int 3
21.00401033 CC int 3
了解了下:
ILT是INCREMENTAL LINK TABLE的缩写,这个@ILT其实就是一个静态函数跳转的表,它记录了一些函数的入口然后跳过去,每个跳转jmp占一个字节,然后就是一个四字节的内存地址,加起为五个字节(计算方法与上面的分析相同)
比如代码中有多处地方调用boxer函数,别处的调用也通过这个ILT表的入口来间接调用,而不是直接call 该函数的偏移,这样在编译程序时,如果boxer函数更新了,地址变了,只需要修改跳表中的地址就可以,有利于提高链接生成程序的效率。
这个是用在程序的调试阶段,当编译release程序时,就不再用这种方法。
继续。
1.printf("\ntest 1:
standard call\n");
2.a(0, 0, 0);
3.分析之前,我们先了解下什么是__declspec(naked)。
4.MSDN上这么说:
5.Forfunctionsdeclaredwiththenakedattribute,thecompilergeneratescodewithoutprologandepilogcode.Youcanusethisfeaturetowriteyourownprolog/epilogcodesequencesusinginlineassemblercode.Nakedfunctionsareparticularlyusefulinwritingvirtualdevicedrivers.Notethatthenakedattributeisonlyvalidonx86,andisnotavailableonx64orItanium.
6.我的理解:
naked顾名思义--赤裸、裸露的意思,用naked修饰的函数(裸函数),当编译器生成代码的时候,在函数开始时没有通常的形成栈的语句块(参数、局部变量的入栈操作),在函数结束时也没有恢复栈的动作(参数、局部变量的出栈操作,以及返回语句)。
我们可以用这个特征来自己完成参数等的入栈出栈操作及返回操作,当然这些操作得用内嵌汇编码的方式完成。
裸函数在写虚拟设备驱动时特别有用。
注意naked属性仅在x86系列CPU中有效,在x64和安腾系列CPU中无效。
7.如果使用_declspec(naked)修饰的话,要注意自己恢复堆栈平衡......
8.现在分析函数调用a(0,0,0)的函数栈帧:
9.
10.可以看到main函数的栈基址ebp等于0012FF80,可以看出上一级的栈基址为0012FFC0,main函数返回地址为00401C89。
11.
12. 从地址0012FF30~0012FF20分别为main函数传递给a函数的参数、返回地址、main函数的栈基址,新的栈基址ebp等于0012FF20。
13.
14.从地址0012FED0~0012FEC4分别为a函数传递给b函数的参数、返回地址、a函数的栈基址,新的栈基址ebp等于0012FEC4。
15.
16.可以看出由于__declspec(naked)的原因,尽管调用了StackTrace函数,但栈基址ebp没变,依然是0012FEC4。
从地址0012FE74~0012FE68一次是返回地址、入栈的esp、入栈的ebp、入栈的fs:
[0x4]。
17.这里需要提一下,线程运行在RING0(系统地址空间)和RING3(用户地址空间)时,FS段寄存器分别指向不同内存段的。
线程运行在RING0下,FS段值是0x3B(WindowsXP下值,在Windows2000下值为0x38);运行在RING3下时,FS段寄存器值是0x30。
18.
19.可以看出当前程序运行在ring0下,所以FS指向的段是GDT中的0x3B段。
该段的长度也为4K,基地址为0xFFDFF000。
该地址指向系统的处理器控制区域(KPCR)。
这个区域中保存这处理器相关的一些重要数据值,如GDT、IDT表的值等等。
下面就是WindowsXPsp2中的KPCR数据结构:
20._NT_TIB
21.+0x000 ExceptionList:
Ptr32 _EXCEPTION_REGISTRATION_RECORD
22.+0x004 StackBase
23.:
Ptr32 Void
24.+0x008 StackLimit
25.:
Ptr32 Void
26.+0x00c SubSystemTib
27.:
Ptr32 Void
28.+0x010 FiberData
29.:
Ptr32 Void
30.+0x010 Version
31.:
Uint4B
32.+0x014 ArbitraryUserPointer :
Ptr32 Void
33.
34.+0x018 Self
35.
36.:
Ptr32 _NT_TIB
到达_StackTrace函数,新的栈基址ebp等于0012FE60,地址从0012FE60~0012FE64依次为上级栈基址、返回地址。
可以简略的画出函数栈帧图如下
可以很容易得看出回溯顺序为main()->a()->b()。
1.printf("\ntest 2:
fake call\n");
2.c(0, 0, false);
3.用上面相同的方法,可以得到函数栈帧简略图如下:
4.
5.
6.
7.可以看出在d函数中在栈上压入了fake_ebp_1和fake_ebp_2,所以回溯的时候的路径与函数调用顺序是不同的,这就是栈欺骗。
8.我们再看
9.printf("\ntest 3:
fake call & hide self\n");
10. c(0, 0, true);
的函数栈帧:
这里最需要我们注意的是e函数中的
1.push ebp
2. mov ebp, [ebp]
3.这里并不是将ebp赋值为当前的esp,而是[ebp],也就是main函数的栈基址,所以在我们回溯的时候就会忽略函数c的函数栈帧,这也是栈欺骗的一个手段。
4.好了,我就分析到这里了。
5.也许有人问我分析这些有什么用?
也许的确用处不大,但是我认为这个过程可以培养我的分析问题的能力,以及程序调试能力。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 简单 回溯 Trackback 文件