Hardware Challenges [ACSC 2024] - Thu, Apr 11, 2024
TLDR
I’ve wanted to learn hardware for a while, so I tried out some hardware challenges at ACSC 2024. Here are the writeups for the 4 hardware challenges I’ve managed to solve during the CTF (as well as 1 stray web challenge that I solved because I couldn’t get the last hardware chall :"<)!
Table of Contents
An4lyz3_1t
Our surveillance team has managed to tap into a secret serial communication and capture a digital signal using a Saleae logic analyzer. Your objective is to decode the signal and uncover the hidden message.
Authored by Chainfire73
A “chall.sal” file was provided, which is the filetype for the Saleae Logic Analyzer.
We will open the file and create a new “Async Serial” analyzer:
As you can see, the bit rate has been 57600 Bits/s. In this case that works very nicely, but in some cases, you will have to measure the bit rate, which you can do with the “Baud rate estimate” plugin that Saleae has.
Once that is done, return back to the main screen and go to the Analyzers tab. In the data terminal, you will be able to see the flag!
Vault
Can you perform side-channel attack to this vault? The PIN is a 10-digit number.
Authored by v3ct0r and Chainfire73
A binary vault
was provided, as well as a Dockerfile to start a container to test the exploit locally. When you connect to the remote netcat instance, you will be given a user shell with access to python.
Let’s take a look at the decompilation of the vault binary:
void main(void)
{
ssize_t sVar1;
int i;
printart();
printf("Enter your PIN: ");
fflush((FILE *)stdout);
sVar1 = read(0,input.1,10);
if ((int)sVar1 == 10) {
delay();
for (i = 0; i < 10; i = i + 1) {
if ((int)(char)input.1[i] != (i + 1U ^ (int)(char)pins[(long)i * 0xaf + 0x45])) {
flag.0 = 0;
puts("Access Denied\n It didn\'t take me any time to verify that it\'s not the pin");
return;
}
delay();
}
if (flag.0 != 0) {
printflag(input.1);
}
}
else {
puts("Access Denied\n It didn\'t take me any time to verify that it\'s not the pin");
}
return;
}
The binary first checks that the PIN entered is 10 characters long. After which, the binary starts doing a character-by-character password check. Here is the logic of the password check:
- Check if the character is correct
- If the character is not correct, instantly fail and return
- If the character is correct, delay(), then return to instruction 1
In short, we will be able to perform a timing-based side channel attack and bruteforce the PIN digit by digit. This is because if the character is correct, the program will take longer to run as it calls delay and starts another iteration of the loop, but if the character is wrong, the program will just return. We can do that with a simple python script (I had to use subprocess because pwntools was not installed on the remote box :<):
import subprocess
import time
## ACSC{b377er_d3L4y3d_7h4n_N3v3r_b42fd3d840948f3e} -- the flag!
password = ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"]
## Initialize stuff
diff_arr = []
pay_arr = []
for k in range(10):
diff_arr = []
pay_arr = []
for i in range(10):
## Prepare payload
password[k] = str(i)
payload = "".join(password)
payload = payload.encode("utf-8")
pay_arr.append(payload)
print(payload)
proc = subprocess.Popen(["/home/user/chall"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
start = time.time()
cat, err = proc.communicate(payload)
diff = time.time() - start
diff_arr.append(diff)
print(diff)
print("---")
print("RESULTS: ")
print(max(diff_arr))
print(pay_arr[diff_arr.index(max(diff_arr))])
password[k] = str(diff_arr.index(max(diff_arr)))
Once the script is run, you will have the correct PIN, just enter that and the flag will pop out!
picopico
Security personnel in our company have spotted a suspicious USB flash drive. They found a Raspberry Pi Pico board inside the case, but no flash drive board. Here's the firmware dump of the Raspberry Pi Pico board. Could you figure out what this 'USB flash drive' is for?
Authored by op
This challenge was fun because I found out about a really cool website: https://binvis.io/#/ (it is very pretty, and has colors! I love colors)
Anyways, back to the challenge. We were provided with a firmware.bin
file. Being a noob, I decided to do the first thing everyone does with dealing with unknown stuff: run strings.
While running strings, I noticed that there was some sort of python code that looked like it was used for encoding key presses:
And there was also this really suspicious bit at the end:
import storage
storage.disable_usb_drive()
import time
L=len
o=bytes
l=zip
import microcontroller
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode
w=b"\x10\x53\x7f\x2b"
a=0x04
K=43
if microcontroller.nvm[0:L(w)]!=w:
microcontroller.nvm[0:L(w)]=w
O=microcontroller.nvm[a:a+K]
h=microcontroller.nvm[a+K:a+K+K]
F=o((kb^fb for kb,fb in l(O,h))).decode("ascii")
S=Keyboard(usb_hid.devices)
C=KeyboardLayoutUS(S)
time.sleep(0.1)
S.press(Keycode.WINDOWS,Keycode.R)
time.sleep(0.1)
S.release_all()
time.sleep(1)
C.write("cmd",delay=0.1)
time.sleep(0.1)
S.press(Keycode.ENTER)
time.sleep(0.1)
S.release_all()
time.sleep(1)
C.write(F,delay=0.1)
time.sleep(0.1)
S.press(Keycode.ENTER)
time.sleep(0.1)
S.release_all()
time.sleep(0xFFFFFFFF)
So what this seemed like it did was that it took a bunch of bytes from some region of memory. It then does the following:
- Check if the first 4 bytes are “\x10\x53\x7f\x2b” – some sort of magic number I suppose
- If the check passes, perform XOR operations starting from the 5th byte together with the 5+43th byte and work all the way down until you get the flag
Now the challenge would be to find the target bytes in the firmware dump; this is where binvis.io comes to play!
So what binvis.io does is that it takes a binary and colors in regions of data depending on whether it is filled with null bytes, ascii characters, low/high bytes, or 0xff. Different binaries look different, and the website itself shows you the difference between ELF, PE and PDF files:
If we take a look at the firmware.bin file, it looks like this:
There is this section that is particularly interesting:
That number looks familiar! That is actually our target for the XOR, and it contains the flag!
We can simply dump the memory from that region and perform the XOR to get the flag:
info = b"\x10\x53\x7f\x2b\x41\xa0\x71\x51\x9f\xca\xfd\x84\x35\x0a\xd2\xb0\x1e\xa8\xa9\xb7\x10\x1f\x55\x7a\x8c\x98\xb2\x69\xef\x92\xc5\x15\xd0\x4b\xff\x87\x17\x63\xe4\x62\xc6\xa5\xb2\xbc\x8e\xef\xd8\x24\xc3\x19\x3e\xbf\x8b\xbe\xd7\x76\x71\xe1\x84\x27\x98\x9d\x87\x73\x2e\x63\x19\xbf\xae\xd4\x0b\x8d\xf3\xfd\x76\xe4\x73\xcb\xe5\x25\x5b\xdd\x07\xf6\xc1\xd3\xd9\xb8\x89\xa5\x00\x00\x00\x00\x00\x00"
flag = []
for i in range(43):
flag.append(chr(info[4+i] ^ info[4+i+43]))
print("".join(flag))
The script will return echo ACSC{349040c16c36fbba8c484b289e0dae6f}
!
Sidenote: There are two other ways to potentially find the target memory region.
- Use a python script to comb the entire memory for the magic number, and dump that region of memory
- XOR the entire firmware dump, and run strings a second time to find the flag
(But I think the pretty colors makes the binvis method worth XD)
PWR_Tr4ce
You've been given power traces and text inputs captured from a microcontroller running AES encryption. Your goal is to extract the encryption key.
EXPERIMENT SETUP
scope = chipwhisperer lite
target = stm32f3
AES key length = 16 bytes
Authored by Chainfire73
This challenge is about power analysis, which is a type of side-channel attack that looks at the power consumption of hardware to gain information about the operations that the machine is performing. Some types of math require more power than others, and by analyzing power traces, we are able to determine what cryptographic algorithms are being used, and how we can potentially crack them.
We are given 2 files: textins.npy
and traces.npy
. Textins is the array for the plain text, and traces is the power traces. Having never done a power analysis before, I went to google for tutorials, and came across this wonderful walkthrough for power analysis by coastalwhite: https://coastalwhite.github.io/intro-power-analysis/intro.html
In his walkthrough, he goes through step by step how to crack an AES key, which is what we need here! This is all covered superbly in the “Breaking AES” section of his writeup, and the script he provides works for solving this challenge:
#!/bin/python3
import numpy as np
from tqdm import trange
# Modeling the power consumption
########################################
HammingWeightFn = lambda x: bin(x).count('1')
# Precompute Hamming Weight
HammingWeight = [ HammingWeightFn(n) for n in range (0x00, 0xff + 1) ]
# Rijndael Substitution box
SBox = [
# 0 1 2 3 4 5 6 7 8 9 a b c d e f
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, # 0
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, # 1
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, # 2
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, # 3
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, # 4
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, # 5
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, # 6
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, # 7
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, # 8
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, # 9
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, # a
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, # b
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, # c
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, # d
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, # e
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 # f
]
def hypothetical_power_usage(subkey, plain_text_char):
# Use the Hamming Weight power usage model
return HammingWeight[
# Do a SBox look up of the XOR-ed value
#
# Since the Hamming Weight of the SBox value will
# persist for longer in memory this will make finding the
# pattern easier. It is also still before the Row Shifting
# so it doesn't cause trouble.
SBox [
# The initial round key XOR-ed with the plain text
subkey ^ plain_text_char
]
]
########################################
# Loading our trace data
########################################
import numpy as np
traces = np.load('./traces.npy')
textins = np.load('./textins.npy')
num_traces = np.shape(traces)[0]
num_points = np.shape(traces)[1]
########################################
# Pearson correlation
########################################
def covariance(X, Y):
if len(X) != len(Y):
print("Lengths are unequal, quiting...")
quit()
n = len(X)
mean_x = np.mean(X, dtype=np.float64)
mean_y = np.mean(Y, dtype=np.float64)
return np.sum((X - mean_x) * (Y - mean_y)) / n
def standard_deviation(X):
n = len(X)
mean_x = np.mean(X, dtype=np.float64)
return np.sqrt( np.sum( np.power( (X - mean_x), 2 ) ) / n )
def pearson_correlation_coefficient(X, Y):
cov = covariance(X, Y)
sd_x = standard_deviation(X)
sd_y = standard_deviation(Y)
return cov / ( sd_x * sd_y )
########################################
# Define a function to calculate the Correlation Coefficients for a byte in a
# subkey.
########################################
def calculate_correlation_coefficients(subkey, subkey_index):
# Declare a numpy for the hypothetical power usage
hypothetical_power = np.zeros(num_traces)
for trace_index in range(0, num_traces):
hypothetical_power[trace_index] = hypothetical_power_usage(
subkey,
textins[trace_index][subkey_index]
)
# We are going to the determine correlations between each trace point
# and the hypothetical power usage. This will save all those coefficients
point_correlation = np.zeros(num_points)
# Loop through all points and determine their correlation coefficients
for point_index in range(0, num_points):
point_correlation[point_index] = pearson_correlation_coefficient(
hypothetical_power,
# Look at the individual traces points for every trace
traces[:, point_index]
)
return point_correlation
########################################
# Looping through all possible bytes
########################################
# Save all correlation coefficients
max_correlation_coefficients = np.zeros(256)
# Loop through values this subkey
for subkey in trange(0xff + 1, desc="Attack Subkey"):
max_correlation_coefficients[subkey] = max(abs(
calculate_correlation_coefficients(subkey, 0)
))
########################################
# Printing the best guess
########################################
# Select the element with the highest correlation
best_guess = np.argmax(max_correlation_coefficients)
# Print both the hex value and the ASCII character
print("Best guess: {:02x} or '{}'".format(best_guess, chr(best_guess)))
########################################
# Looping through all subkeys
########################################
# The eventual key guess
best_guess = np.zeros(16)
# Loop through all possible subkeys
for subkey_index in trange(16, desc="Subkey Index"):
# Save all correlation coefficients
max_correlation_coefficients = np.zeros(256)
# Loop through values this subkey
for subkey in range(0x00, 0xff + 1):
max_correlation_coefficients[subkey] = max(abs(
calculate_correlation_coefficients(subkey, subkey_index)
))
# Save the best guess
best_guess[subkey_index] = np.argmax(max_correlation_coefficients)
########################################
# Printing the best guess
########################################
print("Best guess:")
for b in best_guess: print("{:02x} ".format(int(b)), end="")
print("")
for b in best_guess: print("{}".format(chr(int(b))), end="")
print("")
########################################
Running the script will give us the flag!
Login!
I really wanted to solve the last hardware chall and max hardware, but sadly I did not know enough about RFID to solve it. It will definitely be one of the things to learn though!
While getting frustrated at the RFID challenge I decided to take a look at this web challenge for fun (and also because I was scared of Frida for the other pwn chall that I wanted to try – it’s also on my list of stuff to learn!):
Here comes yet another boring login page ... http://login-web.chal.2024.ctf.acsc.asia:5000
Authored by splitline
The source of the webpage was given in app.js
:
const express = require('express');
const crypto = require('crypto');
const FLAG = process.env.FLAG || 'flag{this_is_a_fake_flag}';
const app = express();
app.use(express.urlencoded({ extended: true }));
const USER_DB = {
user: {
username: 'user',
password: crypto.randomBytes(32).toString('hex')
},
guest: {
username: 'guest',
password: 'guest'
}
};
app.get('/', (req, res) => {
res.send(`
<html><head><title>Login</title><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head>
<body>
<section>
<h1>Login</h1>
<form action="/login" method="post">
<input type="text" name="username" placeholder="Username" length="6" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</section>
</body></html>
`);
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username.length > 6) return res.send('Username is too long');
const user = USER_DB[username];
if (user && user.password == password) {
if (username === 'guest') {
res.send('Welcome, guest. You do not have permission to view the flag');
} else {
res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
}
} else {
res.send('Invalid username or password');
}
});
app.listen(5000, () => {
console.log('Server is running on port 5000');
});
The “guest” and “user” users exist. “guest”’s password is “guest”, but “user”’s password is some giant randomly generated hex string, which is not fun. Looking at the source code will tell us that guest seemingly does not have permission to view the flag.
Here is the interesting part of the source code:
const user = USER_DB[username];
if (user && user.password == password) {
if (username === 'guest') {
res.send('Welcome, guest. You do not have permission to view the flag');
} else {
res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
}
} else {
res.send('Invalid username or password');
}
It checks whether the user exists (i.e. user or guest), and then checks if the user’s password is the same as the one in the object. It then performs a “===” operation with the string “guest”, and this is where the vulnerability lies.
Basically, Javascript is weird, and there are two types of equality. “==” or loose equality will first convert both operands into the same type before performing a comparison, while “===” or strict equality will check that the types and values of both operands are the same before returning true. if (username === 'guest')
hence means that username must have a value equal to ‘guest’ AND be a string in order for this condition to be fulfilled.
A simple bypass for this would be to simply have username not be a string! We can provide a username that is an array but is still equal to ‘guest’, which will fail the strict equality check and give us the flag.
Using Burp, we can see the original request sent to the web server:
As expected this fails, as the username is a string.
We can modify the web request to get the flag:
The payload used was username[]=guest&password=guest
, which would result in username being an object instead of a string.
Thanks for reading this writeup and props to the ACSC team for a wonderful CTF! :D