首页
社区
课程
招聘
2
[翻译]野蛮fuzz - part3:尝试理解代码覆盖率
发表于: 2024-8-30 08:59 6596

[翻译]野蛮fuzz - part3:尝试理解代码覆盖率

2024-8-30 08:59
6596

简介

在这一期的“野蛮fuzz”中,我们将继续由菜鸟为菜鸟的模糊测试之旅,尝试理解代码覆盖的概念及其重要性。据我所知,代码覆盖在高层次上是模糊测试器试图追踪/增加模糊测试器输入所能覆盖的目标应用程序代码的程度。其理念是,你的模糊测试器输入覆盖的代码越多,攻击面越大,你的测试就越全面,还有其他一些我目前还不理解的高级概念。

我一直在提升我的pwn技能,但会短暂休息一下,写点C代码,看看@gamozolabs的直播。@gamozolabs在其中一个直播中详细讲解了代码覆盖的重要性,我怎么也找不到那个片段,但我记得大概内容,于是设置了一些测试用例,专门用于我的测试,以演示为什么“愚蠢的”模糊测试器相比于代码覆盖引导的模糊测试器是如此劣势。准备好一些(可能不正确的????)八年级概率理论吧。在这篇博客文章结束时,我们至少应该能够大致理解1990年最先进的模糊测试器是如何工作的。

我们的模糊测试器

我们有这个美丽、无错误、完美编写的单线程jpeg变异模糊测试器,我们已经从之前的博客文章中将其移植到C语言,并为我们的实验目的进行了一些调整。

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include
#include
#include
#include
#include
#include
#include
 
int crashes = 0;
 
struct ORIGINAL_FILE {
    char * data;
    size_t length;
};
 
struct ORIGINAL_FILE get_data(char* fuzz_target) {
 
    FILE *fileptr;
    char *clone_data;
    long filelen;
 
    // open file in binary read mode
    // jump to end of file, get length
    // reset pointer to beginning of file
    fileptr = fopen(fuzz_target, "rb");
    if (fileptr == NULL) {
        printf("[!] Unable to open fuzz target, exiting...\n");
        exit(1);
    }
    fseek(fileptr, 0, SEEK_END);
    filelen = ftell(fileptr);
    rewind(fileptr);
 
    // cast malloc as char ptr
    // ptr offset * sizeof char = data in .jpeg
    clone_data = (char *)malloc(filelen * sizeof(char));
 
    // get length for struct returned
    size_t length = filelen * sizeof(char);
 
    // read in the data
    fread(clone_data, filelen, 1, fileptr);
    fclose(fileptr);
 
    struct ORIGINAL_FILE original_file;
    original_file.data = clone_data;
    original_file.length = length;
 
    return original_file;
}
 
void create_new(struct ORIGINAL_FILE original_file, size_t mutations) {
 
    //
    //----------------MUTATE THE BITS-------------------------
    //
    int* picked_indexes = (int*)malloc(sizeof(int)*mutations);
    for (int i = 0; i < (int)mutations; i++) {
        picked_indexes[i] = rand() % original_file.length;
    }
 
    char * mutated_data = (char*)malloc(original_file.length);
    memcpy(mutated_data, original_file.data, original_file.length);
 
    for (int i = 0; i < (int)mutations; i++) {
        char current = mutated_data[picked_indexes[i]];
 
        // figure out what bit to flip in this 'decimal' byte
        int rand_byte = rand() % 256;
         
        mutated_data[picked_indexes[i]] = (char)rand_byte;
    }
 
    //
    //---------WRITING THE MUTATED BITS TO NEW FILE-----------
    //
    FILE *fileptr;
    fileptr = fopen("mutated.jpeg", "wb");
    if (fileptr == NULL) {
        printf("[!] Unable to open mutated.jpeg, exiting...\n");
        exit(1);
    }
    // buffer to be written from,
    // size in bytes of elements,
    // how many elements,
    // where to stream the output to :)
    fwrite(mutated_data, 1, original_file.length, fileptr);
    fclose(fileptr);
    free(mutated_data);
    free(picked_indexes);
}
 
void exif(int iteration) {
     
    //fileptr = popen("exiv2 pr -v mutated.jpeg >/dev/null 2>&1", "r");
    char* file = "vuln";
    char* argv[3];
    argv[0] = "vuln";
    argv[1] = "mutated.jpeg";
    argv[2] = NULL;
    pid_t child_pid;
    int child_status;
 
    child_pid = fork();
    if (child_pid == 0) {
         
        // this means we're the child process
        int fd = open("/dev/null", O_WRONLY);
 
        // dup both stdout and stderr and send them to /dev/null
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
         
 
        execvp(file, argv);
        // shouldn't return, if it does, we have an error with the command
        printf("[!] Unknown command for execvp, exiting...\n");
        exit(1);
    }
    else {
        // this is run by the parent process
        do {
            pid_t tpid = waitpid(child_pid, &child_status, WUNTRACED |
             WCONTINUED);
            if (tpid == -1) {
                printf("[!] Waitpid failed!\n");
                perror("waitpid");
            }
            if (WIFEXITED(child_status)) {
                //printf("WIFEXITED: Exit Status: %d\n", WEXITSTATUS(child_status));
            } else if (WIFSIGNALED(child_status)) {
                crashes++;
                int exit_status = WTERMSIG(child_status);
                printf("\r[>] Crashes: %d", crashes);
                fflush(stdout);
                char command[50];
                sprintf(command, "cp mutated.jpeg ccrashes/%d.%d", iteration,
                exit_status);
                system(command);
            } else if (WIFSTOPPED(child_status)) {
                printf("WIFSTOPPED: Exit Status: %d\n", WSTOPSIG(child_status));
            } else if (WIFCONTINUED(child_status)) {
                printf("WIFCONTINUED: Exit Status: Continued.\n");
            }
        } while (!WIFEXITED(child_status) && !WIFSIGNALED(child_status));
    }
}
 
int main(int argc, char** argv) {
 
    if (argc < 3) {
        printf("Usage: ./cfuzz \n");
        printf("Usage: ./cfuzz Canon_40D.jpg 10000\n");
        exit(1);
    }
 
    // get our random seed
    srand((unsigned)time(NULL));
 
    char* fuzz_target = argv[1];
    struct ORIGINAL_FILE original_file = get_data(fuzz_target);
    printf("[>] Size of file: %ld bytes.\n", original_file.length);
    size_t mutations = (original_file.length - 4) * .02;
    printf("[>] Flipping up to %ld bytes.\n", mutations);
 
    int iterations = atoi(argv[2]);
    printf("[>] Fuzzing for %d iterations...\n", iterations);
    for (int i = 0; i < iterations; i++) {
        create_new(original_file, mutations);
        exif(i);
    }
     
    printf("\n[>] Fuzzing completed, exiting...\n");
    return 0;
}

这里不会花太多时间讨论模糊测试器的功能(有什么功能?),但关于模糊测试器代码的一些重要事项:

  • 它以文件作为输入,并将文件中的字节复制到一个缓冲区中。
  • 它计算缓冲区的字节长度,然后通过随机覆盖任意字节来变异2%的字节。
  • 负责变异的函数create_new不跟踪哪些字节索引被变异,所以理论上,同一个索引可能会被多次选择进行变异,因此实际上,模糊测试器最多变异2%的字节。

小插曲,抱歉

我们这里只使用了一种变异方法以保持事情的简单性,在此过程中,我实际上学到了一些之前没有清晰考虑过的非常有用的东西。在之前的一篇文章中,我曾尴尬地大声思考并写到,随机比特翻转与随机字节覆盖(翻转?)有多大不同。事实证明,它们非常不同。让我们花点时间看看。

假设我们正在变异一个名为bytes的字节数组。我们正在变异索引5。未变异的原始文件中,bytes[5] == \x41(十进制为65)。如果我们只进行比特翻转,我们在变异这个字节的程度上非常有限。65的二进制表示是01000001。让我们看看任意翻转一位会有多大变化:

  • 翻转第一位:11000001 = 193,
  • 翻转第二位:00000001 = 1,
  • 翻转第三位:01100001 = 97,
  • 翻转第四位:01010001 = 81,
  • 翻转第五位:01001001 = 73,
  • 翻转第六位:01000101 = 69,
  • 翻转第七位:01000011 = 67,
  • 翻转第八位:010000001 = 64。

如你所见,我们被限制在一个非常有限的可能性范围内。

因此,对于这个程序,我选择用一种方法代替这种变异方法,即直接替换一个随机字节,而不是字节内的比特。

易受攻击的程序

我写了一个简单的卡通化程序来演示“愚蠢的”模糊测试器发现漏洞的难度。想象一个目标应用程序在二进制的反汇编视图中有几个决策树。该应用程序对输入执行2-3次检查,以查看其是否满足某些条件,然后再将输入传递给某种易受攻击的函数。我的意思是这样的:

我们的程序正是这样做的,它获取输入文件的字节,并检查文件长度的1/3、1/2和2/3处的字节,看看这些位置的字节是否与一些硬编码的值(任意的)匹配。如果所有检查都通过,应用程序会将字节缓冲区复制到一个小缓冲区中,导致段错误以模拟一个易受攻击的函数。以下是我们的程序:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include
#include
#include
#include
 
struct ORIGINAL_FILE {
    char * data;
    size_t length;
};
 
struct ORIGINAL_FILE get_bytes(char* fileName) {
 
    FILE *filePtr;
    char* buffer;
    long fileLen;
 
    filePtr = fopen(fileName, "rb");
    if (!filePtr) {
        printf("[>] Unable to open %s\n", fileName);
        exit(-1);
    }
     
    if (fseek(filePtr, 0, SEEK_END)) {
        printf("[>] fseek() failed, wtf?\n");
        exit(-1);
    }
 
    fileLen = ftell(filePtr);
    if (fileLen == -1) {
        printf("[>] ftell() failed, wtf?\n");
        exit(-1);
    }
 
    errno = 0;
    rewind(filePtr);
    if (errno) {
        printf("[>] rewind() failed, wtf?\n");
        exit(-1);
    }
 
    long trueSize = fileLen * sizeof(char);
    printf("[>] %s is %ld bytes.\n", fileName, trueSize);
    buffer = (char *)malloc(fileLen * sizeof(char));
    fread(buffer, fileLen, 1, filePtr);
    fclose(filePtr);
 
    struct ORIGINAL_FILE original_file;
    original_file.data = buffer;
    original_file.length = trueSize;
 
    return original_file;
}
 
void check_one(char* buffer, int check) {
 
    if (buffer[check] == '\x6c') {
        return;
    }
    else {
        printf("[>] Check 1 failed.\n");
        exit(-1);
    }
}
 
void check_two(char* buffer, int check) {
 
    if (buffer[check] == '\x57') {
        return;
    }
    else {
        printf("[>] Check 2 failed.\n");
        exit(-1);
    }
}
 
void check_three(char* buffer, int check) {
 
    if (buffer[check] == '\x21') {
        return;
    }
    else {
        printf("[>] Check 3 failed.\n");
        exit(-1);
    }
}
 
void vuln(char* buffer, size_t length) {
 
    printf("[>] Passed all checks!\n");
    char vulnBuff[20];
 
    memcpy(vulnBuff, buffer, length);
 
}
 
int main(int argc, char *argv[]) {
     
    if (argc < 2 || argc > 2) {
        printf("[>] Usage: vuln example.txt\n");
        exit(-1);
    }
 
    char *filename = argv[1];
    printf("[>] Analyzing file: %s.\n", filename);
 
    struct ORIGINAL_FILE original_file = get_bytes(filename);
 
    int checkNum1 = (int)(original_file.length * .33);
    printf("[>] Check 1 no.: %d\n", checkNum1);
 
    int checkNum2 = (int)(original_file.length * .5);
    printf("[>] Check 2 no.: %d\n", checkNum2);
 
    int checkNum3 = (int)(original_file.length * .67);
    printf("[>] Check 3 no.: %d\n", checkNum3);
 
    check_one(original_file.data, checkNum1);
    check_two(original_file.data, checkNum2);
    check_three(original_file.data, checkNum3);
     
    vuln(original_file.data, original_file.length);
     
 
    return 0;
}

请记住,这只是一个类型的条件检查,二进制文件中存在几种不同类型的条件检查。我选择这个条件是因为这些检查非常具体,可以夸张地展示仅靠随机性达到新代码是多么困难。

我们的示例文件,即我们将变异并输入到这个易受攻击的应用程序中的文件,仍然是之前文章中的文件,即带有exif数据的Canon_40D.jpg文件。

1
2
3
4
h0mbre@pwn:~/fuzzing$ file Canon_40D.jpg
Canon_40D.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, Exif Standard: [TIFF image data, little-endian, direntries=11, manufacturer=Canon, model=Canon EOS 40D, orientation=upper-left, xresolution=166, yresolution=174, resolutionunit=2, software=GIMP 2.4.5, datetime=2008:07:31 10:38:11, GPS-Data], baseline, precision 8, 100x68, frames 3
h0mbre@pwn:~/fuzzing$ ls -lah Canon_40D.jpg
-rw-r--r-- 1 h0mbre h0mbre 7.8K May 25 06:21 Canon_40D.jpg

该文件有7958字节长。让我们将其输入到易受攻击的程序中,看看选择了哪些索引进行检查:

1
2
3
4
5
6
7
h0mbre@pwn:~/fuzzing$ vuln Canon_40D.jpg
[>] Analyzing file: Canon_40D.jpg.
[>] Canon_40D.jpg is 7958 bytes.
[>] Check 1 no.: 2626
[>] Check 2 no.: 3979
[>] Check 3 no.: 5331
[>] Check 1 failed.

我们可以看到,索引2626、3979和5331被选中进行测试,并且文件在第一次检查时失败了,因为该位置的字节不是\x6c。

实验1:仅通过一个检查

让我们去掉第二和第三个检查,看看当我们只需要通过一个检查时,愚蠢的模糊测试器对二进制文件的表现如何。

我将注释掉第二和第三个检查:

1
2
3
4
5
check_one(original_file.data, checkNum1);
//check_two(original_file.data, checkNum2);
//check_three(original_file.data, checkNum3);
     
vuln(original_file.data, original_file.length);

现在,我们将使用未修改的jpeg文件,该文件自然不会通过第一个检查,并让我们的模糊测试器对其进行变异并发送到易受攻击的应用程序,希望能导致崩溃。请记住,模糊测试器在每次模糊测试迭代中最多变异7958字节中的159字节。如果模糊测试器随机在索引2626处插入一个\x6c,我们将通过第一个检查,执行将传递到易受攻击的函数并导致崩溃。让我们运行我们的愚蠢模糊测试器100万次,看看我们得到了多少次崩溃。

1
2
3
4
5
6
h0mbre@pwn:~/fuzzing$ ./fuzzer Canon_40D.jpg 1000000
[>] Size of file: 7958 bytes.
[>] Flipping up to 159 bytes.
[>] Fuzzing for 1000000 iterations...
[>] Crashes: 88
[>] Fuzzing completed, exiting...

在100万次迭代中,我们得到了88次崩溃。所以在大约0.0088%的迭代中,我们满足了通过检查1的条件并触发了易受攻击的函数。让我们仔细检查一下我们的崩溃,以确保我们的代码中没有错误(我在QEMU模式下使用AFL对启用所有检查的易受攻击程序进行了14小时的模糊测试,但未能使程序崩溃,所以我希望没有我不知道的错误 )。

1
2
3
4
5
6
7
8
h0mbre@pwn:~/fuzzing/ccrashes$ vuln 998636.11
[>] Analyzing file: 998636.11.
[>] 998636.11 is 7958 bytes.
[>] Check 1 no.: 2626
[>] Check 2 no.: 3979
[>] Check 3 no.: 5331
[>] Passed all checks!
Segmentation fault

所以,实际上将一个崩溃输入提供给易受攻击的程序确实会导致崩溃。很酷。

免责声明:这里会涉及一些数学计算,我不保证这些数学计算是正确的。我甚至向一些非常聪明的人求助,比如@Firzen14,但我仍然对我的数学计算不完全自信,哈哈。不过,我确实进行了数亿次的系统模拟,得到的经验数据结果与可能有误的数学计算结果非常接近。所以,如果它不正确,至少也足够接近证明我想要展示的观点。

让我们尝试弄清楚我们通过第一个检查并导致崩溃的可能性。我们需要通过的第一个障碍是,我们需要选择索引2626进行变异。如果它没有被变异,我们知道默认情况下它不会持有我们需要的值,我们将无法通过检查。由于我们选择变异159次,而我们有7958个字节可供选择,我们变异索引2626处的字节的概率大概接近159/7958,即0.0199798944458407


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-8-30 09:02 被pureGavin编辑 ,原因: 修改内容
收藏
免费 2
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞!
2024-12-14 04:06
mb_zkbvqvuw
你的分享对大家帮助很大,非常感谢!
2024-11-24 20:56
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册