summaryrefslogtreecommitdiff
path: root/trees/blog/tfc-2024.typ
diff options
context:
space:
mode:
Diffstat (limited to 'trees/blog/tfc-2024.typ')
-rw-r--r--trees/blog/tfc-2024.typ277
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
+```