Reverse300-3
Hightlighted techniques
- a bit harder ghidra reveng
Learning the game
We are presented with a file called Reverse300-3
. I run the file
command against it:
Reverse300-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=769e50127c2c2acfc39695dd52a429b3b7f510be, for GNU/Linux 3.2.0, not stripped
it seems to be an ELF file, so I’ll be running it with my docker debian container.
1
2
3
4
5
## ./Reverse
Memory initialized. Encoded flag loaded.
Decoding the flag...
octf{
Done.
Playing the game
Initial static analysis
Okay so let’s open up the file in ghidra, all the code I show here has been modified a bit for better understanding
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
undefined8 main(void)
{
char local_38 [32];
local_38[0] = '\x01';
local_38[1] = '\0';
local_38[2] = '\x05';
local_38[3] = '\x01';
local_38[4] = '\x06';
local_38[5] = '\0';
local_38[6] = '\x01';
local_38[7] = '\x01';
local_38[8] = '\x05';
local_38[9] = '\x02';
local_38[10] = '\x06';
local_38[0xb] = '\0';
local_38[0xc] = '\x01';
local_38[0xd] = '\x02';
local_38[0xe] = '\x05';
local_38[0xf] = '\x03';
local_38[0x10] = '\x06';
local_38[0x11] = '\0';
local_38[0x12] = '\x01';
local_38[0x13] = '\x03';
local_38[0x14] = '\x05';
local_38[0x15] = '\x04';
local_38[0x16] = '\x06';
local_38[0x17] = '\0';
local_38[0x18] = '\x01';
local_38[0x19] = '\x04';
local_38[0x1a] = '\x05';
local_38[0x1b] = '\x05';
local_38[0x1c] = '\x06';
local_38[0x1d] = '\0';
local_38[0x1e] = -1;
local_38[0x1f] = '\0';
initialize_memory();
execute_vm(local_38,32);
return 0;
}
So in our main function we have:
- a byte string
local_38
- a call to
initialize_memory()
without any arguments - a call with the following arguments
execute_vm(local_38,32)
let’s take a look at that initialize_memory()
function
Okay so it seems that it is just loading the encoded flag into a static region of memory, presumably to later be decoded (at least partially) by the execute_vm()
function
So now let’s see what the execute_vm()
function does
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
void execute_vm(long instructions,ulong param_2)
{
byte current_value;
ulong i;
long iplus1;
byte operand;
byte operator;
i = 0;
current_value = 0;
puts("Decoding the flag...");
while( true ) {
if (param_2 <= i) {
return;
}
iplus1 = i + 1;
operator = *(byte *)(i + instructions);
i = i + 2;
operand = *(byte *)(iplus1 + instructions);
if (6 < operator) break;
switch(operator) {
case 1:
current_value = memory[(int)(uint)operand];
break;
case 2:
memory[(int)(uint)operand] = current_value;
break;
case 3:
current_value = current_value + memory[(int)(uint)operand];
break;
case 4:
current_value = current_value - memory[(int)(uint)operand];
break;
case 5:
current_value = current_value ^ memory[(int)(uint)operand];
break;
case 6:
putchar((uint)current_value);
break;
default:
goto switchD_004011f2_caseD_6;
}
}
if (operator == 0xff) {
puts("\nDone.");
return;
}
switchD_004011f2_caseD_6:
printf("Unknown instruction: %02x\n",(ulong)operator);
return;
}
Okay so a lot is going on but if we follow the function arguments we can realize a few things to begin with:
param_2
is only used to know for how long to run the while loop- the first parameter (renamed
instructions
) is iterated in batches of two bytes - we can indeed see that the symbol
memory
(where the flag is stored) is being used in some way
After looking at it for a while we realize that we have kind of a custom instructions language. I’ll leave the realizing how it works exactly as an exercise for the reader ;)
Instruction set
Instructions come in pairs
- there is a single register which I’m calling
current_value
- everything is relative to the
memory
symbol address
Operators
We have six different possibilities for an operator
- 1 : copy value at operand offset
- 2 : set value at operand offset equal to
current_value
- 3 : add current value to the value at operand offset and set it
- 4 : sub current value to the value at operand offset and set it
- 5 : xor current value to the value at operand offset and set it
- 6 : print current value
How is the flag decoded?
Now that we have the knowledge of how this internal instruction set works, let’s figure out the flag’s encoding.
We know that the execute_vm
is getting a static set of instructions stored at local_38
in main()
, let’s analyse it and see what it does.
Here’s the full program:
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
local_38[0] = '\x01';
local_38[1] = '\0';
local_38[2] = '\x05';
local_38[3] = '\x01';
local_38[4] = '\x06';
local_38[5] = '\0';
local_38[6] = '\x01';
local_38[7] = '\x01';
local_38[8] = '\x05';
local_38[9] = '\x02';
local_38[10] = '\x06';
local_38[0xb] = '\0';
local_38[0xc] = '\x01';
local_38[0xd] = '\x02';
local_38[0xe] = '\x05';
local_38[0xf] = '\x03';
local_38[0x10] = '\x06';
local_38[0x11] = '\0';
local_38[0x12] = '\x01';
local_38[0x13] = '\x03';
local_38[0x14] = '\x05';
local_38[0x15] = '\x04';
local_38[0x16] = '\x06';
local_38[0x17] = '\0';
local_38[0x18] = '\x01';
local_38[0x19] = '\x04';
local_38[0x1a] = '\x05';
local_38[0x1b] = '\x05';
local_38[0x1c] = '\x06';
local_38[0x1d] = '\0';
local_38[0x1e] = -1;
local_38[0x1f] = '\0';
I’ll reformat it, so we can see the instructions with their operator and operand.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
copy 0
xor 1
print 0
copy 1
xor 2
print 0
copy 2
xor 3
print 0
copy 3
xor 4
print 0
copy 4
xor 5
print 0
-1 0
a bit better, now I’ll “disassemble” this, so it looks even better, keep in mind that operands are always offsets of memory
We can see a pattern here, I’ll guide you through the first few lines:
- copy value at offset 0 into the register
- xor the value at the register with the value at offset 1
- print the value at the register
then we do the same four more times increasing the offsets by two.
So the pattern is pretty simple, the encoding on the flag is a xor of the characters in pairs of two.
Get the full encoded flag and reverse it
To get the string I copied it as “Python byte string” in ghidra
Then with this script we do the reversing
1
2
3
4
5
6
7
8
9
10
11
12
def decode_key(encoded_key: bytes):
decoded_key = ""
for i in range(len(encoded_key)):
try:
decoded_key += chr(encoded_key[i] ^ encoded_key[i+1])
except:
pass
return decoded_key
encoded_key = b'\x70\x1f\x7c\x08\x6e\x15\x60\x17\x64\x14\x4b\x7c\x14\x27\x78\x1f\x2b\x46\x75\x2a\x1b\x2e\x71\x45\x23\x13\x23\x14\x69'
print(decode_key(encoded_key))
for some reason we don’t get the first char: octf{uwsp_7h3_g4m3_15_4f007}
but that’s okay, we know that after all
Flag
user: root: