Jmp flag
Prologue¶
Difficulty: easy
Category: reverse engineering
Solved: 71
Description
The flag is just a hop, a skip and a jump away.
Input files:
NB:
-
Following indices bases system is used to avoid ambiguity. Whenever element of a collection is referenced by number, 0-based index implied.
Ie, element
0
of list[1, 2, 4, 8, 16]
is1
, Element3
is8
.When element is reference in explanation with word (first, third...), 1-based system is implied.
Ie, first character of string
Hello World!
isH
, fifth iso
.
- Solution code was redacted for readability purposes. Due to time pressure during the competition I was using a lot of one-letter variables and questionable code structure.
- I am using gdb with pwndbg plugin
My struggle¶
Analysis¶
Running the binary doesn't reveal much:
1 2 3 |
|
Let open Ghidra. First thing is to understand the flow of the program: entry
function is language
generated wrapper. And program specific code starts with function FUN_00105300
:
protram entrypoint | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
On success program prints out our input - this means we won't find the flag in the binary. Instead, our goal is to find input that would satisfy algorithm encoded in the program.
win check function | |
---|---|
1 2 3 4 |
|
Function that check if answer is correct or not is very short: our task is to make sure global variable DAT_00109010 is 0. Note that that that DAT_00109010 is 8 byte value with initial value 0xFFFFFFFFFFFFFFFF.
loop function | |
---|---|
1 2 3 4 5 |
|
As we can see this function that is also very short: it converts character that we enter into number (ascii) then multiplies is by 0x80 and uses it as on offset to jump to (base address is 0x00101300). Depending on character, it would jump to:
ascii value | address |
---|---|
0x00 | FUN_00101300 + 0 |
0x01 | FUN_00101300 + 0x80 |
0x02 | FUN_00101300 + 0x100 |
0x03 | FUN_00101300 + 0x180 |
... | ... |
(I ignored +4 in the formula for now for readability purposes - its constant it doesn't change much).
Lets see what is at those addresses. I can see few different type of functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
As a first step I went through all functions and extracted test constants and xor values:
extracted constants
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 59 60 61 62 63 64 65 66 |
|
I can see that there are 64 functions. Each one unsets one specific bit (different for all of them) and also has a different test constant. Because unset bit is always different we have to call all of them exactly once. Here are few first steps of the program:
- We start with global value 0xFFFFFFFFFFFFFFFF
- Only one function can be executed (test for other functions fails): line 54 [0, 0x2000000]
- Now we have global value 0xFFFFFFFFFDFFFFFF
- Only one new function can be executed (test for other functions fails): line 7 [0x2000000, 0x20000]
- Now we have global value 0xFFFFFFFFFDFDFFFF
- Only one new function can be executed (test for other functions fails): line 35 [0x2020000, 0x1000000000000]
- and so on
So, there is only one specific order in which we can call this functions to unset all bits and get global value to 0.
Solution¶
We can get find the order in which functions should be executed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
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 59 60 61 62 63 64 |
|
Now we need to find out what offset corresponds to each function. I don't have it yet and not looking to map each 64 values manually. Therefore, I wrote a script for that:
- calculate address we jump for a character;
- disassemble function at address from step 1. Sample output
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp, rsp 48 8b 05 81 4c 00 00 mov rax, QWORD PTR [rip+0x4c81] 48 ba de c1 c7 02 24 40 c9 71 movabs rdx, 0x71c9402402c7c1de 48 21 d0 and rax, rdx 48 85 c0 test rax, rax 75 27 jne 0x48 48 8b 05 68 4c 00 00 mov rax, QWORD PTR [rip+0x4c68] 48 ba 00 00 00 00 00 10 00 00 movabs rdx, 0x100000000000 48 31 d0 xor rax, rdx 48 89 05 54 4c 00 00 mov QWORD PTR [rip+0x4c54], rax 48 8b 05 4d 4c 00 00 mov rax, QWORD PTR [rip+0x4c4d] 48 85 c0 test rax, rax eb 01 jmp 0x49 90 nop 90 nop 5d pop rbp c3 ret
- For each function check if test_number and xor_number are in the snippet. Given each function has unique test number and xor value - this uniquely identifies snippet to function relation. Few values that were not resolved uniquely I handled manually.
One thing to point out is that Ghidra decoded idea of function FUN_00101280 correctly, but formula turns out to be slightly
different (this doesn't change logic, just different start address and offset multiplier). Correct values are taken disassembly of FUN_00101280
.
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 |
|
Now that we know character for each function and in what order to enter those characters we just loop through them and print out. Full script:
solve.py
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 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 |
|
Epilogue¶
- Official website: https://downunderctf.com/
- Official writeups: https://github.com/DownUnderCTF/Challenges_2024_Public