网安导实验三教程

网安导实验三:“逆向工程、漏洞挖掘与利用”教程

逆向工程

逆向工程是指对软件、硬件、芯片等产品进行逆向分析,以获取其设计原理、技术规格、实现方法等技术信息的过程。逆向工程是一种合法的技术手段,但在实际应用中,逆向工程也被用于非法目的,如破解软件、盗版等。逆向工程的主要内容包括逆向分析、逆向设计、逆向仿制等。

网安导实验只涉及软件分析。汇编与 GDB 已在实验二教程中介绍,这里将介绍使用 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
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_SIZE 100000

void obfuscate(char *input)
{
int random_factor[100][3];
srand(0LL);

for (int j = 0; j < 100; j++)
{
for (int k = 0; k < 3; k++)
{
random_factor[j][k] = rand();
}
}

for (int i = 0; input[i] != '\0'; i++)
{
input[i] = abs((input[(input[i + 1] == '\0') ? i + 1 : 0] ^ random_factor[i][0] ^ (input[i] + random_factor[i][1]) % (256 ^ random_factor[i][2])) + random_factor[i][3]) % 95 + 32;
}
}

int main()
{
char input[MAX_SIZE];
scanf("%99s", input);

printf("You input: %s\n", input);

obfuscate(input);

printf("Processed input: %s\n", input);

return 0;
}

现在,我们获得了某个输入的输出,想要找到原始输入。

例子
1
2
3
You input: AAAAB3NzaC1yc2EAAAADAQABAAABgQCnZfmfODcOKr9lrOtKO7rl+KdgPGenju+xfPiiKLT2MbnKUAzn7oXJc3cv50b8OT4vOI4
Processed input: cUw)TJLdO8Ezqi;P?w{5Jaj2^i91UwLRS403dqm0`A* 7iX;5dF DrAM=vzj/gZ^yI;"\+{'Gt-9CrhCej.Ar|#5n!XF'oLg[q?
(假设无输入,推演原始输入)

在此代码中,由于使用了 srand(0LL),所以随机数将是一个固定的序列,而且混淆后的字符串的第 i 位与 ii+1 位有关。即除了最后一位,混淆后的序列的每一位都与原序列的两位有关。然而,其逆向操作代码写起来十分的烦人,需要考虑许多东西;在更多情况下只有二进制,并且文件经过混淆和膨胀难以分析逻辑。

这时,我们可以使用 GDB Python 脚本来自动化调试。其具体思路为,在输入字符串后设置还原点,并在执行 obfuscate 函数后设置断点,每次逐位检测混淆后字符串是否满足条件,逐位增加还原字符串。

script.py
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
import gdb

target = "cUw)TJLdO8Ezqi;P?w{5Jaj2^i91UwLRS403dqm0`A* 7iX;5dF DrAM=vzj/gZ^yI;\"\+{'Gt-9CrhCej.Ar|#5n!XF'oLg[q?"
alphabet = ascii.printable

class ObfuscateBreakpoint(gdb.Breakpoint):
def __init__(self, spec, target):
super().__init__(spec)
self.target = target
self.current_index = 0
self.current_guess = [' '] * len(target)

def stop(self):
frame = gdb.newest_frame()
input_addr = frame.read_var('input')
input_str = input_addr.string()

if input_str == self.target:
print("Found input:", ''.join(self.current_guess))
gdb.execute("quit")
return True

for char in alphabet:
self.current_guess[self.current_index] = char
gdb.execute(f"set input[{self.current_index}] = '{char}'")
gdb.execute("continue")
input_str = input_addr.string()
if input_str[self.current_index] == self.target[self.current_index]:
self.current_index += 1
break

return False

def main():
gdb.execute("file a.out")
gdb.execute("b obfuscate")
gdb.execute("r")
ObfuscateBreakpoint("*obfuscate", target)
gdb.execute("continue")

if __name__ == "__main__":
main()

漏洞挖掘与利用

漏洞挖掘

我们这里介绍最简单的几种漏洞。

栈溢出

栈溢出是一种常见的漏洞,其原理是当程序向栈中写入数据时,如果写入的数据超出了栈的大小,就会覆盖栈中的其他数据,从而导致程序崩溃或执行恶意代码。

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

void target() {
printf("You have reached the target function!\n");
}

void vuln() {
char buf[10];
gets(buf);
printf("You entered: %s\n", buf);
}

int main() {
vuln();
return 0;
}

我们只要在执行 vuln 函数时覆盖其返回地址为 target 函数的地址,就可以执行 target 函数。

字符串格式化漏洞

字符串格式化漏洞是一种常见的漏洞,其原理是当程序使用 printf 等函数输出字符串时,如果字符串中包含格式化字符串,就会导致程序崩溃或执行恶意代码。

例如,对这样的代码:

1
printf("%d %d %d %d %d %d %d %d %d %d\n");

若此代码中的格式化字符串与其后的参数一一对应,那么程序会先将全部参数压栈,然后按照格式化字符串的要求输出参数。而在这里,这个代码会输出栈中的内容,从而泄露栈中的数据。

在实际情况中,由疏忽导致的格式化字符串与参数不匹配的情况并不常见,因为这样的代码会在编译时提示错误。而这是一个更为常见的情况:

1
2
3
4
char buf[100];
gets(buf);
trackle(buf);
printf(buf);

若不对 buf 进行检查,那么用户输入的字符串中就可能包含格式化字符串,从而导致漏洞。