Home pointer overflow 2024 Reverse300-3
Writeup
Cancel

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

img_27.png

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

img_28.png

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:
Trending Tags