Interactive reverse
This year, the ECW organizers came up with 4 reverse engineering challenges. This challenge was the third one. The statement is the following :
You have laid hands upon a novel kind of chip that runs an unknown architecture. You cannot reverse engineer the chip itself, but you are able to converse with it, and are tasked with finding the secret hidden in an associated sample program.
All the programs and files used for the resolution of the challenge might be found on my github repo for the ECW (sorry, it’s a mess) : https://github.com/micronoyau/ECW-2023/tree/master/interactive.
Overview of the challenge
We are given the following elements :
- a server
- a python script
sample.py
- an unkown architecture binary file
prog.bin
First, connecting to the server reveals we need to supply it a binary file :
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive]$ nc instances.challenge-ecw.fr 42471
Welcome to my VM. Before I run, you will have to load some firmware. To do this, follow these steps:
- Send the following string ended by a newline: '-----PROGRAM START-----'
- Send your program (like the content of prog.bin) encoded in base64, then ended by a newline
- Send the following string ended by a newline: '-----PROGRAM END-----'
If you follow these steps, we shall confirm VM execution has started and then run your program.
The provided python script sample.py
does exactly that. Basically, it executes the prog.bin
file on the remote server under the unkown architecture.
import pwn
import base64
SERVER_ADDRESS = ?
SERVER_PORT = ?
with open("prog.bin", "rb") as f:
program = f.read()
r = pwn.remote(SERVER_ADDRESS, SERVER_PORT)
r.sendline("-----PROGRAM START-----")
r.sendline(base64.b64encode(program))
r.sendline("-----PROGRAM END-----")
r.interactive()
Let’s try this script :
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ python sample.py
[+] Opening connection to instances.challenge-ecw.fr on port 42471: Done
r.sendline("-----PROGRAM START-----")
r.sendline("-----PROGRAM END-----")
[*] Switching to interactive mode
Welcome to my VM. Before I run, you will have to load some firmware. To do this, follow these steps:
- Send the following string ended by a newline: '-----PROGRAM START-----'
- Send your program (like the content of prog.bin) encoded in base64, then ended by a newline
- Send the following string ended by a newline: '-----PROGRAM END-----'
If you follow these steps, we shall confirm VM execution has started and then run your program.
+-----------------------------+
| THE FIRMWARE IS RUNNING NOW |
+-----------------------------+
Hello and welcome. Please enter the code that confirms you are my AI overlord:
> $ password
Impostor! Get out of here!
=== PROGRAM CRASHED ===
Unknown error
---- REGISTER INFO ----
IP: 0x5d
FLG: 0x1
R1: 0x0
R2: 0xa
R3: 0x1
R4: 0x0
R5: 0x2
R6: 0x6c
R7: 0x1
R8: 0x3
POINTED ADDRESS: 0x103
---- CALL STACK ----
[]
[*] Got EOF while reading in interactive
$
[*] Closed connection to instances.challenge-ecw.fr port 42471
Ok, lots of information here ! First, prog.bin
is a crackme and we need to find the password. Second, we have some information about the target architecture :
- There are 8 general purpose registers (
R1
toR8
) with a 1 byte capacity - The instruction pointer is simply called
IP
FLG
must correspond to some kinds of flagsPOINTED ADDRESS: 0x103
: this is probably used in some kind of instruction (e.g.call
,jump
,load
orstore
?)- We have access to the call stack
Well, let’s hexdump prog.bin
to see what it contains !
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ hexdump -C -v prog.bin
00000000 f8 24 f9 31 71 a0 24 92 32 72 b0 24 cb 33 73 85 |.$.1q.$.2r.$.3s.|
00000010 94 86 e8 24 77 02 a8 24 89 31 71 94 85 8e 87 02 |...$w..$.1q.....|
00000020 a0 24 71 c4 85 8e 90 24 77 02 e8 24 71 e0 24 a2 |.$q....$w..$q.$.|
00000030 32 72 a0 24 fb 33 73 c8 24 75 84 86 e8 24 77 02 |2r.$.3s.$u...$w.|
00000040 8e a8 24 77 02 81 59 86 b8 24 77 12 88 24 d9 31 |..$w..Y..$w..$.1|
00000050 71 94 a8 24 8d 35 75 88 76 40 77 02 40 01 00 00 |q..$.5u.v@w.@...|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000070 00 98 24 89 31 71 94 b0 24 e5 35 75 88 76 40 77 |..$.1q..$.5u.v@w|
00000080 02 98 01 40 01 00 00 00 00 00 00 00 00 00 00 00 |...@............|
00000090 81 d8 77 d4 08 0b 49 e4 2b 9a 23 60 4d 2c d8 0b |..w...I.+.#`M,..|
000000a0 64 d8 b3 a7 8c ef 9c e4 86 94 e0 6b 3f a6 7f 08 |d..........k?...|
000000b0 f6 9f bf 8b 2c fc 56 c7 03 b4 76 30 9e d4 5b b0 |....,.V...v0..[.|
000000c0 bb 8f 2f 50 3f ab ac 79 d3 38 2f d2 1f ae c2 a7 |../P?..y.8/.....|
000000d0 00 40 76 e8 24 bf 37 77 04 60 42 68 7a 21 52 53 |.@v.$.7w.`Bhz!RS|
000000e0 72 7f 8f 57 77 7f 59 04 13 03 00 00 00 00 00 00 |r..Ww.Y.........|
000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000100 00 8a 79 73 04 61 90 01 04 7d 52 75 7b 4a 73 80 |..ys.a...}Ru{Js.|
00000110 5b 8e 9f 13 03 00 00 00 00 00 00 00 00 00 00 00 |[...............|
00000120 00 8a 79 73 d1 04 88 01 68 04 59 8e a0 24 97 37 |..ys....h.Y..$.7|
00000130 77 12 7d 52 75 7b 4a 73 80 5b 8e 90 24 a7 37 77 |w.}Ru{Js.[..$.7w|
00000140 13 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000150 00 81 c8 24 51 77 40 76 62 79 77 c0 76 63 b8 24 |...$Qw@vbyw.vc.$|
00000160 b7 37 77 8e 7a 5b 13 79 89 51 71 8e a8 24 8f 37 |.7w.z[.y.Qq..$.7|
00000170 77 a0 24 59 13 80 03 88 03 00 00 00 00 00 00 00 |w.$Y............|
00000180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000200 0a 4a 9a 27 25 67 5f 4d b6 ff 71 1e b6 94 21 3e |.J.'%g_M..q...!>|
00000210 07 a1 36 fb 06 c2 3f f0 97 1f 43 b5 8e 32 1c 93 |..6...?...C..2..|
00000220 f6 87 53 2b e9 68 1a 86 32 eb 2e 5a 6e 97 ed 7c |..S+.h..2..Zn..||
00000230 cc 29 3f 19 c7 14 be 3a 5d 8a 46 fa 48 72 8e 1e |.)?....:].F.Hr..|
00000240 bb 8f 37 82 ea a8 c8 c6 20 33 e9 89 3e 4d c4 ed |..7..... 3..>M..|
00000250 c2 46 fb 5b 85 54 aa 6c 00 9e 86 1c 1f a3 ce 5c |.F.[.T.l.......\|
00000260 77 1b 96 e4 6c a7 96 06 e0 7a e7 b1 c9 42 6d f0 |w...l....z...Bm.|
00000270 47 bc a5 8d 5f 8b 3e a2 c7 0b 8e 7e d4 e3 47 90 |G..._.>....~..G.|
00000280 23 5b 9f 24 24 69 1e 74 ba be 72 5b b3 84 6e 2a |#[.$$i.t..r[..n*|
00000290 0d fa 64 8b 08 ce 3a e7 9b 51 41 e4 f0 |..d...:..QA..|
0000029d
Hmmm… no strings. The password is not simply encoded and stored somewhere as a string. Well, we need to understand in more depth the target architecture!
Understanding the target architecture
First steps
To achieve this, I sent only the first byte of the program to experiment (note: I also changed sample.py
to take the program to be executed as a parameter).
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ echo -ne '\xf8' > prog.1.bin
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ python sample.py prog.1.bin
[...]
+-----------------------------+
| THE FIRMWARE IS RUNNING NOW |
+-----------------------------+
=== PROGRAM CRASHED ===
Instruction pointer went out of memory range
---- REGISTER INFO ----
IP: 0xffff
FLG: 0x0
R1: 0xf
R2: 0x0
R3: 0x0
R4: 0x0
R5: 0x0
R6: 0x0
R7: 0x0
R8: 0x0
POINTED ADDRESS: 0x0
---- CALL STACK ----
[]
[...]
We learned 3 new facts :
- The maximum
IP
is0xffff
, suggesting the target memory is only addressable on 2 bytes. So POINTED ADDRESS must also be on 2 bytes. - It seems like a sequence of NULL bits indicates a NOP
- The provided byte was enough to set
R1
to0x0f
Lets add the second byte :
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ echo -ne '\xf8\x24' > prog.2.bin
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ python sample.py prog.2.bin
[...]
+-----------------------------+
| THE FIRMWARE IS RUNNING NOW |
+-----------------------------+
=== PROGRAM CRASHED ===
Instruction pointer went out of memory range
---- REGISTER INFO ----
IP: 0xffff
FLG: 0x0
R1: 0xf0
R2: 0x0
R3: 0x0
R4: 0x0
R5: 0x0
R6: 0x0
R7: 0x0
R8: 0x0
POINTED ADDRESS: 0x0
---- CALL STACK ----
[]
[...]
Interesting… Under the assumption that every instruction is 1 byte long, 0xf8
can set 0x0f
in R1
and 0x24
can shift R1
by 4 bits to the left. What happens if we sent 0xf9
instead of 0xf8
? Let’s see
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ echo -ne '\xf9\x24' > prog.3.bin
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ python sample.py prog.3.bin
[...]
=== PROGRAM CRASHED ===
Instruction pointer went out of memory range
---- REGISTER INFO ----
IP: 0xffff
FLG: 0x0
R1: 0x0
R2: 0xf
R3: 0x0
R4: 0x0
R5: 0x0
R6: 0x0
R7: 0x0
R8: 0x0
POINTED ADDRESS: 0x0
---- CALL STACK ----
[]
[...]
Aha ! We managed to put 0x0f
in R2
instead, and it was not shifted to the left. Since there are 8 general purpose registers, the last 3 bits must be used to select the target register. Let’s try with R8
(0xff = 11111 111
) :
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ echo -ne '\xff\x24' > prog.4.bin
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ python sample.py prog.4.bin
[...]
=== PROGRAM CRASHED ===
Instruction pointer went out of memory range
---- REGISTER INFO ----
IP: 0xffff
FLG: 0x0
R1: 0x0
R2: 0x0
R3: 0x0
R4: 0x0
R5: 0x0
R6: 0x0
R7: 0x0
R8: 0xf
POINTED ADDRESS: 0xf
---- CALL STACK ----
[]
Yup, exactly what was expected. But wait, the POINTED ADDRESS
was also altered ! And its value matches exactly the value stored R8
. However, R8
is only 1 byte long and the program needs to address 2 bytes of memory. Therefore, under the assumption that POINTED ADDRESS
is nothing more than the concatenation of two registers, let’s try modifying R7
:
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ echo -ne '\xfe' > prog.5.bin
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive/original_files]$ python sample.py prog.5.bin
[...]
=== PROGRAM CRASHED ===
Instruction pointer went out of memory range
---- REGISTER INFO ----
IP: 0xffff
FLG: 0x0
R1: 0x0
R2: 0x0
R3: 0x0
R4: 0x0
R5: 0x0
R6: 0x0
R7: 0xf
R8: 0x0
POINTED ADDRESS: 0xf00
---- CALL STACK ----
[]
[...]
Therefore, it can be deduced that POINTED ADDRESS = [R7:R8]
.
Complete target architecture
I am not going to detail exactly how I found out about the full ISA because there is not much to say about it. There is no proper method : this is just a bunch of trials and errors. After enough attempts, the full picture becomes clear. In this section, I tediously summarized all the instructions that the target system supports, but feel free to do it on your own and compare your results afterwards.
Registers
All registers from R1
to R8
are of size = 1
byte.
5
general purpose registers :R2
toR6
.1
“main” registerR1
(please see the available instructions to understand the name)2
“address pointer” registers :[R7:R8] = POINTED ADDRESS
.IP
on 2 bytes : points to the current instructionFLAGS = [GT, 2bits] [LT, 2bits] [Z, 1bit]
Instruction set
0x00, 0x14, 0x15, 0x16, 0x17, 0x14, 0x15, 0x16, 0x17
:nop
.0x05, 0x06, 0x07, 0x08, 0x10
: unkown.0x01
:syscall
.R1
holds the syscall number (1
=read,2
=write). The parameter isADDR POINTER
.0x02
:call
. Pushes currentIP
in the call stack and jumps toADDR POINTER
.0x03
:ret
. Pops the call stack and jumps to the savedIP
+ 1.0x04
:xcg
. Exchanges[R5:R6]
with[R7:R8]
.0x11
:jmp
. Jumps toADDR POINTER+1
.0x12
:jz
. Jumps toADDR POINTER+1
if and only ifZ=1
.0x13
:jnz
. Jumps toADDR POINTER+1
if and only ifZ=0
.0b00011 [shift, 3 bits]
:shr
. ShiftsR1
by[shift]
bits to the right.0b00100 [shift, 3 bits]
:shl
. ShiftsR1
by[shift]
bits to the left.0b00101 [dst reg, 3 bits]
:not
. NOT logical operation.0b00110 [src reg, 3 bits]
:or
.R1 <- R1 OR [src reg]
.0b00111 [src reg, 3 bits]
:and
.R1 <- R1 AND [src reg]
.0b01000 [src reg, 3 bits]
:xor
.R1 <- R1 XOR [src reg]
.0b01001 [src reg, 3 bits]
:sub
.R1 <- R1 SUB [src reg]
.0b01010 [src reg, 3 bits]
:sub
.R1 <- R1 ADD [src reg]
.0b01011 [reg, 3 bits]
:cmp
. ComparesR1
with[reg]
and sets the flags.0b01100 [dst reg, 3 bits]
:ld
. Loads byte in memory at addressADDR POINTER
into[dst reg]
.0b01101 [src reg, 3 bits]
:st
. Stores[src reg]
in memory at addressADDR POINTER
.0b01110 [dst reg, 3 bits]
:mov
. Moves the content ofR1
into[dst reg]
.0b01111 [src reg, 3 bits]
:mov
. Moves the content of[src reg]
intoR1
.0b1 [imm, 4 bits] [dst reg, 3 bits]
:seti
. Sets the immediate value[imm]
into register[dst reg]
.
Disassembling and understanding the program
Now we know every instruction. Let’s write a small program to disassemble prog.bin
.
[micronoyau@pwnixos:~/Documents/ctfs/ecw/interactive]$ python decode.py disas progs/prog.bin
0x00 : seti r1, 0x0f
0x01 : shl r1, 4
0x02 : seti r2, 0x0f
0x03 : or r1, r2
0x04 : mov r2, r1
0x05 : seti r1, 0x04
0x06 : shl r1, 4
0x07 : seti r3, 0x02
0x08 : or r1, r3
0x09 : mov r3, r1
0x0a : seti r1, 0x06
0x0b : shl r1, 4
0x0c : seti r4, 0x09
0x0d : or r1, r4
0x0e : mov r4, r1
0x0f : seti r6, 0x00
0x10 : seti r5, 0x02
0x11 : seti r7, 0x00
0x12 : seti r1, 0x0d
0x13 : shl r1, 4
0x14 : mov r8, r1
0x15 : call
0x16 : seti r1, 0x05
0x17 : shl r1, 4
[...]
To understand better the behaviour I also wrote a small debugger to step through the execution of the program.
- The string “Hello and welcome. Please enter the code that confirms you are my AI overlord:\n” is stored at address
0x200
. write
syscall is made to print this string to the screen.read
syscall is called repeatedly (for a maximum of0x40
rounds) until it reads a NULL byte to store user input at address0x800
.- Another function is called
- Finally a check function is called, checking character by character the input string.
To solve the challenge, we only need to put a breakpoint on the comparison instruction (address 0x165
) and to look at the value inside the register r4
holding the flag. This allows us to extract the flag character per character.
FLAGS : ZF=1 GT=0 LT=0 ADDR PTR : 0176 IP : 0165
CALLSTACK : ['0x0044']
seti r8, 0x06 r1 | 45
or r1, r8
mov r8, r1 r2 | 00
seti r7, 0x01
mov r1, r3 r3 | 45
=>* cmp r1, r4
jnz r4 | 45
mov r1, r2
seti r2, 0x01 r5 | 00
add r1, r2
mov r2, r1 r6 | d0
r7 | 01
r8 | 76
Flag : ECW{Th1S_1s_A_pL@c3h0lD3R_P4S5worD_d0_n07_fORg37_to_Ch4nG3_M3}
.