diff options
Diffstat (limited to 'trees/blog/tfc-2024.typ')
| -rw-r--r-- | trees/blog/tfc-2024.typ | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/trees/blog/tfc-2024.typ b/trees/blog/tfc-2024.typ new file mode 100644 index 0000000..cf40479 --- /dev/null +++ b/trees/blog/tfc-2024.typ @@ -0,0 +1,277 @@ +#import "./html_elements.typ": post + +#show: post + +// --- +// title: "license - tfc 2024" +// date: 2024-08-12T15:26:51-03:00 +// draft: false +// description: "" +// tags: ["rev", "ctf"] +// --- + +This is a writeup for the [TFC CTF 2024](https://ctftime.org/event/2423/). + +For the challenge we are provided with a single binary called `license`. + +Let's check the file: + +```bash +$ file license +license: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=336b3d35e851f9b302e938e557e766e57ed406b7, for GNU/Linux 3.2.0, stripped +``` + +What I expected, a regular linux ELF binary, so let's see the security measures: + +```bash +$ checksec license + Arch: amd64-64-little + RELRO: Full RELRO + Stack: Canary found + NX: NX enabled + PIE: PIE enabled +``` + +Ok, everything looking normal so far. + +When the binary is executed, it asks for a license, this should be the flag we +have to find. + +I open the ELF in _binary ninja_ and go to the decompilation of the `main` +function. + +```c ++0x14ae int32_t main(int32_t argc, char** argv, char** envp) ++0x14ae { ++0x14c4 puts("Please enter your license key to…"); ++0x14e2 fgets(&buffer_start, 18, stdin); ++0x14f1 uint64_t rax = strlen(&buffer_start); ++0x14f1 ++0x14ff if (rax != 0x11) ++0x14ff { ++0x1506 exit(0); ++0x14ff } ++0x14ff ++0x1527 if ((rax != 0 && *(uint8_t*)(rax + 0x405f) == 0xa)) ++0x1538 *(uint8_t*)(rax + 0x405f) = 0; ++0x1538 ++0x1555 strncpy(&first_8, &buffer_start, 8); ++0x155a data_4088 = 0; ++0x155a ++0x1573 if (check_1st_half(&first_8) == 1) ++0x1573 { ++0x157f puts("Nope"); ++0x1589 exit(0); ++0x1573 } ++0x1573 ++0x1597 if (_9th_byte != '-') ++0x1597 { ++0x159e exit(0); ++0x1597 } ++0x1597 ++0x15bc strncpy(&last_8, &buffer_10th, 8); ++0x15bc ++0x15d3 if (check_2nd_half(&last_8) != 1) ++0x15d3 { ++0x15f8 puts("Congrats! Get the flag on remote…"); ++0x1603 return 0; ++0x15d3 } ++0x15d3 ++0x15df puts("Nope"); ++0x15e9 exit(0); ++0x14ae } +``` + +Due to the call to `strlen` and the conditional below it, we know that *the +flag is 17 characters long*. + +Then there is: + +- function that checks the first 8 chars of the flag. +- a conditional to check that the 9th char is `-`. +- a function to analyze the last 8 chars of the flag. + +So we already know that the 9th byte has to be `-`, so the flag format is +`XXXXXXXX-XXXXXXXX`. + +This is the decompilation of the `check_1st_half` function + +```c ++0x1209 int64_t check_1st_half(void* input_buffer) ++0x1209 { ++0x1219 void* fsbase; ++0x1219 int64_t rax = *(uint64_t*)((char*)fsbase + 0x28); ++0x12e6 void to_compare; ++0x12e6 ++0x12e6 for (int32_t i = 0; i <= 7; i += 1) // copy input_buffer to to_compare with a few modifications ++0x12e6 { ++0x1254 int32_t rax_7 = (i % 3); ++0x1254 ++0x1259 if (rax_7 == 2) ++0x12c1 *(uint8_t*)(&to_compare + ((int64_t)i)) = (*(uint8_t*)((char*)input_buffer + ((int64_t)i)) - 0x25); ++0x1259 else if (rax_7 == 0) ++0x1285 *(uint8_t*)(&to_compare + ((int64_t)i)) = (*(uint8_t*)((char*)input_buffer + ((int64_t)i)) ^ 0x5a); ++0x1262 else if (rax_7 == 1) ++0x12a3 *(uint8_t*)(&to_compare + ((int64_t)i)) = (*(uint8_t*)((char*)input_buffer + ((int64_t)i)) + 0x10); ++0x12a3 ++0x12da *(uint8_t*)(&to_compare + ((int64_t)i)) ^= 0x33; ++0x12e6 } ++0x12e6 ++0x12ec int32_t iter = 0; ++0x1328 int64_t result; ++0x1328 ++0x1328 while (true) // this checks if to_compose == global_flag_buffer_0 ++0x1328 { ++0x1328 if (iter > 7) ++0x1328 { ++0x132a result = 0; ++0x132a break; ++0x1328 } ++0x1328 ++0x1317 if (((uint32_t)*(uint8_t*)(&to_compare + ((int64_t)iter))) != ((int32_t)global_flag_buffer_0[((int64_t)iter)])) ++0x1317 { ++0x1319 result = 1; ++0x131e break; ++0x1317 } ++0x1317 ++0x1320 iter += 1; ++0x1328 } ++0x1328 ++0x1333 *(uint64_t*)((char*)fsbase + 0x28); ++0x1333 ++0x133c if (rax == *(uint64_t*)((char*)fsbase + 0x28)) ++0x1344 return result; ++0x1344 ++0x133e __stack_chk_fail(); ++0x1209 } +``` + +Reverse Engineering is the art of understanding how things work. So, reading the code we know that the function: + +- Copies the input buffer to a new buffer, doing a few operations on each character. +- Compares the new buffer against a global buffer, which is `"Xsl3BDxP"` + +So, to solve this part of the challenge we just need to provide 8 characters +that after they're copied to that new buffer they are `"Xsl3BDxP"`. + +There are multiple ways to do this - even just bruteforcing char by char - in +this chal I used [angr](https://angr.io/) to do this. This kind of problem is a classical application of angr - it's a symbolic execution engine - + + +```python +base = 0x400000 + +def first_check(project): + + check0_start = base + 0x1211 + check0_end = base + 0x12f5 + + initial_state = project.factory.entry_state( + addr = check0_start, + add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY, + angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS }) + simulation = project.factory.simgr(initial_state) + + + p0 = claripy.BVS('p0', 8 * 8) + input0_addr = initial_state.regs.rdi + initial_state.memory.store(input0_addr, p0) + + simulation.explore(find=check0_end) + + if simulation.found: + solution_state = simulation.found[0] + print('found!') + + to_compare_addr = solution_state.regs.rbp - 0x10 + constraint_sym = solution_state.memory.load(to_compare_addr, 8) + constraint_value = 'Xsl3BDxP'.encode() + solution_state.add_constraints(constraint_sym == constraint_value) + + print(solution_state.solver.eval(constraint_sym,cast_to=bytes)) + solution = solution_state.solver.eval(p0,cast_to=bytes) + return solution + else: + raise Exception('Could not find the solution') + +``` + +I don't mean to explain how do use angr here, but a quick summary of what this +script does is: + + +- Create a symbolic state where the execution will start (`0x1211` the function's start). +- Create a symbolic variable with 8 bytes. +- Mark where the symbolic variable will be during the initial stage (`RDI`), which is the register for the 1st argument in the [x64 linux call convention](https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame). +- Mark where the execution will stop (`0x12f5` right before comparing the strings). +- Add a constraint that the new buffer (stored at `RBP-0x10`) should be equal to the string that's going to check against. + + +With this script we have the solution for the first half, mazal tov! + +Due to some limitations of angr, the differences in the second function make it +harder to solve using this approach, so for the second function the approach +was different. + +```c ++0x1345 int64_t check_2nd_half(void* input_buffer) ++0x1345 { ++0x1463 for (int32_t i = 0; i <= 7; i += 1) ++0x1463 { ++0x1366 uint16_t* rdx_1 = *(uint64_t*)__ctype_b_loc(); ++0x1366 ++0x1390 if ((((uint32_t)rdx_1[((int64_t)*(uint8_t*)((char*)input_buffer + ((int64_t)i)))]) & 0x200) == 0) ++0x1390 { ++0x13e4 uint16_t* rdx_11 = *(uint64_t*)__ctype_b_loc(); ++0x13e4 ++0x140e if ((((uint32_t)rdx_11[((int64_t)*(uint8_t*)((char*)input_buffer + ((int64_t)i)))]) & 0x100) != 0) ++0x1459 *(uint8_t*)((char*)input_buffer + ((int64_t)i)) = (((int8_t)((((int32_t)*(uint8_t*)((char*)input_buffer + ((int64_t)i))) - 0x30) % 0x1a)) + 0x41); ++0x1390 } ++0x1390 else ++0x13db *(uint8_t*)((char*)input_buffer + ((int64_t)i)) = (((int8_t)((((int32_t)*(uint8_t*)((char*)input_buffer + ((int64_t)i))) - 0x5c) % 0x1a)) + 0x61); ++0x1463 } ++0x1463 ++0x1469 int32_t iter = 0; ++0x1469 ++0x14a5 while (true) // just checking the equality ++0x14a5 { ++0x14a5 if (iter > 7) ++0x14a7 return 0; ++0x14a7 ++0x1494 if (*(uint8_t*)((char*)input_buffer + ((int64_t)iter)) != global_flag_buffer_1[((int64_t)iter)]) ++0x1494 break; ++0x1494 ++0x149d iter += 1; ++0x14a5 } ++0x14a5 ++0x1496 return 1; ++0x1345 } +``` + + +This function is only slightly different: + +- The buffer gets modified in place. +- The modification uses `__ctype_b_loc`. + +This is enough to break a simple angr approach here. + +I wrote a bash script and `gdb` script to run the binary with every possible value for a byte, and print the value of the modified input. I ran this manually for all the bytes until I had the flag at the end. + +```bash +for i in {1..255} +do + I=$(printf '%02x' $i) + printf "${I}\x30\x84\x5a\x61\x9c\x11\x53\x2d\x68\x75\x47\x76\x59\x55\x75\x41" > input_x + Y=$(gdb -q -x ./table.gdb --batch --args ./license | grep '0x555555558090') + echo "$I = $Y" +done +``` + +```bash +break *0x55555555547c +run < input_x +x/8xb $rax +kill +quit +``` |
