逆向基础知识

逆向基础知识

0. 目标

本文通过一个简单的c程序,简要介绍逆向相关的基础知识。

1. 前置知识

  • 会c语言的基础语法,了解即可
  • 知道x86的常用寄存器和汇编指令
  • 知道一些常用的gdb的命令

2. c程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int sum(int a, int b) {
return a + b;
}

int main() {
int a = 10;
int b = 20;
int x = sum(a, b);
printf("sum=%d\n", x);
return 0;
}
1
2
3
4
# 编译,最好加上
# -g 用于gdb匹配到c代码
# -O0 告诉编译器不要优化, 如果被优化了,有些指令的顺序可能是乱的
gcc -g -O0 test.c -o test

3. 使用gdb调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
gdb test

# 启动tui,展示汇编指令
layout asm

# 展示通用寄存器的值, 如果程序还没开始执行,会显示Unavailable
layout regs

# 在main函数加断点
b main

# 查看所有断点
i b

# 执行单条指令, 如果遇到函数调用,会进入函数内
si

# 执行单条指令,如果遇到函数调用,不进入函数内
ni

# 查看栈中的数据
x/16xd $rsp

# 查看eflag
i r eflags

# 查看基地址
info proc mappings

4. 反汇编说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 默认是gnu风格汇编,这里改成intel风格
objdump -M intel -d test

# 这里只列出了关键的与c代码匹配的汇编,其他段暂时不相关
0000000000001149 <sum>:
1149: f3 0f 1e fa endbr64

# 保存上个栈帧的起始地址
114d: 55 push rbp

# 新建栈帧
114e: 48 89 e5 mov rbp,rsp

# 把第一个参数拷贝到栈上
# 细心的人,起始会发现一个问题,这里少移动rsp的动作,还没分配栈空间,就写入数据?其实这里利用了 x86_64 ABI 的 红区(Red Zone) 特性
# 因为sum函数中,没有调用其他函数,所以被称为叶子函数,所以栈顶下面的128字节是安全区,内核和信号处理不会破环,编译器为了少一条指令,所以去掉了分配的动作
1151: 89 7d fc mov DWORD PTR [rbp-0x4],edi

# 把第二个参数拷贝到栈上
1154: 89 75 f8 mov DWORD PTR [rbp-0x8],esi

1157: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
115a: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]

# eax=eax+edx
# 按规范,eax被用作函数返回值
115d: 01 d0 add eax,edx

# 切回到上个栈帧
115f: 5d pop rbp

# ret是call的逆操作
# ret指令相当于如下操作:
# pop rip ; 弹出返回地址,跳回 main
# 相当于
# 1. rip=rsp
# 2. rsp=rsp-0x8 这行就是把1185的call指令保存的rip的位置释放掉
1160: c3 ret

0000000000001161 <main>:
# 没球用
1161: f3 0f 1e fa endbr64

# 把rbp压栈,rbp保存的是当前栈帧的起始地址,
# 当内核加载完程序后,因为main函数并不是第一个被调用的函数,所以也是要返回到上层栈帧的,所以要先保存rbp的值到栈顶。
1165: 55 push rbp

# 把rsp保存到rbp中,相当于创建新栈帧
1166: 48 89 e5 mov rbp,rsp

# rsp=rsp-0x10, 在栈上分配16字节
# 因为栈底在高地址,所以rsp向下移动,表示分配栈空间
1169: 48 83 ec 10 sub rsp,0x10

# 给[rbp-0xc]块内存写入0xa,占用大小为4字节,DWORD(即double word),即a=10
116d: c7 45 f4 0a 00 00 00 mov DWORD PTR [rbp-0xc],0xa

# 给[rbp-0x8]块内存写入0x14, 即a=20
1174: c7 45 f8 14 00 00 00 mov DWORD PTR [rbp-0x8],0x14

# 让edx=0x14
117b: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]

# 让eax=0xa
117e: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]

# 前面4条指令,都是废话,起始可以直接给esi和edi赋值的
# 之所以要给这俩寄存器赋值,是为了要为函数调用传参,edi是第一个参数,esi是第二个参数
# 让esi=0x14
1181: 89 d6 mov esi,edx
# 让edi=0xa
1183: 89 c7 mov edi,eax

# 终于调用到sum函数了
# 这里的call指令相当于如下两个步骤
# push rip ; 压入返回地址
# jmp sum ; 跳转到 sum
# 这里是跳转到1149,1149是相对地址
# info proc mappings 可以查看基地址, 或者在gdb调试时,可以看到call 0x555555555149, 0x555555555149=0x555555554000+1149
1185: e8 bf ff ff ff call 1149 <sum>

# 把eax返回值,写入到栈上
118a: 89 45 fc mov DWORD PTR [rbp-0x4],eax
118d: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]

# 下面是调用printf函数
# 把eax做为第二个参数
1190: 89 c6 mov esi,eax

# 计算一个地址,放入eax
1192: 48 8d 05 6b 0e 00 00 lea rax,[rip+0xe6b] # 2004 <_IO_stdin_used+0x4>

# 把填入rdi,作为第一个参数
1199: 48 89 c7 mov rdi,rax

# eax=0x0
119c: b8 00 00 00 00 mov eax,0x0

# 调用printf
11a1: e8 aa fe ff ff call 1050 <printf@plt>

# 把main函数的返回值设为0
11a6: b8 00 00 00 00 mov eax,0x0

# ?
11ab: c9 leave

# main函数返回,与sum函数退栈帧没区别
11ac: c3 ret