2023Asuri暑假培训-Reverse-1

Re-静态分析入门-1

2023.8.5 by Shen_Fan

本次仅仅简单讲一下静态分析的基本原理和入门需要了解的基础知识

前置知识-1

0x1 一段C代码是如何跑起来的

*以下内容仅作了解

以下是我们作为示例的一段c代码hello.c

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello World!"); //打印Hello World!
return 0;
}

C语言不像python,python作为一门脚本语言,拥有自己的专用解释器来运行。所有的代码会在转换后喂给解释器,再由解释器解释运行。而C语言需要经过编译直接转化为最底层的机器代码,直接喂给CPU运行。因此,C语言的效率远高于python。

C语言一般的编译过程如图:

1
2
graph LR
src[源代码.c]--预处理-->o[预处理文件.i]--编译-->s[汇编.s]--汇编&链接-->e[可执行文件]

接下来,我们将一步步分析编译的过程。

0b01 预处理

1.删除所有的注释

2.拓展宏(#define语句)

3.包含文件(#include语句)

经过预处理的hello.i文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1 "hello_world.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "hello_world.c"
# 1 "E:/mingw64/x86_64-w64-mingw32/include/stdio.h" 1 3
# 9 "E:/mingw64/x86_64-w64-mingw32/include/stdio.h" 3
# 1 "E:/mingw64/x86_64-w64-mingw32/include/crtdefs.h" 1 3

// 以下省略若干行

# 1398 "E:/mingw64/x86_64-w64-mingw32/include/stdio.h" 2 3

# 1 "E:/mingw64/x86_64-w64-mingw32/include/_mingw_print_pop.h" 1 3
# 1400 "E:/mingw64/x86_64-w64-mingw32/include/stdio.h" 2 3
# 2 "hello_world.c" 2


# 3 "hello_world.c"
int main(){
printf("hello world!");
return 0;
}
0b10 编译

编译过程会将预处理文件转化为汇编语言。汇编代码是一门面向底层的低级语言。语句较为简单,例如把某个数字存到寄存器,把寄存器的某些值进行数学运算等等。

hello.i编译得到的汇编文件

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
	.file	"hello_world.c"
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "hello world!\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
leaq .LC0(%rip), %rcx
call printf
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0"
.def printf; .scl 2; .type 32; .endef

0b11 汇编&链接

首先,汇编程序将.s文件转化为二进制的目标文件(.o),然后链接器将库或者是其他文件链接到该文件中产生可直接运行的程序(.exe等等)。

当我们谈到运行C语言程序时,运行的就是最后得到的二进制文件而非从源代码开始再编译一遍。Reverse便是要从最后得到的二进制文件出发尝试还原源码的逻辑。

IDA的功能便是以CPU的视角将二进制文件(一堆看上去意义不明的0和1)解析为一堆汇编代码,然后再根据逻辑划分程序的各个部分并试图还原C语言代码。但已经在编译过程中丢失的东西(如注释)都已经永远的被删除了,留下的只有IDA的推测。

0x2 IDA的基本使用方法

如果你已经下载并查看过IDA,就会发现IDA拥有ida.exe和ida64.exe两个可执行文件。ida.exe用于32位的文件,ida64.exe用于64位的程序。若要查看某个程序是32位还是64位,可以使用exeinfope(仅适用于Windows),linux下的控制台file指令,以及我正在研究的DIE-engine。

将程序拖到IDA的exe文件上,ida就会识别这个程序的信息。一路ok过去后再等待一段时间,IDA就能成功反编译(也有可能寄了)这个程序。IDA的左侧为函数窗口,显示了程序中所有以及被识别了的函数(绝大多数会是库函数),以下为IDA的常用快捷键及功能:

快捷键 功能
F5 一键反汇编当前函数并生成类c代码
shift + F12 一键显示程序中的所有字符串
x 选择某个函数或变量,查看程序所有用到这些的地方
shift + e 选中区域或变量,一键得到其中的数值
/ 在伪代码页面写下注释
u 将某个函数的定义取消
p 在此处定义函数
c 将机器码(16进制的一堆数值)编译为汇编代码

0x3 C语言常见函数

首先向我们走来的是来自stdio.h的输入输出函数

stdin代表一个指向标准输入(键盘)的指针

stdout代表一个指向标准输出(屏幕)的指针

stderr代表一个指向标准错误(也是屏幕)的指针

函数声明 具体作用
int getchar(void) 返回下一个输入单个字符的值
int putchar(int c) 输出单个字符c
char *gets(char *s) 从stdin读取一行到s,若出错返回NULL,否则s
int puts(const char *s) 向stdout写入字符串s和一个换行,返回输出的字符数量
int scanf(const char *format, …) 从stdin读入符合format规则的输入至之后的地址,返回成功读入的变量数量
int printf(const char *format, …) 根据format规则输出到stdout,返回成功打印的字符数量。

然后是来自string.h的字符串函数

size_t是头文件中用来保存大小的类型,是sizeof()的结果

函数声明 具体作用
size_t strlen(const char *str) 返回str指向的字符串的长度
int strcmp(const char* s1, const char* s2) 比较s1和s2指向的字符串,返回0代表相等
char strcpy(char dst, const char*src) 将src所指的字符串复制到dst
void *memset(void *str,int c, size_t n) 复制c到str的前n个字符

前置知识-2

c语言中的位运算

和他的名字一样,位运算就是对变量中的位进行运算。位运算符号一共有6种,如下表

运算符 作用
& 与 位都为1结果才位1
| 或 位存在1结果为1
^ 异或 位不同则结果为1
~ 取反 0->1, 1->0
<< 左移 二进制位左移若干位
>> 右移 二进制位右移若干位,注意有符号数高位补符号位,无符号数高位补0

接下来是每种运算符的运算演示

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
&:
(char) 0x12: 0 0 0 1 0 0 1 0
(char) 0x6B: 0 1 1 0 1 0 1 1
-----------------------------
0 0 0 0 0 0 1 0

|:
(char) 0x12: 0 0 0 1 0 0 1 0
(char) 0x6B: 0 1 1 0 1 0 1 1
-----------------------------
0 1 1 1 1 0 1 1

^:
(char) 0x12: 0 0 0 1 0 0 1 0
(char) 0x6B: 0 1 1 0 1 0 1 1
-----------------------------
0 1 1 1 1 0 0 1
注: c = a ^ b
则 a ^ c == b

~:
char a = 0x12 /*(0 0 0 1 0 0 1 0)*/;
a = ~a;
a == 0xED /*(1 1 1 0 1 1 0 1)*/;

<<:
char a = 0x12 /*(0 0 0 1 0 0 1 0)*/;
a = a << 2;
a == 0x48 /*(0 1 0 0 1 0 0 0)*/;

>>:
char a = 0x89 /*(1 0 0 0 1 0 0 1)*/;
unsigned char b = 0x89 /*(1 0 0 0 1 0 0 1)*/;
a = a >> 2;
b = b >> 2;
a == 0xE2 /*(1 1 1 0 0 0 1 0)*/;
b == 0x22 /*(0 0 1 0 0 0 1 0)*/;


例题-test_your_ida.exe

测试ida是否正常运行的题,对main函数按下F5得到flag。

例题-ida_PRO.exe

根据提示,main函数在按下F5后得到第一部分flag,按下shift + f12查看字符串得到第二部分flag,注意到字符串里有个让我们按x查引用的字符串,对他按x进入函数后得到最后一部分flag。

例题-bit_operation.exe

由strlen的判断得知flag长度为37.

观察加密函数encrypto发现每个字符都被前四位和后四位调换后异或上了0x11,故考虑将其先异或0x11再进行前四位和后四位调换以获得flag。