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
|
a
$ ./auth0 $(python -c 'print "A"*512')
ERROR: password incorrect
Segmentation fault
b
With optimisation, `correct` isn't really a variable anymore. The
compiler recognises the moments it can change (i.e. at definition time
and after the strcmp), and when it is used (the last if), and manages
to do the same without the variable.
It now uses code similar to the following control flow:
if (strcmp(hash1,hash2) == 0) {
printf("Starting root shell\n");
setuid(0);
setgid(0);
system("/bin/sh");
} else {
printf("ERROR: password incorrect\n");
}
So, `correct` has been eliminated. Therefore, the buffer overflow
attack doesn't work anymore: there is no variable to overwrite.
We can see this in auth3.s. The `bool correct = false;` statement doesn't
translate to anything in assembly, while it does in auth0.s.
More interestingly, we can see the control flow:
19:auth.c **** if (strcmp(hash1,hash2) == 0) {
99 .loc 1 19 0
100 001f BF000000 mov edi, OFFSET FLAT:.LC1 # tmp90,
100 00
101 0024 B96B0000 mov ecx, 107 # tmp91,
101 00
102 0029 4889C6 mov rsi, rax # hash1, hash1
103 .LBE7:
104 002c F3A6 repz cmpsb
105 .LVL4:
106 002e 7530 jne .L6 #,
107 .LVL5:
108 .LBB8:
109 .LBB9:
20:auth.c **** correct = true;
21:auth.c **** } else {
22:auth.c **** printf("ERROR: password incorrect\n");
23:auth.c **** }
24:auth.c ****
25:auth.c **** if (correct) {
26:auth.c **** printf("Starting root shell\n");
110 .loc 1 26 0
111 0030 BF000000 mov edi, OFFSET FLAT:.LC3 #,
111 00
112 0035 E8000000 call puts #
112 00
113 .LVL6:
27:auth.c **** setuid(0);
114 .loc 1 27 0
115 003a 31FF xor edi, edi #
116 003c E8000000 call setuid #
116 00
The jump in 106 goes to .L6 where the error printf assembly is stored:
22:auth.c **** }
138 .loc 1 22 0
139 0060 BF000000 mov edi, OFFSET FLAT:.LC2 #,
139 00
140 0065 E8000000 call puts #
140 00
If we don't jump (i.e. if the result from strcmp was 0), we continue in
.LVL5, .LBB8, .LBB9 where we printf "Starting root shell" and after do
the set{u,g}id stuff and start the root shell.
In auth0.s we see a control flow that stays much closer to the C code.
There is an actual variable `correct` which is set if the strcmp returns 0:
19:auth.c **** if (strcmp(hash1,hash2) == 0) {
76 .loc 1 19 0
77 004f 488B55E8 mov rdx, QWORD PTR [rbp-24] # tmp88, hash2
78 0053 488B45F0 mov rax, QWORD PTR [rbp-16] # tmp89, hash1
79 0057 4889D6 mov rsi, rdx #, tmp88
80 005a 4889C7 mov rdi, rax #, tmp89
81 005d E8000000 call strcmp #
81 00
82 0062 85C0 test eax, eax # D.3514
83 0064 7506 jne .L2 #,
20:auth.c **** correct = true;
84 .loc 1 20 0
85 0066 C645FF01 mov BYTE PTR [rbp-1], 1 # correct,
Finally: the compiler changes the control flow because it is more efficient.
In this case, I think, it is both timewise and spacewise more efficient: we
store one bool less, and also don't lose time in setting and checking its
value.
|