3 Luckily I could spend two afternoons to work uninterruptedly on challenges during the Asis CTF. Even though I started with a bit of delay on saturday I quickly found a pwn challenge that was already available and still unsolved that looked interesting which I ultimately finished in the late afternoon. I didn't expect to get far on sunday due to less time but decided to take on a Reverse/Misc-challenge because the full python source code was available and it seemed easier to get into than some others, and I managed to finish it in time before the CTF ended as well. I had some cursory looks at other challenges as well but did not seriously attempt any of them.
4 All in all the challenges I solved weren't extremely complicated and I spent more time on them than was probably necessary figuring some things out; especially "Cursed app" was simpler when I analyzed it after having finished it than it seemed like during working on it, but both were interesting nonetheless and were fun to figure out. I was slowed down by not being too familiar with all the relevant assembly instructions in "Cursed app", and wasted some time on both challenges by not analyzing them closely enough before taking them on, but it was a good learning experience.
9 * 60 solves / 90 points
10 * Time spent: ~4 hours
12 If we want to say 100, should we start at 1 and count until 100?
14 sha256(flag) = dd791346264fc02ddcd2466b9e8b2b2bba8057d61fbbc824d25c7957c127250d
17 The "cursed app" was a ELF binary that analyzed the contents of a file passed as an argument; depending on whether it contained the correct file or not it printed a message to let the user know.
18 Looking at the binary in cutter revealed a lot of similar looking blocks that all ended in a conditional jump to the program section that printed the message indicating a wrong flag; the blocks iterated over an ever increasing variable that was probably the content read from the file, and in total 59 positions were handled suggesting that the flag was 59 characters long, of which the first 5 and the last one were known. Storing "ASIS{" in the file provided to the executable and stepping through indeed showed that the first five blocks in question passed successfully but the check in the sixth one jumped to the error message. Now knowing for sure that each code block analyzed a character by reading the byte value, doing calculations based on it and finally checking the results to decide whether to proceed or fail it was simply a matter of reversing the calculations to retrieve the correct characters one by one.
19 The calculations had a similar structure but seemed to vary a bit so I transferred the to python one after another in order to iterate over all printable characters and find the ones for which the calculations worked out; only after having retrieved the flag and cleaning up the script did I realize that not only could most parts be copy-pasted with some values exchanged, but they actually all had the exact same structure if fed with the appropriate values. For example some blocks were missing instructions that were present in most others, but they could simply be performed by the same function by providing parameters that simply neutralized those instructions, such as multiplying values by 1 to simply prevent any changes taking place.
20 The basic structure of the function was as follows:
22 - Read character byte value into EAX, multiply it by some value and then add to it.
23 - Perform a CDQ instruction which sign-extends the value in EAX by copying the sign bit into every bit of the EDX register. Because we know that our bytes are all positive due to representing printable characters and only being multiplied by positive numbers this effectively serves as a way to zero out EDX.
24 - An IDIV instruction is performed which divides EAX by the provided argument and stores the result in EAX and, more importantly, the remainder in EDX. Because EDX wasn't explicitely mentioned in the assembly code this was a bit tricky to figure out when just translating the obvious calculation steps into python because they obviously didn't match and no characters for which they worked out could be found.
25 - The value of EDX is then copied to EAX and, multiplied by some value, into ESI.
26 - EAX and EDX are multiplied by some values.
27 - Values are added to EAX and ESI, this time not by ADD instructions but by way of LEAs.
28 - Finally there's another EAX IDIV followed by a TEST on EDX and a JNE conditional jump; effectively at this point EAX has to be a multiple of the divisor, thus writing a remainder of 0 into EDX which is the only way to avoid a jump to the error message, instead continuing into the next block loading and analyzing the following character.
30 Most of the calculation steps were pretty straight forward, but some such as IDIV and CDQ had some side effects that weren't easily spotted in the disassembly and required some analysis. In some blocks certain instructions were changed, such as a multiplication being replace by a shift, which made them look more different than they actually were. The challenge description was probably a hint to that fact.
31 Transcribing the calculations to python this way allowed for bruteforcing the character in each position, with some being obvious due to the flag format. The following script is a heavily cleaned up version that defines the basic structure that is common to each calculation and provides the appropriate values visible in the disassembly to construct a function for each position into which characters can be passed to check whether they pass the test (returning the remainder of the final division):
38 def calc(char, eax_m_1, eax_a_1, esi_m, edx_m_1, eax_m_2, eax_s_1, esi_s_1):
49 eax = eax + esi + eax_s_1
55 lambda char: 0 if c == "A" else 1,
56 lambda char: 0 if c == "S" else 1,
57 lambda char: 0 if c == "I" else 1,
58 lambda char: 0 if c == "S" else 1,
59 lambda char: 0 if c == "{" else 1,
60 lambda char: calc(char, 91, 2, 0x2f7, 0x30e, 5, 0x4b, 0x55),
61 lambda char: calc(char, 0x43, 0x76, 0xed, 0x106, 0x37d, 0x1f1, 0xdd),
62 lambda char: calc(char, 0xe7, 0x74, 0x287, 0xf5, 0x213, 0x3e5, 0x184),
63 lambda char: 0 if c == "_" else 1,
64 lambda char: calc(char, 0xcb, 0x88, 0x95, 0x78, 0x336, 0x3bd, 0x230),
65 lambda char: calc(char, 0x51, 0x96, 0x2b0, 0x144, 0x9d, 0x21a, 0x265),
66 lambda char: calc(char, 0x29, 0x8d, 0x2db, 0x15, 0x56, 0xcb, 0x203),
67 lambda char: calc(char, 0x73, 0x5f, 0x178, 0x116, 0x2df, 0x88, 0x262), # _
68 lambda char: calc(char, 0xf3, 0xe4, 0xdd, 0xca, 0x31b, 0x37c, 0x33b),
69 lambda char: calc(char, 0x4f, 0x51, 0x345, 0x234, 0x66, 0x366, 0xf9),
70 lambda char: calc(char, 0x7b, 0x8e, 0x2b0, 5, 0x12e, 0x282, 0xd5),
71 lambda char: calc(char, 0xa9, 0x59, 0x272, 0x96, 0xe, 0x2a8, 0x1c8), #_
72 lambda char: calc(char, 0xad, 0xe6, 0x214, 0x19f, 0x33e, 0x12e, 0x1f1),
73 lambda char: calc(char, 0xab, 0x9a, 0x72, 2**5, 0x10f, 0xe2, 0x292),
74 lambda char: calc(char, 0x4b, 0xb8, 0x35a, 0x37d, 0x366, 0x39, 0x1a8),
75 lambda char: calc(char, 0xf5, 0x5b, 0x2f3, 0x6a, 0x375, 0x9d, 0x3d7),
76 lambda char: 0 if c == "_" else 1,
77 lambda char: calc(char, 0x3d, 0x69, 0x3c0, 0x3e, 0x3cb, 0x214, 0x390),
78 lambda char: calc(char, 0x9f, 0x2c, 0x1d8, 0x29, 0x215, 0x7a, 0x32),
79 lambda char: calc(char, 0x77, 0xef, 0x21f, 0x38, 0xa7, 0x308, 0x290), #_
80 lambda char: calc(char, 0xb3, 0x70, 0x3ce, 0x87, 0xf8, 0x19e, 0x52),
81 lambda char: calc(char, 0x47, 0xae, 0x1cf, 0xa, 0x2f3, 0x115, 0x113),
82 lambda char: calc(char, 0xbf, 0x90, 0x2a6, 0x37, 0xe3, 0x234, 0x27a),
83 lambda char: calc(char, 0x51, 0x9b, 0x210, 0x173, 0x379, 0x330, 0x2ea),
84 lambda char: calc(char, 0x57, 0xc8, 0x2e0, 0x2e6, 0x14d, 0x25, 0xd),
85 lambda char: calc(char, 0xa7, 0xa0, 0xc8, 0x382, 0x27c, 0xd5, 0x265),
86 lambda char: 0 if c == "_" else 1,
87 lambda char: calc(char, 0x71, 0xd9, 0x34b, 0x14d, 0x8a, 0xfc, 0x244),
88 lambda char: calc(char, 0xaf, 9, 0x164, 0x69, 0x1ae, 0x2cb, 0x1fc),
89 lambda char: calc(char, 0xdb, 0xa6, 0x2ca, 2, 0xac, 0x32c, 0x124),
90 lambda char: calc(char, 0xb3, 0x39, 0x3be, 0x1f1, 0x358, 0x2b9, 0x3be),
91 lambda char: 0 if c == "_" else 1,
92 lambda char: calc(char, 0x3d, 0xf0, 0x3b9, 0x78, 0x141, 0x1ec, 0x199),
93 lambda char: calc(char, 0x29, 0x76, 0x10c, 0x4a, 0x7b, 0x3c4, 0x14e),
94 lambda char: calc(char, 0x2d, 0x96, 0x210, 0xd, 0x264, 0x16c, 0x27d),
95 lambda char: 0 if c == "_" else 1,
96 lambda char: calc(char, 0x3f, 0xa0, 0x15a, 0xf0, 0x388, 0x34d, 0x3d),
97 lambda char: calc(char, 0xd3, 0x72, 0x22a, 0x93, 0x22f, 0x299, 0x379),
98 lambda char: calc(char, 0x79, 2, 0x3df, 0x82, 0x2cf, 0x32b, 0x195),
99 lambda char: calc(char, 0x75, 0xe6, 0x134, 0x27, 0x21d, 0x12c, 0x29),
100 lambda char: calc(char, 0x51, 0x52, 0x253, 0x23, 0x234, 0x229, 0x205),
101 lambda char: 0 if c == "_" else 1,
102 lambda char: calc(char, 0xed, 0xa5, 0x306, 0x24, 0x203, 0x14b, 0x28c),
103 lambda char: calc(char, 0x81, 0x7f, 0x1a4, 0xb, 0x362, 0x14, 0x15a),
104 lambda char: calc(char, 0x19, 0xeb, 0x67, 0x211, 0xb8, 0x140, 0x17e),
105 lambda char: calc(char, 0x9b, 0x3a, 0x33c, 0x46, 0x357, 0xa0, 0x10e),
106 lambda char: calc(char, 0x55, 0xf0, 0x234, 0xa, 0x3b7, 0xd5, 0x207),
107 lambda char: calc(char, 0x49, 0x3d, 0x79, 0x7, 0x282, 0x362, 0x142),
108 lambda char: calc(char, 0x81, 0x56, 0x326, 0x17, 0x3d, 0x181, 0x14f),
109 lambda char: calc(char, 0x9b, 0xb1, 0xc4, 0x65, 0x18f, 0x24, 0xda),
110 lambda char: calc(char, 0x99, 0x7e, 0x116, 0x3c, 0x360, 0x2aa, 0x142),
111 lambda char: calc(char, 0xe3, 0xf5, 0x152, 0x51, 0x123, 0x1f8, 0x2bc),
112 lambda char: calc(char, 0xc3, 0x16, 0x3ca, 0x148, 0x300, 0x13, 0x1e1)
115 possible_chars = string.printable
117 for i in range(len(calculations)):
118 for c in possible_chars:
119 if calculations[i](c) == 0:
120 print("Found character for index {}: {}".format(i, c))
123 ### Yet funny (Misc/Reverse / 154pts / 29 solves)
124 * Category: Misc/Reverse
125 * 29 solves / 154 points
126 * Time spent: ~7 hours
128 Sometimes you come up with something incredibly stupid yet funny.
131 nc 76.74.177.238 7777
134 This challenge was a python script running on a remote server that consisted of two parts: A proof of work as well as the possibility to modifiy C++-code in a limited way that then was compiled and executed on the server; only if it exited with status 0 the flag was read and printed.
137 The proof of work generated a random string of a random length between 10 and 40, hashed it with one randomly selected function out of five and printed the final 6 characters; the task was to find and enter the correct random string.
138 This was easily solvable by parsing the provided information and bruteforcing strings of the appropriate lengths and hash functions. The actual work that had to be proven wasn't massive bcause the strings mainly consisted of zeroes and only a couple of relevant characters at the end; this was pretty straight forward and mainly a speedbump.
140 #### Randomized C++-program
141 After clearing the proof of work the second part of the program could be accessed: Here a simple C++-function was filled with four randomly generated integers and could be displayed. To get to the flag two of thos values had to be replaced by user input, and only if they then matched a simple formula the information would be displayed.
147 c = first_number, d = second_number;
148 int a = first_random, b = second_random;
155 ```first_random` and ```second_random``` were immutable random numbers while ```first_number``` and ```second_number``` would be replaced by user input. However this user input was filtered: Besides making sure that only printable characters were present, there was an additional function that was executed on each input:
161 vux = inp.replace(list(pm)[0] * 2, chr(0)).replace(list(pm)[1] * 2, chr(1))
162 if set(vux) == set([chr(0), chr(1)]) and len(inp) <1<<10:
163 return True and len(inp) > 4
169 This had me stumbling around for a bit: At first I thought the first two characters of each input had to be 0 and 1, but at some point I realized that the conversions to and from sets indeed had a point: They reduced the input to the unique values present, and by replacing the first two and then comparing the sets this effectively ensured that only two different characters were present; the multiplication of course didn't alter those characters but ensured that only pairs would be replaced, further limiting the input to strings of at least 4 characters, consisting only of pairs of two unique characters.
170 Having cleared that hurdle it was time to think about possible inputs. The first obvious idea was to insert two slashes to comment out parts of the code to alter the variables and thus the calculations, but there was no way to get legal C++-code this way and get the compilation pass. However the compiler errors were helpfully printed out and pointed out a peculiarity in the way the user input was used to subsitute program parts that was present in the available python code:
174 tmp_code = str(tmp_code).replace("first_random", str(first_rand))
175 tmp_code = str(tmp_code).replace("second_random", str(second_rand))
176 tmp_code = str(tmp_code).replace("first_number", str(first_var) + "c+" + str(first_number))
177 tmp_code = str(tmp_code).replace("second_number", str(second_var) + "d+" + str(second_number))
180 Instead of replacing the placeholder text in the source code the script actually inserted the user input followed by the variable name and another addition, resulting in a line like:
183 c = <USER_INPUT>c+<first_number>, d = <USER_INPUT>d+<second_number>;
186 Considering the input could only consist of pairs of the same character there was no way to end the statement early by inserting a semicolon or commenting parts of it out, but there are pairs of characters that could immediately preceed variables and result in legal code: Pre-increments and pre-decrements.
187 While I at first only tested with up to six characters because thats what I had used to analyze the ```aux```-function I at some point realized that it also explicitely checked the input length and not only limited it to being at least 6 characters but also added an upper bound of 1024. Because two different characters were required to be present, resulting in an increment and a decrement that cancelled each other out, this left 509 operations that could be performed on either variable which opened up a huge range of values that could possibly be used in the program.
188 With this information it was again down to bruteforcing the values by iterating over them until a combination was found that satisfied the formula, then generating and submitting the appropriate string.
190 The following steps combine both tasks and exploit the remote server fully automatted retrieving the flag:
193 #!/usr/bin/env python
198 from hashlib import *
199 from itertools import chain, product
201 prompt1_template = "Please submit a printable string X, such that {}(X)[-6:] = {} and len(X) = {}"
202 vars_cd_template = " c = {}, d = {};"
203 vars_ab_template = " int a = {}, b = {};"
204 HASH = {"md5": md5, "sha1": sha1, "sha224": sha224, "sha256": sha256, "sha384": sha384, "sha512": sha512}
206 def pow(algo, hash, len):
207 candidates = product(string.printable, repeat=int(len))
208 for candidate in candidates:
209 if HASH[algo](''.join(candidate)).hexdigest()[-6:] == hash:
210 return ''.join(candidate)
212 def find_solution_strings(a, b, c, d):
213 solution_string = "++--"
214 for c_diff in range(-509, 509):
215 for d_diff in range(-509, 509):
216 if a*(c + c_diff) + b*(d + d_diff) == 1:
217 print("Solution found!")
218 solution_string_c = abs(c_diff)*"--" if c_diff < 0 else c_diff*"++"
219 solution_string_d = abs(d_diff)*"--" if d_diff < 0 else d_diff*"++"
220 return solution_string + solution_string_c, solution_string + solution_string_d
223 r = remote("76.74.177.238", "7777")
225 print("##### Step 1: Proof of work #####")
226 prompt1_request = r.recvline()
227 prompt1_data = parse(prompt1_template, prompt1_request)
228 print("Attempting PoW: {}(X) = {}, len(X) = {}".format(prompt1_data[0], prompt1_data[1], prompt1_data[2]))
229 correct_X = pow(prompt1_data[0], prompt1_data[1], prompt1_data[2])
230 print("Found PoW: {}".format(correct_X))
231 r.sendline(correct_X)
233 print("##### Step 2: Find equation solution #####")
234 r.recvline_endswith("[Q]uit!")
236 r.recvline_endswith("d = 0;")
237 vars_cd_line = r.recvline()
238 vars_ab_line = r.recvline()
239 vars_cd_data = parse(vars_cd_template, vars_cd_line)
240 vars_ab_data = parse(vars_ab_template, vars_ab_line)
241 a = int(vars_ab_data[0])
242 b = int(vars_ab_data[1])
243 c = int(vars_cd_data[0])
244 d = int(vars_cd_data[1])
245 print("Challenge: a = {}, b = {}, c = {}, d = {}".format(a, b, c, d))
246 solution_c, solution_d = find_solution_strings(a, b, c, d)
247 r.recvline_endswith("[Q]uit!")
249 r.recvline_endswith("first variable:")
250 r.sendline(solution_c)
251 r.recvline_endswith("second variable:")
252 r.sendline(solution_d)
253 flag_line = r.recvline()
257 if __name__ == "__main__":