Points: 400 Category: Choose your Pwn Adventure Author: sheidan

Introduction

PwnAdventure3 is an awesome CTF. If you don’t know what is it, check out the official website.

The challenge called Blocky’s Revenge, is a very complex logic circuit as shown below:

circuit

To solve it, we have to find the location of the circuit implementation.

Note that the game is running on Windows 10 and in offline mode.

Primary static analysis

Once the gamelogic.dll file is loaded in the disassembler, we try to find any function related to the challenge (cave, block, circuit, …). We find the function Player::PerformSetCircuitInputs at gamelogic.dll+0x000566F0.

At Player::PerformSetCircuitInputs+0xA7, there is a call to Game::GetCircuitOuputs virtual function with the following parameters:

  • level name
  • state (or user input)
  • pointer to a char array of a size defined before
  • size of the char array
  • pointer to the variable that will store the result

GetCircuitOutputs

From here, we carry out a dynamic analysis with a first breakpoint at Player::PerformSetCircuitInputs+0xA7.

Dynamic analysis

We reach the breakpoint by pressing the E key (action key) in the final room and step into Game::GetCircuitOuputs (pwnadventure3-win32-shipping.exe+0xb73d0) virtual function.

An absolute indirect call seems interesting in a loop at pwnadventure3-win32-shipping.exe+0xb747e. It initializes a buffer with our input value.

The next absolute indirect call, at pwnadventure3-win32-shipping.exe+0xb748d uses this buffer and we finally find the function which implement the logic of the circuit !

Note: If you are in another level, follow this call to have the implementation of the circuit.

Its address is pwnadventure3-win32-shipping.exe+0xaa860

sub esp,18
push ebx
push esi
mov esi,ecx
movzx eax,byte ptr ds:[esi+7]
mov byte ptr ds:[esi+85],al
movzx eax,byte ptr ds:[esi+8]
mov byte ptr ds:[esi+8E],al
movzx eax,byte ptr ds:[esi+9]
mov byte ptr ds:[esi+99],al
movzx eax,byte ptr ds:[esi+A]
mov byte ptr ds:[esi+A5],al
mov al,byte ptr ds:[esi+B]
[...]

There are a lot of instructions and the CFG is big (but clear). It will not be displayed.

Let’s back to static analysis.

Decompiling and solve func_AA860

The fastest and lazy way is to use a decompiler to make the function more readable. After that, we notice that the initialized buffer will contain the inputs and the outputs. The first four elements is here for padding therefore inputs starts at array+0x4.

At the end of the function, we can see the condition to solve the challenge:

if (ecx->f207 || al213) {
    eax214 = 1;
} else {
    eax214 = 0;
}
ecx->f206 = *(int8_t*)&eax214;
if (ecx->f39 || (ecx->f203 || *(int8_t*)&eax214)) {
    ecx->f38 = 1;   //success
    ecx->f37 = 0;
    ecx->f36 = 0;
    return;
} else {
    ecx->f38 = 0;   //failure
    ecx->f37 = 1;
    ecx->f36 = 1;
    return;
}

To resolve this condition, we use Z3 solver.

After some replacements with sed to translate pseudo-code to python, we finally have a script of ~340 lines of code:

# Initialization of the array which will contains inputs and outputs
m = [None for _ in range(0xae*2)]

# Set inputs as z3 boolean
for i in range(0+4, 32+4):
    m[i] = z3.Bool(str(i-4))

# Define the equation 
[...]
m[209] = m018
m019 = z3.Or(m[207], m018)
m[206] = m019

# Solve
s = z3.Solver()
s.add(z3.Not(z3.Or(m[39], m[203], m[206])))

print("[.]Checking...")
print(s.check())
print("[.]Resolving...")
model = s.model()

binStr = ""
for i in range(4, 32+4):
    if "true" == model[m[i]].sexpr():
        binStr += "1"
    else:
        binStr += "0"

print(binStr[::-1])

output: 01101001100011111010101111111010

Now, the door is open. Open the chest and get the flag.


Pwntera

Yet another french CTF team that sux !