Home Pointer Overflow 2024 Challenges
Ctf-event
Cancel

Pointer Overflow 2024 Challenges

Pointer Overflow 2024 challenges

Overview

ChallengeDifficultyPointsCategoryFlag
Reverse100-1easy100reversingpoctf{uwsp_1n_w1n3_7h3r3_15_7ru7h}
Reverse100-2easy100reversingpoctf{uwsp_d0_0r_d0_n07}
Reverse100-3easy100reversingpoctf{uwsp_br3v17y_15_7h3_50u1}
Reverse200-1Easy200reversingpoctf{uwsp_4_7h1n6_0f_b34u7y}
Reverse200-2Easy200reversingpoctf{uwsp_7h3_n16h7_15_d4rk}
Reverse200-3Easy200reversingpoctf{uwsp_1_4m_7h3_0c34n}
Reverse300-1Easy300reversingpoctf{uwsp_7h3_w0rld_15_4_57463}
Reverse300-2Medium300reversingpoctf{uwsp_4b4nd0n_4ll_h0p3}
Reverse300-3Medium300reversingpoctf{uwsp_7h3_g4m3_15_4f007}
Reverse400-1Hard400reversingpoctf{uwsp_4ll_7h47_gl1773r5}

Reverse100-1

Highlighted techniques

  • how to patch files in ghidra
  • how to retype strings to make them readable in ghidra
  • small gdb tutorial on how to set a breakpoint and skip the function with jump

Learning the game

We are presented with a file called Reverse100-1. I run the file command against it:

Reverse100-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9604fd8c1b649b4686112951a38c3b7280449fc5, 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
## ./Reverse100-1
Encoded flag: ΑΑ̠ȗ̠̍ʠȍȗ

The program seems to be printing out the flag after it has had some sort of transformation applied, time to put on the gloves.

Playing the game

Alright time to get a bit more serious, I’ll be showcasing two ways to solve this, by performing dynamic and static analysis.

For the sake of understanding we’ll start by looking at the program through the eyes of the dragon (ghidra, you guessed correctly)

We saw from the file command that this program is not stripped so ghidra should have plenty of information to work with.

decompiled “main” function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
undefined8 main(void)

{
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined7 local_20;
  undefined4 uStack_19;
  
  local_38 = 0x77757b6674636f70;
  local_30 = 0x31775f6e315f7073;
  local_28 = 0x33723368375f336e;
  local_20 = 0x7572375f35315f;
  uStack_19 = 0x7d6837;
  obfuscate(&local_38);
  printf("Encoded flag: %s\n",&local_38);
  return 0;
}

Hmm… Okay so let’s break it down:

  • first of all we can see a bunch of weird variables with unrecognized types being defined, we’ll come back to it later
  • after that we have a call to obfuscate() with a pointer to local_38 (one of our weird variables) being passed as an argument
  • then we have the following call printf("Encoded flag: %s\n",&local_38);

So we can clearly guess that the last two lines we identified are obfuscating/encoding the flag and then printing it, also the obfuscation is performed in place since both the obfuscation function and the print function receive the same pointer.

Let’s solve the mistery of those weird variables, you may have already guessed that they are actually meant to be a single variable, a string, the flag. Why does ghidra show us a long string like this? Well because the disassembly looks like this:

img.png

As you can see there are a bunch of weird values being moved into registers, ghidra sees each of these as a new variable, but we can solve that.

How to spoon feed a string to Ghidra

  • Right click the first variable generated and look for the “Retype Variable” option (or do Ctrl+L)

img_1.png

  • Retype it to char[n] where n is the amount of characters in the desired string, to get that you can add up how many bytes each of the variables you want to merge is using, but in this case since we only have this string as a variable we can check the function prologue to know how many bytes we need (remember that values in the disassembly are in hex)

img_2.png img_3.png

img_4.png

It’s not perfect but a lot more readable than before, with this we could already reconstruct the entire flag by hand.

What if I don’t want to rebuild the flag like a LEGO?

Okay then you could create a script for ghidra that gets the variable and reconstructs it… Okay let’s just see how to patch the program to make the obfuscation never happen.

  • We go to the call to obfuscate in the disassembly and look for the Patch Instruction option (or use Ctrl+shift+G)

img_5.png

  • We’ll patch it so it becomes a NOP instruction, I don’t know if there’s a NOP instruction that is long enough but I just added two of them

img_6.png

after this we can export the program and run it.

  • press O to export the program, select “Original File”, change it’s name and then click “Ok”

img_7.png

  • after this you can run your patched file.

img_8.png

Extra: How to do it all from the terminal and feel like a superhero

Lastly I’ll show you how to solve this from a linux terminal (bash) using gdb (dynamic analysis).

  • run the program with gdb
1
gdb Reverse100-1
  • set a breakpoint in the obfuscate function (we can do this since the program has symbols left on it)

img_9.png

  • Run the program and let it hit the breakpoint, let them come to us.

img_10.png

  • Now, enter the matrix by enabling the disassembly layout
layout asm

img_11.png

  • From here we can see everything, and by everything I mean that we can see the instruction we are currently about to execute and what follows. Let’s show a bit more of our power and jump straight to the prologue of the function, ignoring the rest of the instructions.

img_12.png

BOOM!!

Got ‘em, they don’t even know what hit them ;)

Flag

poctf{uwsp_1n_w1n3_7h3r3_15_7ru7h}

Reverse100-2

Hightlighted techniques

  • symbolic execution with angr

Learning the game

We are presented with a file called Reverse100-2. I run the file command against it:

Reverse100-2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=48d49fdc0741aa19b895d5fec898bd484d2eb49e, 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
## ./Reverse100-2
Enter the password: asdasd
Access denied!

The program asks for a password and later displays the Access denied! text.

Playing the game

Okay so this structure of challenge tempts me to go for an easy angr solve, but then when I looked at the output of the strings command looking for the target text to use for stdout I see this:

1
2
3
4
5
6
7
8
9
10
...
poctf{uwH
sp_d0_0rH
p_d0_0r_H
d0_n07}
Enter the password: 
%99s
Access granted!
Access denied!
...

sooo, the flag seems to already be there, this is that string fixed in a single line:

poctf{uwsp_d0_0rp_d0_0r_d0_n07}

Hm, looks kinda weird, we can probably guess the flag from here but let’s just do a simple angr script to make sure.

1
2
3
4
5
6
7
import angr

project = angr.Project("Reverse100-2", auto_load_libs=False)

simgr = project.factory.simgr()

print(simgr.explore(find=lambda state: b"granted" in state.posix.dumps(1)).found[0].posix.dumps(0))
1
b'poctf{uwsp_d0_0r_d0_n07}\x00\x00\x00\x00...\x00'

It works!! (with some extra trailing \x00)

Flag

poctf{uwsp_d0_0r_d0_n07}

Reverse100-3

Learning the game

We are presented with a file called Reverse100-3. I run the file command against it:

Reverse100-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a372e4d793f897e5a6c3382036f11a4a12f029a2, 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
## ./Reverse100-3
Encoded flag: Flag after reverse step 0: 8e79a99cacd5c5c7917aa58ab88dc6815583a5597bb987b851697b58bb8bcd
Decode function not added yet!Decoded flag (plaintext in hex): Flag after reverse step 0: 8e79a99cacd5c5c7917aa58ab88dc6815583a5597bb987b851697b58bb8bcd

We are also provided with the following source code

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
#include <stdio.h>
#include <string.h>

// Convert each byte of the flag to hex and print it
void print_flag_hex(unsigned char *flag, int length, int step) {
    printf("Flag after reverse step %d: ", step);
    for (int i = 0; i < length; i++) {
        printf("%02x", flag[i]);  // Print each byte in hexadecimal
    }
    printf("\n");
}

// Reverse the modification of the flag bytes based on the seed
void reverse_modify_flag(unsigned char *flag, unsigned int seed) {
    int length = strlen((char *)flag);

    for (int i = 0; i < length; i++) {
        flag[i] = (flag[i] - (seed % 10)) % 256;  // Reverse each byte modification
        seed = seed / 10;
        if (seed == 0) {
            seed = 88974713;  // Reset seed if it runs out
        }
    }
}

int main() {
    unsigned char encoded_flag[] = { 0x8e, 0x79, 0xa9, 0x9c, 0xac, 0xd5, 0xc5, 0xc7, 0x91, 0x7a, 0xa5, 0x8a, 0xb8, 0x8d, 0xc6, 0x81, 0x55, 0x83, 0xa5, 0x59, 0x7b, 0xb9, 0x87, 0xb8, 0x51, 0x69, 0x7b, 0x58, 0xbb, 0x8b, 0xcd};

    unsigned int seed = 88974713;
    int length = sizeof(encoded_flag);

    printf("Encoded flag: ");
    print_flag_hex(encoded_flag, length, 0);

    // Reverse the modifications 10 times (finish this!)
    printf("Decode function not added yet!");

    printf("Decoded flag (plaintext in hex): ");
    print_flag_hex(encoded_flag, length, 0);  // Print final decoded flag in hex

    return 0;
}

Playing the game

Seems like we just need to modify the source code to call the reversing function in a loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
int main() {
    unsigned char encoded_flag[] = { 0x8e, 0x79, 0xa9, 0x9c, 0xac, 0xd5, 0xc5, 0xc7, 0x91, 0x7a, 0xa5, 0x8a, 0xb8, 0x8d, 0xc6, 0x81, 0x55, 0x83, 0xa5, 0x59, 0x7b, 0xb9, 0x87, 0xb8, 0x51, 0x69, 0x7b, 0x58, 0xbb, 0x8b, 0xcd};

    unsigned int seed = 88974713;
    int length = sizeof(encoded_flag);

    printf("Encoded flag: ");
    print_flag_hex(encoded_flag, length, 0);

    for(int i = 0; i < 10; i++){
        reverse_modify_flag(encoded_flag, seed);
            printf("Decoded flag (plaintext in hex): ");
        print_flag_hex(encoded_flag, length, i);  // Print final decoded flag in hex
    }

    return 0;
}

We compile and run this…

1
2
3
##.\a.exe
Encoded flag: Flag after reverse step 0: 8e79a99cacd5c5c7917aa58ab88dc6815583a5597bb987b851697b58bb8bcd
Decoded flag (plaintext in hex): Flag after reverse step 10: 706f6374667b757773705f627233763137795f31355f3768335f353075317dcf00000079a54d050a0000008000400010ff610054ff610088124000010000008815c1008822c100fdffffff020000000000000054ff6100cd88c5750070360084ff6100f512400001000000000000000000000000000000000000000000000000000000a97bd97500703600907bd975dcff6100cbc0cd770070360081430b0d000000000000000000703600000000000000000000000000000000000000000000000000000000000000000000000000

Then copy whatever this is into cyberchef to get a string

img_15.png

Flag

poctf{uwsp_br3v17y_15_7h3_50u1}

Reverse200-1

Hightlighted techniques

  • ghidralib for semi-automatic string extraction

Learning the game

We are presented with a file called Reverse. I run the file command against it:

Reverse200-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ee89e6f8d8bc723c2eabc56f150f344af85be5f3, 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
## ./Reverse200-1
Obfuscated Flag (Hex): 73 0b 22 21 1e 1c 11 22 21 73

seems to print the obfuscated flag

Playing the game

I opened the file in ghidra and modified it a bit to make it more readable

img_13.png

img_14.png

The actual flag seems to be redacted but there’s a call to a obfuscate() function and then every byte of the string is being printed in hex

This is the code of the obfuscate() function:

img_16.png

There’s also a deobfuscate() function, but it seems to not do anything

img_17.png

Reading the assembly for this function we can see that it is simply building a string in a variable and then not using it for anything, that’s why the decompiler is not producing any code

img_18.png

Let’s first create the reversed obfuscate function and then come back to what the flag is. The obfuscate() function seems to simply do a xor and add a constant to the result, so we just reverse that by subtracting the same constant and performing the same xor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def main():
    encoded_flag = ""
    decoded_flag = ""

    for char_i in range(0, len(encoded_flag), 2):
        try:
            current_char = encoded_flag[char_i: char_i + 2]
        except IndexError:
            current_char = encoded_flag[char_i:]
        decoded_char = chr((int(current_char, 16) - 0x03) ^ 90)
        print(decoded_char)
        decoded_flag += decoded_char

    print(decoded_flag)

Now let’s fill the encoded_flag variable

From the assembly we had before we can just get the full string, but there’s an important thing to take into account, not all MOVs are performed equally The second to last MOV actually overwrites some of the characters from the one done before (at 0x0040121d)

img_19.png

To avoid any mistake when copying or transforming the string, it’s better to do it in an automated way, so for this I will be showcasing ghidralib.

You can go read the documentation for a more in depth tutorial, but basically I will:

  • Use the emulator to emulate the desired function from 0x004011c5 to 0x0040123d
  • get the value from the stack

so the variable that the function builds seem to be 0x48 long but values are only loaded up to RBP - 0x40.

img_20.png

We can put this result into our script anddddd…

img_21.png

Flag

poctf{uwsp_4_7h1n6_0f_b34u7y}

Reverse200-2

Hightlighted techniques

  • symbolic execution doesn’t work
  • still using angr to get data dynamically
  • reversing said data

Learning the game

We are presented with a file called Reverse200-2. I run the file command against it:

Reverse200-2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=11b0463ea4dbbd923c4513ffb7da9e6d5bf1cfb0, 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
## ./Reverse200-2
Enter the correct input: asdfasdf
Incorrect input. Try again.

Asks for an input and then says it’s incorrect

Playing the game

I tried going for the simple angr technique, but it is not able to solve the correct input, so I used angr but not to directly get the correct input

Looking at the file in ghidra we find that there’s a function check_input() which does the following

  • creates a variable with a static value
  • gets user input into another variable
  • transforms user input with a call to transform()
  • compares the transformed user input to the first variable

img_22.png

We quickly realize that the transformed user input has to match the static variable which we already have so the following steps are:

  • reverse the transform() function
  • apply the inverse of transform() to local_28

The transform() function is really simple, for every character, it XORs it with 0x3f and then adds 5

img_23.png

I made Python script to reverse this

1
2
3
4
5
6
def solve(encoded_flag: bytes):
    decoded_flag = ""
    for b in encoded_flag:
        decoded_b = chr((b - 5) ^ 0x3f)
        decoded_flag += decoded_b
    return decoded_flag

Now we need to get the static byte string from the program, for this I will show how you can do it in an interesting way using angr.

Get values from memory using angr

First we need to get the assembly of the program, for that we will use objdump

objdump -d Reverse200-2 > dump.s

In the assembly we look for the memory address where the desired string is already stored in memory. To achieve this I looked for the call to strcmp so I can read the value from the argument

...
401236:	48 8d 45 c0          	lea    -0x40(%rbp),%rax
40123a:	48 89 c7             	mov    %rax,%rdi
40123d:	e8 34 ff ff ff       	call   401176 <transform>
401242:	48 8d 55 e0          	lea    -0x20(%rbp),%rdx
401246:	48 8d 45 c0          	lea    -0x40(%rbp),%rax
40124a:	48 89 d6             	mov    %rdx,%rsi
40124d:	48 89 c7             	mov    %rax,%rdi
401250:	e8 1b fe ff ff       	call   401070 <strcmp@plt>
401255:	85 c0                	test   %eax,%eax
...

We see that pointers to the arguments are stored in rsi and rdi, to know which is which we can also see a bit further up that the argument to transform() (which is the user input) is stored in rbp - 0x40, a pointer to this is later being moved to rax and finally to rdi before strcmp(), meaning that the other argument is the static string local_28

1
2
3
4
5
6
7
8
9
10
11
12
13
import angr


def get_encoded_flag():
    project = angr.Project("Reverse200-2", auto_load_libs=False)

    simgr = project.factory.simgr()

    result = simgr.explore(find=0x401250)

    if result.found:
        state: angr.SimState = result.found[0]
        return state.solver.eval(state.memory.load(state.regs.get("rsi"), 29), cast_to=bytes)

I’ll explain the important lines of this script

1
2
3
4
project = angr.Project("Reverse200-2", auto_load_libs=False)
simgr = project.factory.simgr()

result = simgr.explore(find=0x401250)

The first two lines simply get the simulation manager as usual.

The next line looks for a state located in the call to strcmp()

1
2
3
if result.found:
    state: angr.SimState = result.found[0]
    return state.solver.eval(state.memory.load(state.regs.get("rsi"), 29), cast_to=bytes)

if a state is found, we get the value from memory, lets break up that final line a little bit

1
2
3
rsi_value = state.regs.get("rsi")
unsolved_value = state.memory.load(rsi_value, 29)
return state.solver.eval(unsolved_value, cast_to=bytes)
  • the rsi register holds a pointer to the string we want, so by doing state.regs.get("rsi") we get that pointer as a memory address
  • we load the value at that address into a variable with state.memory.load which takes two arguments
    • a memory address (we use the pointer from rsi here)
    • an integer representing how many bytes we want to read
  • state.solver.eval is used to solve the value, since state.memory.load returns a BV object (there doesn’t seem to be an easier way to read the value as a string even if it is solved)

Here’s the whole code

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
import angr

def get_encoded_flag():
    project = angr.Project("Reverse200-2", auto_load_libs=False)


    simgr = project.factory.simgr()
    simgr.use_technique(angr.exploration_techniques.DFS())

    result = simgr.explore(find=0x401250)

    if result.found:
        state: angr.SimState = result.found[0]
        return state.solver.eval(state.memory.load(state.regs.get("rsi"), 29), cast_to=bytes)


def solve(encoded_flag: bytes):
    decoded_flag = ""
    for b in encoded_flag:
        decoded_b = chr((b - 5) ^ 0x3f)
        decoded_flag += decoded_b
    return decoded_flag


flag = solve(get_encoded_flag())

print(flag)

Flag

poctf{uwsp_7h3_n16h7_15_d4rk}

Reverse200-3

Hightlighted techniques

Learning the game

We are presented with a file called Reverse. I run the file command against it:

Reverse200-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=932c681f4a9c2be824bce27856fe7ee8212bb7f1, 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
## ./Reverse200-3
Enter the correct input: sadfsa
Incorrect input! Try again.

Playing the game

To begin this challenge I used the strings command and found a few interesting things

1
2
3
4
5
6
7
8
9
10
11
12
13
...
H=@@@
*REDACTEH
optc{fwuH
ps1_4__mH
_4__mh7_H
3c043}n
Correct!
Enter the correct input: 
%29s
Incorrect input! Try again.
;*3$"
...

So we have what looks like the flag but a bit scrambled, this is the fixed string

optc{fwups1_4__m_4__mh7_3c043}n

it looks like the characters are switched in pairs of two, another clue of this is what we get when trying to solve this with angr

img_24.png

looks like the *REDACTED* string with characters switched, so let’s try that.

I got: poctf{uwsp_1_4m_4___hm_7c340}3n

Hmmm, so it looks like the flag starts fine, but then it kinda does not make sense.

What I realize is that the last n should switch places with the } but it is not doing so, this gives me an idea, let’s replace half the characters starting from the left and the other half from the right

poctf{uwsp_1_4m_7h3_0c34n}

Nicee it worked, here’s the final script used.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scrambled_flag = "optc{fwups1_4__m_4__mh7_3c043}n"

def unscramble(flag):
    unscrambled_left = ""
    unscrambled_right = ""
    for i in range(0, len(flag) // 2, 2):
        unscrambled_left += "".join(reversed(flag[i:i + 2]))
    for i in range(len(flag)+1, (len(flag) // 2) + 6, -2):
        unscrambled_right = flag[i:i-2:-1] + unscrambled_right

    return unscrambled_left + unscrambled_right


print(unscramble(scrambled_flag))

Flag

poctf{uwsp_1_4m_7h3_0c34n}

Reverse300-1

Hightlighted techniques

  • symbolic execution with angr

Learning the game

We are presented with a file called Reverse300-1. I run the file command against it:

Reverse300-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cd45573f4bd7b1d2d713912994eec4d881dfb71f, 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
## ./Reverse300-1
Enter the key to decrypt the flag: sadfasdf
Incorrect key length. Key must be 22 characters long.

Playing the game

Let’s check the functions in an objdump disassembly.

1
2
3
4
5
6
7
8
9
10
...
00000000004012e0 <decrypt_flag>:
  4012e0:	55                   	push   %rbp
  4012e1:	48 89 e5             	mov    %rsp,%rbp
  4012e4:	48 83 ec 40          	sub    $0x40,%rsp
  4012e8:	48 89 7d c8          	mov    %rdi,-0x38(%rbp)
  4012ec:	48 b8 14 0c 0b 12 11 	movabs $0x16302911120b0c14,%rax
  4012f3:	29 30 16 
  4012f6:	48 ba 14 05 0f 7d 50 	movabs $0x212f12507d0f0514,%rdx
...

We see that there’s a decrypt_flag() function which is probably called when the password is correct, so let’s use angr to look for a state at the start of this function and then get stdin as a string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import angr

def get_key():
    project = angr.Project("Reverse300-1", auto_load_libs=False)


    simgr = project.factory.simgr()

    result = simgr.explore(find=0x4012e0)

    if result.found:
        state: angr.SimState = result.found[0]
        return state.posix.dumps(0)


print(get_key())

This outputs: b'dchfwREaguPJ8!pV*^U&Ms'

okay then, let’s use that as our password

1
2
3
# ./Reverse300-1 
Enter the key to decrypt the flag: dchfwREaguPJ8!pV*^U&Ms
The flag is: poctf{uwsp_7h3_w0rld_15_4_57463}

Flag

poctf{uwsp_7h3_w0rld_15_4_57463}

Reverse300-2

Hightlighted techniques

  • procmon
  • using burpsuite as proxy

Learning the game

We are presented with a file called Reverse. I run the file command against it:

Reverse300-2.exe: PE32+ executable (console) x86-64, for MS Windows, 10 sections

1
2
3
## ./Reverse300-2.exe
Fetching the flag from a secure source...
Success!

Playing the game

Okay so I got kinda stuck in the beginning for this one I’ll admit, I tried looking at the program in ghidra and looking for where the Success! string was being printed, but I couldn’t find it.

Finally, I decided that the “secure source” from where the flag was being gotten had to be one of two:

  • a hidden file created at runtime
  • some remote server

So I looked at what the program does with procmon.

img_26.png

After applying a few filters to make it easier we can see that the program is clearly performing some http request, so I will use burpsuite to intercept the data for this request

img_25.png

Flag

poctf{uwsp_4b4nd0n_4ll_h0p3}

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

poctf{uwsp_7h3_g4m3_15_4f007}

Reverse400-1

Hightlighted techniques

Learning the game

We are presented with a file called Reverse. I run the file command against it:

Reverse400.exe: PE32+ executable (console) x86-64, for MS Windows, 6 sections

1
2
## ./Reverse400.exe
Encrypted flag: bd7e9dad4a5fe0e7911f93cb1bf5a321

From the icon we can figure out that this is a compiled Python program

img_29.png

Playing the game

Decompile Python

To do this there are actually two steps:

  • extraction
  • decompiling

Extracting the executable

To extract the .pyc files I used pyinstxtractor, but there’s a web version that lets you upload an executable and returns the extracted files if you don’t want to download stuff now.

this yielded a folder with a bunch of files, from those I chose one that caught my attention.

img_30.png

Decompiling Reverse400.pyc

For the decompilation I used pycdc

pycdc Reverse400.pyc > Reverse400.pyc.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Source Generated with Decompyle++
# File: Reverse400.pyc (Python 3.11)


pyobfuscate = lambda getattr: getattr.items()()
Il = chr(114) + chr(101)
lI = '[^a-zA-Z0-9]'
lIl = chr(115) + chr(117) + chr(98)
__import__('sys').setrecursionlimit(100000000)
exec(
    getattr(__import__("zlib"), "decompress")
    (bytes.fromhex(
        '789ced5ded6e1b4b8e7d15efaf488947f0fe4de0579817308c8693f8de35e0c417892f66068b7df7952c75abbbc8734856557f48967167d2eaaa220fc94316ab25cb4df3f673fbfcf0e3ebf787abe6f3eb7ffe7a5c7d6cd65f9a9be6f6f7ebaf2fbbd1ef4fdf5eb737ba59379f9b9b4dd37c7bf9fed8349b6f2fcdc3af3fbfbdfcfdf3f5cb5eda3f5f7e3e6e2f6f6e9b9bb73bb77b2dcdea6e2b75b5bedefeb77bb5bd7fb3dacbfcbc9dbdbb79bcb3d7f2367170737be36d607dbffe7210dbdcdeddb7d748556be2e7c3f8663f6bf04abc4e87b702d7ebbbf6cee7f6e2637b71ff89c9db2ebe5b1d5e7decd67c6a2fb606edc11e7f6e13abbdd0056e819d23557efae015f46bdbf4c30b9766d3559f68e4faeabad5eb90c97d11ebd53e0049743a4afdefff5d0fef6cf368b7f0e7c38f6d7e248b062bb709960ef793ac8fcfa2ea5be21eefeda2bd5e6f3359a01e2278f9f55d1def4df9f6f2e3afe7c77f5f8b8104510f543b57d75d29190fb77776f6d4a4397373bb2d093702c711c11f4fcf699474030f843c28ebc5e9cfc7d787d7d75f4780ab6d26df716b0626f453339299a185524e6fb5b9b88b6c89c622b811b4ea5a5e508212f752dfe476fb40913fc60b3c5e787ffd561e5abafff9fcf2f5e1f937aa07ddbcffe91783e3ed3f9e5f1e5eaffb779e5e1f457d1bae1920356b7a7e0457ccd1f2e7b8d3adb44d8fe158f7e7b189ca26bae67b1435a2a2b3ca98ea47496b7a9beac7dade72e6ee5ee1d470e7fdfef44bdf7893c9b13d56fcacdc15297fada36b3ceef1432f7c7b7ef8fd7bef87e1c09f6a5bd2faf7af977ff59aeb7ccfe4b78b48623b61e2ee95a2d9f99fae5ddddd935aad885e45cb86b3321def197506599ad940fbf7c7374f85ea96a2b71dfee8b818c10f0535812c5526d7a0897b533319d36779fdf46cab517bba4f05ad7add7677e8271a779ddb9bd08a02f700334fecbaccf75443e1e9da32aa594dd2a395a5876ef6a5904863c62d24a1d65f0a5ab6e587f14a4fd822009c856537ed1f2d4af1ccec464ff5d5f1e9cdb4a52aea527d6dae2347090006cbb75f7367014558deaad1ac32ed5c969a9811f101e382fb0a291abbcb1585b96d3fa8bac22d1243d4b7d2d0f30e9775dd8a6bde1730a2aed6661bd70cea7f89cfe4d2bc1e21bf49a0a629961d2ed61f83ed40b37ffede2be3abe396b6a6aaf6e1df933785eda0c44d4b9f6468fdf487e25ae4ed5dab7f73af3363f720eaf1f9f7e310e01bb49b7a5a526574ce6ed61f2fbfdecc7ffab9eb9b953759408e0c77e2cfdd563cc2a9ba47009b016f13b547ca7732bf77cd40cf9e9b8337f66fd0ed1cc24c59753bd95675f218511cdd36ad0e2d3ff7b8572a0757e2def6ce2029fa9e8270bbb297887b73d5e1b8b4b043d2e1e2f88e5c009f02af15e76cc5616d8a62e97470bd9db58e370fa91a79417346bedb991bfff6c2bddb8b5a531660cebfe846d4df6bc91bbe5ea0a09352cc385c04da3acf9be929ae4e62f58369ef1d8262ff7806756aed1ec8557d1e5753d8ed7e7c7dd8c2ab8bae240b0aeb4d8b860895a01154590521ba62775c28455931e11d583a293e50ee16fdf0a19552678c012ae6963ae198328f7bb3b2aba3dd1078b42fe039b9370fea332de220afdc3113b11d56ce571979232f328ef9f7695b55f7edb33113a8c4ef658919d45a7cd4296485614e1de9e1cd25b6911bcdcb91c7d3f337ff2979b09f0fa5939bd9aea37621ddcbf936690e55f761598ae18b96f7b53ff390192367232767854f1ff31f306a3e7b71e90b3eaf739a2b1eb0149798b2273c53ab1fb640139f1d3a436a9ddfc77e5890b77155f7449d670f81bd6dd427d9f54e31cec3723fe5fc493fed478a7a59b9cc134995b3a3a8bea53b6faede09ce0075db41eff6dbd7946be7c8479c0a7ae4319ed83ad643e8ce0e7251fd68bd94e75ff59955d48e857be2a2679035fc1868994738401ec56b99345b5f482ea639ca97a4975c1a7e683b66561f2e422d90024bb132f3e835db5b49b20b19b349122936db3ee53fc3c70f9505cf330e1765ef1ed5aa325350e9a88d28abfd76ab446cedf37dda56ff2523dbaefccf436b83418ee41ee3c73c8cd7794470b88897607951a79b2b386777b6882e66896f8ad77b8ab3b813c3e1a2566f010fb4537d1246ce2a7d7a661c50c21b6ea5be251c8561a6cdd7ca50aba451c1b23cf61b228e13f6b44f6e666e45cadd95ebb123e4a2cecfaf1b9cb897fd56cc1cdd85e3a2ca5e7834089831fedb0505d41b29372b3d9225d066e9c44a6af6e1a2da0700a58ad0265527b9ca3f0f252fecad64f4676272a94ed0193e3c55502698f1a31f58738fb8f53e8794d9238ffc7e773b2df3bdafca4d50a1adc724a9fcc0679cc794d492291fb064b5d4937fd8bef6c927fc60ae4292b81f088ff129e2c003d39378cc1f37b1eee777f0739611b652f48e55c623aec2cc2d7a07ddf9048b72406d86e6fa1dbe8c3d77ccedb113ddadaef331513258f12dfed20dc08de3c0a0a94f778cd620bf0f57633eee2bab2c56aa96fe0ac1840f6fc22a2b1f20c885f5f062eace37effb1e71a5c895d7fba99accc7af3fd9fd5c6fe7ee57effed9b45fdd7bb87dfcea94fd8d1db211dfd11bfdd38345c637bd99d20df5d18ef6fb00b59c80a9710a706bd859d7d4512addfcee43b932a1950a8493f4f1fd38bf8d30de7bc7f3b04fdba646b20b907be44d71ce10dd573f608cf87c662efec1cc3d4be394e4abdd12effee9be466a339cbbbffa94dede5dca9bc725606cf3f6f72b902e433482a28f6d067f4043d5a52922f6a677553bdfaeae91da8d4b465f5437b81f1eeb57c1fb3f63d6d7315214b8afb1988807f14a43e2090f2903ca4deab6613e1d4625e123104c9dc82cb10848d928773749da511312d8963c8e1b7a47137b1c53ac4a4179cb269b2d34e9218edcd628c3221dda183c7c8cdd0eb27733e4912f3d7c4a01ecde688d3443f6b264caa5da42322d5ea208fd9c6988f25bbf6b0e7ab25a33855083e73d89bbdb398a585b8e44cce86dfad795f45003a5bfe94384496a08e41776e9f99213d832ae5f2f19a05ba419c9f07443a33cef9ee0610830a8a1aea085833bb191310c285dfa10efa9fa22263d9498d91eab04704cf78d7a572341df2c3d3727efae3857b3d203e98a795b4a197acfdf3eb200854d57d6290c33d0f8f6428db94430c95b1265e4054d11e74a61b80cf89e8da9fd39e16dc41076c2430bdd39149d0158bac57a5ccfb8f2173f452f2ee659b0595833aaac5c358cfa943b168a1314ebddbd306bc2ce0ce47a7a27e0cccb23a2be0223f108964627652ad696b3a403b29e673a2a646966c0598abe2f66025bc6f5ebbba27f02c9d8775cbc2789c39939ca1f8cb934e975862e4d7abf3e5c9af49e332e4dbacf4823392e4dba2ee8d2a4f7fe799f5dcae57ddc22a0a39313d832ae5fdf55062490d0d824fd79fa33ee87fc4fecf70981f71aea775ac578c46892192a973e743a678ef447354b2c0252b488f6312ac2533317780099e873ae5c0e748639b306accb674c8d250b64799c7bc86aa195eb3ca1f62f251f89bbdb398a585bcea2ce2681a4439666061c7a47d772c6cc04b68cebd70bfd214492139c7453fdf62f00c0a19380e3958c8386aea50ff1d6a72f62eabe5d536bd31e66628a8052d8c805a444e3495f981289d47953f4499cceb4d6c212e7a68c0e34ee4bf113a53901a9f32e8f6771eeb36463804b3635b7ec2c932e872963c92689de028a443dde0aad5ce709b5ac29f932124f6328e3b42e6751dd24a5bfe94384496a885431e8d2f32527b0655cbfbeab0c482069c4f6705d7300c3da0d8df0b5a9f3bc3382ad34d42d7d88f7427d11539fae529d974f62f97d05a61b796c554992ea719661d82cac0cbd3b758751cf642a72393428594cab50787f8aec88705515bf38c09e693b73f8e77d761a975ebb08e8e8e404b68cebd77795010924ecb8093aee66bc4f3a9d7833cf086f285bfad0a5956fef18c30280460a60a6293a35fbd2ca2fab95d749ed52ceda7fe0189a6d0641ec548ccab713fa9433873943d344b8e9cab680ed10b619bf92fa8d610a088abde5c641b4522516cdf0abd289a88cd2e62649d8021a173ce8b1f9728c6da40b5d9c669ff508b107999b19759648ef8b9ec09671fdfade73403186e4028692d75be9b680bb84e851da3269863923f7a9a9f3bd194585fa45f7858591d7c36dcb3a2e2e646f3734e19f5dc24122e8292ff14aeff6e5d2bf88216560214f4334b52947a53218911401a5af9107480948a1c1fe481d7339365290aa450539c876d3cb938a0928c763a738832e90800ab8a17756b456f5c903d63769f82e4a901231abfbff933467e14a40a628cbd9fd4599740e4aaceec8dfc2a0db67ebba4994d02e016f53ee9df06c1d378e9281c09c068e48bda4d4484a0602634183c1e40bced8998b881807a168d66ee8511d02d32c206ae8389a9d4249e669a74807232173bd7af1c4440a83e7726182823a14666f20ad8d28479564c3966890a7711650504a3e8a888bd5aacf2437866ba30fb9a566686230fb838140b2706a2b661981508cf0aba5c9a7653d8cb71ec3c8e6d69be0ae7ae9507c800e798b8790c3d3193025f12e7e412b20a08b6a2a0dff70856aa201c5b059b3da573006634aaa7b0bcf5096372125081a7a954954a888374f34148be9fb1e62860f42a3ba8bdecf89acaa1c270ee134b7c28a1300b3116e8e6d22edb8bd8a8648f76c467e49a43001360116374a7748fa11672e878b9c6fdbd73d6d900b8c8911ea0845b004372c30ac8fcb91ae2c09d0883849c36a1188188034a9296a36bf36fd30144560ac2965cc5209da833843f1d517a8fe51dd3887adc3af93b1f6fca314be9d04b69b702a1293031d563ac1d149b09807f8105e300b8108f46431123b259864c0746ea564b2449d6bf50262e97dae787cd7abf34998b4f9c2199ab1241e8a33710a7178a65199b5108174d447aa3a15617ee6c5a2752a4141a419ab888d1046b7470da2143b42264ea244cc72dc278a1907f5a86b813d494fe62bc12475775a0028542a638125a241828fd10041e2081aad2858eb504829b11b7158a57393c9362b046223675cfdf4c052010ac9b7b9e2fe808c24d4601e1ad374a62b97d0c04288489941666f69d01740e1cc323906b9e9e81032474846c65a414762f8a8ec62677937aa030f07b2067519be00f63ac64a1f266bac5d07c58f24e0465d31b2254409e6d168fb7f7da5700c4338beca28f16060a13e9fb7f88c6dec705985dd93f222936c2222eeee555538586fb398381af56d1ea31a8fee9afc9a15504530106406fa49728ea31ac8853a3431ec3be7157c332537572273316b7376d8b41c6e8bbbcc4501f7590b2815d920b1d1800e722bb55ef5118ce2900aa814e83cb2d74a8b18161f1cdd708e170c503ee8efcad1cdd259980c9c43519a6078cf3610d32f3e82dc47880456a42aec54acf444e65df5ab36e60adb9a910803f0158fb51c271c25ec50f1c8c5536edf0cd1642903c78889b55894a9be7bb5c88ec55216c9a8912058b5db44180b0a5666882af584e6cbba541477592c8cb1593691996bda10e562b30bc994e250aef5ab71a4aff42fc6cc286d812ca72daa7e6b63a5a496626b5099a10a20aea2dda4ad0744b202c438950b3d3e47e33e9c3075107d56cba9249b7c1951bb2ae87462d604f7110596cf5209b446ee9fb512c22e5d2c4ebb5044d5c9aa13a03e367ed63163e950a7511c2ef3b44d9247cc3b8e983a0a00f042083799ac823945b604948859fc7ea24ef9324cb3bba588b56fd7841b16355d4c88e2a159d2f00fe029ae16e29cbfe9ce382fa4a69395c5f6f776bb5ac79320f772940c04b25d133091649d893fa68ec3a8df95caa91881237ca7db653bb9097053b587190991fdfbb12adc23239dc0f60b84b59385e1477faf13af5033dc656ffa1905570611a639624cc0665a09531c2dd290c67718316ceea6165e5dcb66e83cee48e68b84b9cc9fde3a966d54406ae5a20ad8eb03734af6f7d507f286fa87680e5466ab06c86155818ad44c8040832c97e69c0eb0501a7a8f93dc9668e099760732bd2829c3189522d4184fb7775f34864d446cef892495e1d17a6555b1906f81646ce9ad69d406bb00ccea4fb600720451e424a94313ddb268843d4ff5c85479b1e886c202538d4bc5a165e0432673af07fb9ec0b7572ec7d8da4ae11886707c85fca72cc9488b71529ac2a0cf1254b9e266a64fab256bb6235dc0abf28899011416c4d60702b82f829e9b1578235be5c465eb7380a9964df315e6a56c7dc3091592185bddce3f8dfda82f67dcfd5862457ee8ab55d48435ab82212ab1c69f71c85f420998282d5bc8b382d948339cb094a499bd89e3b3d54c0b745a1a6c5502e37b8a470f44b4b67bca079680e9e71c3514da9e74bc03e4d9d3b8e9f552c512b5cc66595582e7d62991c365e1c644d5a59a7e429e5fc2bb6361bb7919980442aeebc3e5890e2a12b92546b158c216ed4a9ea5511eac54970159c82cca33575d55018a7be84bc49172443ebd1ae6782f525da5e13864f1119436d0340d8ed1edf9582aa7527ea863aa3578ee349b6ff7ea8c7b9f09bf6cadbaa576169f62a7c1dc608ccde4713b0e9c2856750efc8dab64d0cc0bdd691bc56a67cdd265a838a5589bb127ac6420709af48713902e2df066f1d781012f79363385a704a462cf12ea1ab103db67883aede23aa1eec67272282ac98a148f884d5c0143e6e3b69ccaed59249d1798c68c98dd4d5a3337436a0c370969088461a288957aee46b692bb0421b5b2dfb31fe52f73b91af3cdb16ba9f6619eb3dd6d8ca491533967a6a80f04018231b5b1c9a80c81278b78c72d711a3d5db2d1d81b4faa93247641361952ba510b1ff87b126ea72423ceee52998aad342983f526d172547a986f2eeee52ed5bc0ac5d90192ee5683ecd2c8b0270bd2a930703ca26adea14297bb51530cc69f075551da32b05e0527958e99d6bbe5518f7e2184734fa18dc7610af0c16b3d229b74854339528781814a90ed66094c971f28be409fb031c19dead5c4847fe75309558263eaa7f7fd45f6160db86654265db11a809ccccdd0ddbd0a6d07c9e2b482b3e6a0bf3450c27d7859460582aecd4e45abd998b779e73a42f5844c8d18fb0d3aa8b664e90c87d156ddde8935cbf3274d2a7659802a82812033d0abca81ff981505c148e4f2f9d493120515b651e6021392adccb56d9a582b39937edb53984310985a232c4590dbc8ca48c03400c6266019ac694885d6e8d286f3672e452c48c9022817a1c9b09dc351315036e3b19a20b41bf3f5e6d5cdcbf1f1c4bc5a823a120e1c8846326951f5421b8b58cf3453c58bf7820548dc65b135c68a0e038cf081edcd69a5b26a9c62b260c2410b8b193a3bcdc27e77658f39a18efb2d51cb2f3a098cd9bf10c717ddc25c97e085d4749516ae1a6c364d5a148396544bc2ba9555f53afff26f49564c445623e1307664c120259569dc8d0301707d93aa598e92b9d3d5e015b156ad69f378d632126126f74d51975a14a9456e3b75b5268b8cdd54871cfab058118b8158a7ba253d5a9990b4ddab4a9d048c0c033149e6a84af0dcd91b39686171bca66560c4efda7ccaa848ac878b1c05b1bf62587bfaafcc32040c8061d2078067cdba649986b34baa7346dd48674db81a47c98458526a38226c64898459e9a3e4d2be5934dbd6c5573c392ac419844d24b8b93607af1872281b01b239754a5f84c0f50e24c40438841a029dac57e6d22473eee07698e57a835512e9a92a11b3f87d83a7fd151b3b923ed6cba984c5c8164d1fbd3b1cb74a6c327b2a53e118378c086dc72bd6360b105ba621a16a40b03cfeec4ba749a7cb0864a221c56a14a136ccbd3acd8f8fc42e1a943701631b3b5c3653d9088200318befcf269993612bad8a4e98b67a7d4d328fd2356d8085285707d300874a770203eda4537ce1e02613622a9867db566161bae6253d1cc3108eafd056e6a5ca709cef494621e131f4e4d389f6af394a26ddb75c95a49163f69f87c26b7d4359df00eeadaac7c9ce04b2fffae85c511c4e388f62ab8d55f0af166ecddfaa10e91ac01e7cbb7e7d21fae07dae9804c6273577075df06f3e4ab1d40bca68221698902ac7eee4664d17430b0cf7bcaaac38bc4bf8421e6207b48f45cd11d1a9a9a08d4d9268aa120c68119db707cfa480c4fda1048a53cb308cdd1e45197b5c0c4d73b788ceaff2b0a443442400c1fa274213fe624d9b84a21f5c16d717d3292c612b11770d6616f5ef4349dacedcbf45fcb074432f4f85ccfb45093eca87b563d86ab4a3380e36c67020dc52672e9cf3956720481d051352ff599a9616626dcc17006aa905688a7a63895a1a08e7b7f2f3222fce44cbd953cc1c4b90db1df65289546aa99845d1319d1c89aa9e288a985b9b514bef50bc01601131c64afc0ee6abf04ed7ca6aec1a23833d206ba53136b604821c0805bebb37cbce70e64a8219b1917c619dc87065403fd085d6c36e522d533a4dbb9b44248bd3509bdf2e458e8f1e0827c4af0c0898decfffa4989d81d583385834db9e3c491216d84aa0278b4fac19d296247ffc210600d3197f78c46531cdfd34f4aa18191fe9108020b96da612298aa1eecd36ce11735fd4074b429d0b59451419c0d38fdb244e4ba9e540a47b8eeca914a63e15980377675dba87a80d9ecca5d3e89244c3df4bee3d5ec42ca03986073d36eb35cd6977200103dcd1d868c8717dd77e27c0f4af0ec0f21ccd62cb75088a343ec06fecc9f3e524b0655cbfbe2be22790ec0dd2457dcd1f0c7a3b12da4de46d0986e54ce12ec740f9b89941b4a0e2a04d1b820d07daa594d38bf025440a4e6e9b40d6924d123d6ab9e3c32800af293a451af5b7669eb75063c9296cf7a68a446e6a235440e255c2af3805e2b56e7b637dbfbe6e6e87dabb9fd5dd7160b5bedefe0726b6f3d7abe6f6f9e1c7d7ef0f9f51cc8d3c536cf3aecc49ebe50ff10ad917e1f40d245069ed0bc0d22d362a8e313c55add3c41ec742c4566b53c1b64d323dce320c9b8595a177a7ee30ea994c452e8706258bf9ae15edcc435d145a55c52f0eb0228f7c9ce477cd410fa52f0f171ae9421d80e539837b9eae233bd42c39df1727812de3faf55d113f81947a6182f67aff53b1c9ee493d8fde7d119df63b6add35b57672c17c4f11d09430720b2949e1a5c29448a4ce4b312ee674c038a06942d375a0715f8a9fdce4f5602fe05935eea7a38229581697c69c50b21dbb65eb26f585f02ae5085748298c6d375a8367c85e2f27ad259b247a0b283cf15c40560bad5ce70935db29f932124f6328e3b42e67110740d3561518323633e6d04100d3f99213d832ae5fdf7506a0906654d86670d43bfe9cfae90c5b6ce85afa10ef7afa22a63e9bb1c475745d860a096893948305f432ccae7774887223246a755752e2e04144c24b8366f85707e0f01c3409a0429666061c7a47d772c6cc04b68cebd77745ff049251485dbc2789c39939ece5ea3fb9af2b71ffdf970eeeddea987ddbffeda46c848cddfdad86de4c32992c81ebb4d9fd79509a220c6b96f3c15c621a959e2e2593810a433c0466c1e2e1e2abf5956f6beebfac0e078dabd5e1eaeadbcbf7c7cf578fff7e7a5ded2ed7ebd57fafaf9efeb8faf9f27af5ede1f9f9e1ebf3e3eacfc7d787d7d75faba6f9faf7d3f3ebd3cfdf4d737df5e1dbcb8fbf9e9e1f3f5c5ffdf3e5e776e9d5cbaf2b6deae63071bba4697ebc7cfffbf9b169b6ab3e7c585ffdd7edd58776e60797809f0f3f86cb5b18578fcfbf1f0f5056bba4e93b60d565d1feee6af04abe4e7fb6eebb5bb5133e7e6caf3e1d2ed6f79f8202df24b613c2ababc339a2b95f0fe7e678ea7307adbde0807ada0de0abbb7bf75c151af5dac174589b65d9bef0e6c89bf5ff03329e6814'.replace(
            '\n', ''))).decode()
)

Reversing the decompiled code

The decompiled code might look a bit menacing at first, but actually all it does is:

  • call the decompress function in the zlib module
  • pipe the result into exec

So the actual code is compressed, let’s remove the call to exec and move the result from decompress to another file

Reversing decompressed code from decompiled code (my head might start to hurt)

img_31.png

Well this is… something.

I tried to reverse this by renaming variables, but that was not a great idea, not only because it took like half an hour to refactor the name of a single variable, but also when it was finally done, the code was not executable anymore.

Oh btw, we can run this and get the same output as before

1
2
3
# python3 really_obfuscated_thing.py
Encrypted flag: bd7e9dad4a5fe0e7911f93cb1bf5a321

So my next thought is that this whole obfuscated mess must be building some code that is at least executable and then running it with exec or something like that.

So what I tried is hooking exec by adding the following at the start of the program

1
2
3
4
oexec = exec
def exec(command):
    print(command)
    oexec(command)

and the output is…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
lllllllllllllll,llllllllllllllI,lllllllllllllIl,lllllllllllllII,llllllllllllIll,llllllllllllIlI,llllllllllllIIl=bytearray,print,enumerate,chr,ord,len,bytes
from Crypto.Cipher import AES as IIIIlIIlIIllII
from Crypto.Util.Padding import pad as IIlIlIIllllIlI
def lIlIIIlllllIlllIll(IlIlIlIIlllIIlIlll):return''.join(lllllllllllllII(llllllllllllIll(A)^42)for A in IlIlIlIIlllIIlIlll)
def lllIIllllIIIIlllll(lIIllllllIlllIIIll,llIllIIlllIlIIllII):B=llIllIIlllIlIIllII;A=lIIllllllIlllIIIll;return A<<B&255|A>>8-B
def IllIlllIIlllIIIIlI(lIlIllIlllIlIlIIIl,IlllIIlIIIllIlIlII,lIIIIllIllllIIIlII):
	B=lIIIIllIllllIIIlII;A=IlllIIlIIIllIlIlII;C=lllllllllllllll();E=llllllllllllIlI(A);F=llllllllllllIlI(B)
	for(D,G)in lllllllllllllIl(lIlIllIlllIlIlIIIl):H=(A[D%E]+B[D%F])%8;C.append(lllIIllllIIIIlllll(G,H))
	return C
def IIIllIlIIIIlIlIlIl(IIIIlllIIIIIlIIIll,llIllIlllIIIlllIll,IIllIlllIIIlIIlIlI):A=IIIIlIIlIIllII.new(llIllIlllIIIlllIll,IIIIlIIlIIllII.MODE_CBC,IIllIlllIIIlIIlIlI);return A.encrypt(IIlIlIIllllIlI(IIIIlllIIIIIlIIIll,IIIIlIIlIIllII.block_size))
def lIlIlIllIlIlIlIIll(lIIIIlIlIIIIIIIIIl):return llllllllllllIIl.fromhex(lIIIIlIlIIIIIIIIIl)
IIllIllIlIllIIIIIl='[redacted]'
IIllIIlIllIlIIlIll='fa21c9c2596099915dbc7845c941c14e81594b5c4f69177cc4059da11e782e0b'
IlllIlIlllIIIIIIll='504f43544632303234'
llIllIIlllIlIIllII='437261636b3430302d58'
IllIllllllIIIllIIl=lIlIIIlllllIlllIll(IIllIllIlIllIIIIIl)
IIllllIlIlIIIIIIll=lIlIlIllIlIlIlIIll(IlllIlIlllIIIIIIll)
if llllllllllllIlI(IIllllIlIlIIIIIIll)<16:IIllllIlIlIIIIIIll=IIllllIlIlIIIIIIll.ljust(16,b'\x00')
IlllIIlIIIllIlIlII=llIllIIlllIlIIllII[:32]if llllllllllllIlI(llIllIIlllIlIIllII)>=32 else llIllIIlllIlIIllII.ljust(32,'0')
lIIIIllIllllIIIlII=llllllllllllIIl.fromhex(IlllIIlIIIllIlIlII)
lllllIIllllIlIllll=IllIlllIIlllIIIIlI(IllIllllllIIIllIIl.encode('utf-8'),IIllllIlIlIIIIIIll,lIIIIllIllllIIIlII)
IIIlIIllllIlllIlII=IIIllIlIIIIlIlIlIl(lllllIIllllIlIllll,IIllllIlIlIIIIIIll,lIIIIllIllllIIIlII)
llllllllllllllI('Encrypted flag:',IIIlIIllllIlllIlII.hex())
__import__('sys').exit()

Reversing the deobfuscated code from the decompressed code from the decompiled code (my head 100% hurts now)

Okay this at least I can work with, after a long session of renaming and refactoring I ended up with this

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
# bytearray,print,enumerate,chr,ord,len,bytes=bytearray,print,enumerate,chr,ord,len,bytes
from Crypto.Cipher import AES as AES
from Crypto.Util.Padding import pad as pad


def xor42(data_string):
    return ''.join(chr(ord(A) ^ 42) for A in data_string)


def shifts_masks_sub(data,
                     param2):
    ret = data << param2 & 255 | data >> 8 - param2
    return ret


def three_way_encode(param1, key, iv):
    bytesarray = bytearray()
    param2_len = len(key)
    param3_len = len(iv)
    for (i, data) in enumerate(param1):
        weird_mod_8 = (key[i % param2_len] + iv[i % param3_len]) % 8
        bytesarray.append(shifts_masks_sub(data, weird_mod_8))
    return bytesarray


def encrypt_flag(encoded_flag, key, param3):
    cipher = AES.new(key,
                     AES.MODE_CBC,
                     param3)
    return cipher.encrypt(
        pad(encoded_flag, AES.block_size))


def bytesfromhex(hex_string): return bytes.fromhex(hex_string)


redacted = '[redacted]'
IIllIIlIllIlIIlIll = 'fa21c9c2596099915dbc7845c941c14e81594b5c4f69177cc4059da11e782e0b'
key = '504f43544632303234'  # POCTF2024
iv = '437261636b3430302d58'  # Crack400-X
redacted_xor42 = xor42(redacted)
key_bytes = bytesfromhex(key)

if len(key_bytes) < 16:
    key_bytes = key_bytes.ljust(16, b'\x00')

iv_32 = iv[:32] if len(iv) >= 32 else iv.ljust(32, '0')
iv_32_bytes = bytes.fromhex(iv_32)
encoded_flag = three_way_encode(redacted_xor42.encode('utf-8'), key_bytes, iv_32_bytes)
encrypted_flag = encrypt_flag(encoded_flag, key_bytes, iv_32_bytes)
print('Encrypted flag:', encrypted_flag.hex())
__import__('sys').exit()

Key things to notice

  • the IIllIIlIllIlIIlIll variable is never used (probably the encrypted flag hidden in there)
  • There is encryption but the whole flag encryption process takes three steps
    • Every character is XORed with decimal 42
    • Some form of encoding done in the three_way_encode() function
    • AES encryption

The first and last steps are easy to reverse since we already have the key and iv values for the encryption.

How does the “three way encoding” work? (btw I only called it that because it takes three parameters)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def shifts_masks_sub(data,
                     param2):
    ret = data << param2 & 255 | data >> 8 - param2
    return ret


def three_way_encode(param1, key, iv):
    bytesarray = bytearray()
    param2_len = len(key)
    param3_len = len(iv)
    for (i, data) in enumerate(param1):
        weird_mod_8 = (key[i % param2_len] + iv[i % param3_len]) % 8
        bytesarray.append(shifts_masks_sub(data, weird_mod_8))
    return bytesarray

three_way_encode analysis:

  • a for loop is initiated iterating over the flag, inside this loop:
    • the variable weird_mod_8 is constructed with data from the key and iv, since it does not depend on the flag, we can reconstruct it
    • shifts_masks_sub is called passing it data from the current index of the encoded flag and the weird_mod_8 value
    • the result of the previous call is appended to bytesarray
  • bytesarray is returned

shifts_masks_sub analysis:

  • param2 is a number between 0 and 7
  • data is split at param2 index
  • the left side is moved to the right
  • the right side is moved to the left
  • mixed data is returned

Finally, build a script to reverse this mess

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad


def decrypt_flag(encrypted_flag, key, iv):
    cipher = AES.new(key,
                     AES.MODE_CBC,
                     iv)
    return cipher.decrypt(pad(encrypted_flag, AES.block_size))


def undo_shifts_masks_sub(ret, param2):
    data = ret << (8 - param2) & 255 | ret >> param2
    return data


def three_way_decode(encoded_flag, key, iv):
    decoded_flag = ""
    key_len = len(key)
    iv_len = len(iv)
    for (i, data) in enumerate(encoded_flag):
        weird_mod_8 = (key[i % key_len] + iv[i % iv_len]) % 8
        decoded_flag += chr(undo_shifts_masks_sub(data, weird_mod_8))
    return decoded_flag


def xor42(data_string):
    return ''.join(chr(ord(A) ^ 42) for A in data_string)


if __name__ == "__main__":
    key = bytes.fromhex('504f43544632303234')
    iv_str = '437261636b3430302d58'

    if len(key) < 16:
        key = key.ljust(16, b'\x00')

    iv_32 = iv_str[:32] if len(iv_str) >= 32 else iv_str.ljust(32, '0')
    iv = bytes.fromhex(iv_32)
    
    encrypted_flag_hex = bytes.fromhex("fa21c9c2596099915dbc7845c941c14e81594b5c4f69177cc4059da11e782e0b")

    encoded_flag = decrypt_flag(encrypted_flag_hex, key, iv)

    flag_xor42 = three_way_decode(encoded_flag, key, iv)

    flag = xor42(flag_xor42)

    print(flag)

And the output of this is…

poctf{uwsp_4ll_7h47_gl1773r5})))ü©$¸Î¯kž2ó"'‚

very weird, but you know, the flag is there.

Flag

poctf{uwsp_4ll_7h47_gl1773r5}
Trending Tags