Pwn Adventure Sourcery is a really interesting game made for CSAW finals 2018. The game was made using Rust and WebAssembly.
Commands are simple, we walk using arrow keys, interact using E
key and use weapons/items with SPACE
key. To play the game properly we (sadly) need to use Chrome browser, otherwise CTRL+C/CTRL+V
won't work which will make the game much harder.
In a brief tutorial, we see get assembly code for fire spell that we need to use to break builder standing in our way.
mov eax, SYS_FIRE
mov ebx, 16 ; energy to use
int 0x80
hlt
Going forward we get to Spell extractor
which we can use to get source code of doors and our first challenge Jail Storage Door.
Jail Storage Door
TERMINAL_INPUT = 0
TERMINAL_OUTPUT = 1
DOOR_CONTROL = 2
main:
; Display initial message
mov esi, message
mov ecx, end_message - message
mov dx, TERMINAL_OUTPUT
rep outsb
.input_loop:
; Grab next input
in al, TERMINAL_INPUT ;in dest, src
cmp al, '0'
jb .input_loop ;Jump if Below (unsigned comparison)
cmp al, '9'
ja .input_loop ;Jump if Above (unsigned comparison)
; Rolling code for last 4 digits
mov cl, [entered_code + 1]
mov [entered_code], cl
mov cl, [entered_code + 2]
mov [entered_code + 1], cl
mov cl, [entered_code + 3]
mov [entered_code + 2], cl
mov [entered_code + 3], al
; Display updated code
mov dx, TERMINAL_OUTPUT
mov al, '\r'
out dx, al
mov esi, entered_code
mov ecx, 4
rep outsb
; Check code
mov esi, correct_code
mov edi, entered_code
mov ecx, 4
repe cmpsb
je .open_door
jmp .input_loop
.open_door:
; Send command to unlock door
mov al, 1
out DOOR_CONTROL, al
; Show open message
mov esi, open
mov ecx, end_open - open
mov dx, TERMINAL_OUTPUT
rep outsb
jmp .input_loop
correct_code:
db "5129" ;Solution
message:
db "PIN code:\n"
end_message:
open:
db "\rOPEN"
end_open:
.data
entered_code:
db 0, 0, 0, 0
The only confusing part was the way in which data was loaded in memory. Characters are loaded in the last position in entered_code
array and then moved towards the first position after each new character is entered.
We also see that correct_code
is hardcoded to 5129
which is our solution.
Jail Door
Moving forward we get Pwn tool
which we can use to write our own assembly code to interact with items.
TERMINAL_INPUT = 0
TERMINAL_OUTPUT = 1
SECURE_PIN_STORAGE = 2
DOOR_CONTROL = 3
main:
.correct_pin = 0
sub esp, 32
; Fetch PIN from secure memory
lea edi, [esp + .correct_pin]
push edi
call get_secure_pin
add esp, 4
.main_loop:
push edi
call ask_and_verify_pin
add esp, 4
test al, al
jz .invalid
call open_door
jmp .main_loop
.invalid:
; Show denied message
mov esi, invalid
mov ecx, end_invalid - invalid
mov dx, TERMINAL_OUTPUT
rep outsb
call sleep
jmp .main_loop
ask_and_verify_pin:
; Args
.correct_pin = 16
; Variables
.input = -32
.len = -36
push esi
push edi
push ebp
mov ebp, esp
sub esp, 36
mov dword [ebp + .len], 0
; Display initial message
mov esi, message
mov ecx, end_message - message
mov dx, TERMINAL_OUTPUT
rep outsb
.input_loop:
; Grab next input
in al, TERMINAL_INPUT ;read char from input
; If enter is pressed, done
cmp al, '\n'
je .end_input
; Look for backspace
cmp al, 8
je .backspace
; Add character to input
mov ecx, [ebp + .len]
mov [ebp + .input + ecx], al
inc dword [ebp + .len] ;The inc instruction increments the contents of its operand by one
out TERMINAL_OUTPUT, al
jmp .input_loop
.backspace:
cmp dword [ebp + .len], 0
je .input_loop
; Erase last character
mov al, 8
out TERMINAL_OUTPUT, al
mov al, ' '
out TERMINAL_OUTPUT, al
mov al, 8
out TERMINAL_OUTPUT, al
dec dword [ebp + .len]
jmp .input_loop
.end_input:
out TERMINAL_OUTPUT, al
; Null terminate input
mov ecx, [ebp + .len]
mov byte [ebp + .input + ecx], 0
; Check code
mov esi, [ebp + .correct_pin]
lea edi, [ebp + .input]
mov ecx, [ebp + .len]
inc ecx
repe cmpsb
sete al
mov esp, ebp
pop ebp
pop edi
pop esi
ret
open_door:
push esi
; Send command to unlock door
mov al, 1
out DOOR_CONTROL, al
; Show open message
mov esi, open
mov ecx, end_open - open
mov dx, TERMINAL_OUTPUT
rep outsb
call sleep
pop esi
ret
get_secure_pin:
mov ecx, [esp + 4]
.get_pin_loop:
in al, SECURE_PIN_STORAGE
mov [ecx], al
inc ecx
test al, al ;check if value is zero
jnz .get_pin_loop
ret
sleep:
mov ecx, 120
.sleep_loop:
pause
loop .sleep_loop
ret
message:
db "\fPIN code:\n"
end_message:
open:
db "ACCESS GRANTED"
end_open:
invalid:
db "ACCESS DENIED"
end_invalid:
This time pin is read from secure storage and we can't see it in source code. The input_loop
is pretty simple it reads a char, checks if it is backspace or return, increments the number of chars and saves our input.
Since no checks are being made for the length of input we have classic buffer overflow. Let's see what's the offset to overwrite EIP.
push eip ;call will place EIP on stack
ask_and_verify_pin:
push esi ; esp - 4
push edi ; esp - 4
push ebp ; esp - 4
mov ebp, esp
sub esp, 36 ; esp - 36
We see that esp + 48
will give us ability to overwrite EIP. Input starts from ebp + .input
that is ebp - 32
or esp + 4
. This means that offset from input we control is 44
to EIP. Address of open_door
function is 0x10a7.
The assembly solution we need to write in Pwn tool
:
mov esi, data
mov ecx, end-data
mov dx, 0
rep outsb
hlt
data:
db "11111111111111111111111111111111"
db "111111111111"
db 0xa7,0x10
db "\n"
end:
After leaving jail, we can go visit town (just north) and go right to zombie map. This part is just a classic game, we need to kill zombie boss to unlock a new spell.
There are 3 buttons that need to be pressed to unlock the boss room. Upon entering the room with the boss, doors lock and fight is triggered but only if you move more then two steps from the door, which we can use to clear all other zombies before the boss fight itself.
After the fight we get Explode
spell:
mov ecx, 24
wait:
pause
loop wait
mov eax, SYS_EXPLODE
mov ebx, 32
int 0x80
hlt
Now we have to go back to the map we entered after leaving jail and then left and down to enter the desert area and down again to enter Lab.
Lab Door 1
TERMINAL_INPUT = 0
TERMINAL_OUTPUT = 1
DOOR_CONTROL = 2
main:
.correct_pin = 0
.main_loop:
call ask_and_verify_code
test al, al
jz .invalid
call open_door
jmp .main_loop
.invalid:
; Show denied message
mov esi, invalid
mov ecx, end_invalid - invalid
mov dx, TERMINAL_OUTPUT
rep outsb
call sleep
jmp .main_loop
ask_and_verify_code:
; Variables
.input = -32
.len = -36
push esi
push edi
push ebp
mov ebp, esp
sub esp, 36
mov dword [ebp + .len], 0
; Display initial message
mov esi, message
mov ecx, end_message - message
mov dx, TERMINAL_OUTPUT
rep outsb
.input_loop:
; Grab next input
in al, TERMINAL_INPUT
; If enter is pressed, done
cmp al, '\n'
je .end_input
; Look for backspace
cmp al, 8
je .backspace
; Printable only
cmp al, ' ' ;above ' ' 0x20 and bellow ~ 0xFE
jb .input_loop
cmp al, '~'
ja .input_loop
; Add character to input
mov ecx, [ebp + .len]
cmp ecx, 31
jae .input_loop ;if above 31 jmp input_loop; 31 max length
mov [ebp + .input + ecx], al
inc dword [ebp + .len]
out TERMINAL_OUTPUT, al
jmp .input_loop
.backspace:
cmp dword [ebp + .len], 0
je .input_loop
; Erase last character
mov al, 8
out TERMINAL_OUTPUT, al
mov al, ' '
out TERMINAL_OUTPUT, al
mov al, 8
out TERMINAL_OUTPUT, al
dec dword [ebp + .len]
jmp .input_loop
.end_input:
out TERMINAL_OUTPUT, al
; Null terminate input
mov ecx, [ebp + .len]
mov byte [ebp + .input + ecx], 0
; Check code
lea eax, [ebp + .input]
push eax
call verify_code
mov esp, ebp
pop ebp
pop edi
pop esi
ret
verify_code:
push esi
push ebp
mov ebp, esp
sub esp, 8
push dword [ebp + 12]
call strlen
cmp eax, 16 ;length has to be 16
jne .bad
mov esi, [ebp + 12] ;load eax (input)
lea edx, [ebp - 8] ;load eax again
mov ecx, 8 ;loop 8 times
.loop1:
mov al, [esi]
xor al, [esi + 8] ; xor input + (input + 8)
mov [edx], al ; move result to input
inc esi
inc edx
loop .loop1
lea esi, [ebp - 8]
mov edx, valid
mov ecx, 8 ;loop 8 times
.check: ;check if same as `valid` array
mov al, [esi]
cmp al, [edx]
jne .bad
inc esi
inc edx
loop .check
mov al, 1
mov esp, ebp
pop ebp
pop esi
ret 4
.bad:
xor al, al
mov esp, ebp
pop ebp
pop esi
ret 4
strlen:
xor eax, eax
mov ecx, [esp + 4]
.loop:
mov dl, [ecx]
test dl, dl
jz .end
inc eax
inc ecx
jmp .loop
.end:
ret 4
open_door:
push esi
; Send command to unlock door
mov al, 1
out DOOR_CONTROL, al
; Show open message
mov esi, open
mov ecx, end_open - open
mov dx, TERMINAL_OUTPUT
rep outsb
call sleep
pop esi
ret
sleep:
mov ecx, 120
.sleep_loop:
pause
loop .sleep_loop
ret
valid:
db 0x09, 0x23, 0x06, 0x07, 0x36, 0x38, 0x22, 0x2c
message:
db "\fCLEARENCE LEVEL 1 REQUIRED\nAuthorization code:\n"
end_message:
open:
db "ACCESS GRANTED"
end_open:
invalid:
db "ACCESS DENIED"
end_invalid:
We have another reversing challenge that we need to pass to unlock the door. The length of the input is checked so we need to understand how the code works to bypass the pin check. I have left some comments that should make code a bit clearer.
Basically, we can only enter characters between ' '
and ~
. The length of input has to be 16 and each of the first eight characters from the input are XORed with the input characters 8 positions ahead from them. The result we get is then checked with valid
array.
Brute-force python solution:
alphabet = [x for x in range(0x20, 0xFE + 1)]
values = [0x09, 0x23, 0x06, 0x07, 0x36, 0x38, 0x22, 0x2c]
for val in values:
for x in alphabet:
for y in alphabet:
if x ^ y == val:
print("%x (%c) ^ %x (%c)= %x" % (x, chr(x), y, chr(y), val))
continue
# Verify solution
sol = [0x20, 0x40, 0x21, 0x20, 0x40, 0x40, 0x40, 0x40, 0x29, 0x63, 0x27, 0x27, 0x76, 0x78, 0x62, 0x6c]
for x in range(0,8):
print(hex(sol[x] ^ sol[x+8]))
In the room ahead we get flying boots. And that's it for this part of writeup :D
Comments
comments powered by Disqus