ncore -- CSAW Quals 2021
ncore Writeup – CSAW Quals 2021
Points: 484 (dynamic)
Solves: 53
Description:
We have a very safe core with a very safe enclave
Problem:
Server
We are given a server.py
file that is running on the server that reads user input and stores it in a ram.hex
file. The server then uses vvp
(Icarus Verilog vvp runtime engine) to run a compiled SystemVerilog
file called nco
.
For debugging, you can install the iverilog
compiler, which compiles SystemVerilog
source files to vvp assembly
, which can then be executed by vvp
.
You can compile SystemVerilog
with the following command:
iverilog -g2009 -o nco ncore_tb.v
The -g2009
flag informs the compiler of the language generation to support, being SystemVerilog
supported since g2009
.
Verilog VM
We are also given the file ncore_tb.v
containing the Verilog
source code.
Reading through the code we can see that it implements a sort of VM that runs commands stored in its ram, which the user provides.
The VM’s structures are the following:
safe_rom - 256 byte array
ram - 256 byte array
key - 32 bit array
emode - 1 bit
regfile - array with 4 32-bit entries
The startup sequence looks like this:
initial
begin: initial_block
init_regs();
emode = 0;
set_key();
load_safeROM();
load_ram();
#1500000;
// after 1500000 time units, call print_res
print_res();
$finish;
end :initial_block
init_regs
- initializes the 4 registers inregfile
to 0set_key
- reads 32 bits from/dev/urandom
and stores it inkey
load_safeROM
- reads the contents of a file calledflag.txt
intosafe_rom
load_ram
- reads the contents of theram.hex
file intoram
.print_res
- print the last 64 bytes ofram
Instructions
The main loop of the VM is parsing the user-provided ram
for instructions.
The instructions are 2 bytes long and the opcode is always the first 4 bits.
These instructions can be used:
ADD (opcode 0)
regfile[DD] = regfile[R1] + regfile[R2]; pc += 2;
idx: 0123456701234567
content: 0000DDR1R2------
INC (opcode 12)
regfile[DD] = regfile[DD] + 1 ; pc += 2;
idx: 0123456701234567
content: 1100DD----------
SUB (opcode 1)
regfile[DD] = regfile[R1] - regfile[R2] ; pc += 2;
idx: 0123456701234567
content: 0000DDR1R2------
MOVF (opcode 5)
regfile[DD] = ram[RAM_ADDR] ; pc += 2;
idx: 0123456701234567
content: 0101DD--RAM_ADDR
MOVFS (opcode 13)
- Only in emode
regfile[DD] = safe_rom[FLAGADDR] ; pc += 2; idx: 0123456701234567 content: 1101DD--FLAGADDR
MOVT (opcode 6)
ram[RAM_ADDR] = regfile[DD][0:7] ; pc += 2;
idx: 0123456701234567
content: 0110DD--RAM_ADDR
JGT (opcode 9)
pc = regfile[r1] > regfile[r2] ? RAM_ADDR : pc+2
idx: 0123456701234567
content: 1001R1R2RAM_ADDR
JEQ (opcode 10)
pc = regfile[r1] == regfile[r2] ? RAM_ADDR : pc+2
idx: 0123456701234567
content: 1010R1R2RAM_ADDR
JMP (opcode 11)
pc = ram[RAM_ADDR] ;
idx: 0123456701234567
content: 1011----RAM_ADDR
ENT (opcode 7)
if key[0:13] == regfile[0]:
emode = 1
regfile[3] = 0
else:
regfile[3] = 1
pc += 2;
idx: 0123456701234567
content: 0111------------
EXT (opcode 8)
emode = 0 ; pc += 2
idx: 0123456701234567
content: 1000--------
Extracting the Flag
- We cannot access the
safe_rom
where the flag is stored unlessemode = 1
emode
is set to 1 if regfile[0] contains the first 14 bits of secret key obtained from/dev/urandom
.
The plan:
- Brute force the first 14 bits of the key by repeatedly calling
ENT
with regfile[0] = 1..2^14 - When
regfile[3] == 1
stop the brute force - Write the content of
safe_rom
in the last 64 bytes ofram
- Wait for timeout
The Solution
def code_at(ram, addr, code):
ram[addr] = p8(code[0])
ram[addr+1] = p8(code[1])
def go():
s = remote(HOST, PORT)
ram = [b'\x00' for i in range(256)]
# create an infinite loop
code_at(ram, 150, JMP(154))
code_at(ram, 154, JMP(150))
# try to enter emode (reg0 == key? reg3 = 1 else reg3 = 0)
code_at(ram, 0, ENT())
code_at(ram, 2, JEQ(8, 2, 3))
code_at(ram, 4, INC(0))
code_at(ram, 6, JMP(0))
# hardcode the flag leakage instructions for simplicity
pc = 8
for i in range(0, 32):
code_at(ram, pc, MOVFS(2, i))
pc += 2
code_at(ram, pc, STORE(2, 255-i))
pc += 2
# jump to infinite loop and wait for timeout
code_at(ram, pc, JMP(150))
# separate each character by a space
bytestrm = " ".join([c.hex() for c in ram])
s.sendlineafter(b'WELCOME', bytestrm)
s.interactive()
go()
Running the script, the server outputs the following:
ENT
66 6c 61 67 7b 64 30 6e 54 5f 6d 45 53 73 5f 77 69 54 68 5f 74 48 65 5f 73 43 68 4c 41 6d 69 7d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
From which we can decode the flag: flag{d0nT_mESs_wiTh_tHe_sChLAmi}
The full solution can be found in solve.py.