ProHoster > Blog > Administration > Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC
Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC
This is the second and final part of the article about hacking external self-encrypting drives. I remind you that a colleague recently brought me a Patriot (Aigo) SK8671 hard drive, and I decided to reverse it, and now I'm sharing what happened. Before reading further, be sure to check out first part Article.
4. We start to dump from the internal PSoC flash drive
So, everything points to (as we established in [first part]()) that the pincode is stored in the flash bowels of the PSoC. Therefore, we need to read these flash bowels. Front of necessary work:
take control of "communication" with the microcontroller;
find a way to check if this "communication" is protected from reading from the outside;
find a way to bypass the protection.
There are two places where it makes sense to look for a valid pincode:
internal flash memory;
SRAM, where the pincode can be stored to compare it with the pincode entered by the user.
Looking ahead, Iβll note that I still managed to dump the internal PSoC flash drive - bypassing its protection system, through a hardware cold boot tracing attack - after reversing the undocumented features of the ISSP protocol. This allowed me to directly dump the actual pincode.
$ ./psoc.py
syncing: KO OK
[...]
PIN: 1 2 3 4 5 6 7 8 9
"Communicating" with a microcontroller can mean anything from "vendor to vendor" to interacting using a serial protocol (such as ICSP for Microchip's PIC).
Cypress has its own proprietary protocol for this, called ISSP (in-system serial programming protocol), which is partly described in technical specification. US7185162 patent also gives some information. There is also an OpenSource counterpart called HSSP (we'll use it a little later). ISSP works like this:
restart PSoC;
output a magic number to the serial data leg of this PSoC; to enter the external programming mode;
send commands that are long bit strings called "vectors".
The ISSP documentation defines these vectors for only a handful of commands:
Initialize-1
Initialize-2
Initialize-3 (3V and 5V options)
ID-SETUP
READ-ID-WORD
SET-BLOCK-NUM: 10011111010dddddddd111 where dddddddd=block #
BULK ERASE
PROGRAM BLOCK
VERIFY-SETUP
READ-BYTE: 10110aaaaaaZDDDDDDDDZ1 where DDDDDDDD = data out, aaaaaa = address (6 bits)
WRITE-BYTE: 10010aaaaaadddddddd111 where dddddddd = data in, aaaaaa = address (6 bits)
SECURE
CHECKSUM-SETUP
READ-CHECKSUM: 10111111001ZDDDDDDDDZ110111111000ZDDDDDDDDZ1, where DDDDDDDDDDDDDDDD = data out: device checksum
All vectors have the same length: 22 bits. The HSSP documentation has some additional information on ISSP: "ISSP vector is nothing more than a bit sequence, which is a set of instructions."
5.2. Demystification of vectors
Let's figure out what's going on here. Initially, I assumed that these same vectors were raw versions of M8C instructions, but after checking this hypothesis, I found that the opcodes of the operations did not match.
Then I googled the above vector and came across here it is a study where the author, while not diving into the details, provides some practical clues: βEach instruction starts with three bits that correspond to one of four mnemonics (read from RAM, write to RAM, read register, write register). Then comes the 8-bit address, followed by 8 bits of data (read or write), and finally three stop bits.
Then I managed to glean very useful information from the section "Supervisory ROM (SROM)" technical guidance. SROM is a hard-coded ROM, in PSoC, that provides services (in a similar fashion to Syscall) for program code running in user space:
00h:SWBootReset
01h: ReadBlock
02h: WriteBlock
03h: EraseBlock
06h: TableRead
07h: CheckSum
08h: Calibrate0
09h: Calibrate1
By comparing vector names with SROM functions, we can match the various operations supported by this protocol with the expected SROM parameters. Thanks to this, we can decode the first three bits of ISSP vectors:
100
101 => "rdmem"
110
111
However, a complete understanding of on-chip processes can only be obtained through direct communication with PSoC.
5.3. Communication with PSoC
Since Dirk of Petrautsky already ported Cypress' Arduino HSSP code, I used an Arduino Uno to connect to the ISSP header of the keyboard board.
Please note that in the course of my research, I changed Dirk's code quite a bit. You can find my modification on GitHub: here and the corresponding Python script for communicating with Arduino, in my repository cypress_psoc_tools.
So, using Arduino, I first used only "official" vectors to "communicate". I tried to read the internal ROM using the VERIFY command. As expected, I failed to do so. Probably because the read protection bits are activated inside the flash drive.
Then I created some of my simple vectors for writing and reading memory / registers. Please note that we can read the entire SROM, even though the flash drive is protected!
5.4. Identification of on-chip registers
Looking at the "disassembled" vectors, I found that the device uses undocumented registers (0xF8-0xFA) to specify M8C opcodes that are executed directly, bypassing security. This allowed me to run various opcodes such as "ADD", "MOV A, X", "PUSH", or "JMP". Thanks to them (by looking at the side effects they have on registers) I was able to determine which of the undocumented registers are actually ordinary registers (A, X, SP and PC).
As a result, the "disassembled" code generated by the HSSP_disas.rb tool looks like this (I added comments for clarity):
At this stage, I can already communicate with PSoC, but I still do not have reliable information about the security bits of the flash drive. I was very surprised by the fact that Cypress does not give the user of the device any means to check if the protection is activated. I delved into Google to finally understand that the HSSP code provided by Cypress was updated after Dirk released his modification. And so! Here is a new vector:
Using this vector (see read_security_data in psoc.py), we get all the security bits in SRAM at 0x80, where there are two bits per security block.
The result is depressing: everything is protected in the "disable external read and write" mode. Therefore, we can not only read anything from a flash drive, but also write anything (for example, to introduce a ROM dumper there). And the only way to disable protection is to completely erase the entire chip. π
6. First (failed) attack: ROMX
However, we can try the following trick: since we have the ability to execute arbitrary opcodes, why not execute ROMX, which is used to read flash memory? This approach has a good chance of success. Because the ReadBlock function that reads data from SROM (which is used by vectors) checks if it is called from ISSP. However, the ROMX opcode may presumably not have this check. So, here's the Python code (after adding a few helper classes to the Sish Arduino code):
for i in range(0, 8192):
write_reg(0xF0, i>>8) # A = 0
write_reg(0xF3, i&0xFF) # X = 0
exec_opcodes("x28x30x40") # ROMX, HALT, NOP
byte = read_reg(0xF0) # ROMX reads ROM[A|X] into A
print "%02x" % ord(byte[0]) # print ROM byte
Unfortunately this code doesn't work. π Rather, it works, but we get our own opcodes (0x28 0x30 0x40) at the output! I do not think that the corresponding functionality of the device is an element of read protection. This is more like an engineering trick: when executing external opcodes, the ROM bus is redirected to a temporary buffer.
This is essentially a call to the SROM function 0x07, as presented in the documentation (emphasis mine):
This is a checksum check function. It calculates a 16-bit checksum of the number of blocks specified by the user - in one flash bank, counting from zero. The BLOCKID parameter is used to pass the number of blocks to be used when calculating the checksum. A value of "1" will only calculate the checksum for block zero; whereas "0" will cause the total checksum of all 256 blocks of the flash bank to be calculated. A 16-bit checksum is returned via KEY1 and KEY2. In the KEY1 parameter, the lower 8 bits of the checksum are fixed, and in KEY2, the upper 8 bits. For devices with multiple flash banks, the checksum function is called for each individually. The number of the bank with which it will work is set by the FLS_PR1 register (by setting the bit in it corresponding to the target flash bank).
Note that this is the simplest checksum: the bytes are simply added one by one; no fancy CRC quirks. In addition, knowing that the set of registers in the M8C core is very small, I assumed that when calculating the checksum, intermediate values ββwould be fixed in the same variables that would eventually go to the output: KEY1 (0xF8) / KEY2 (0xF9).
So, in theory, my attack looks like this:
We connect through ISSP.
We start the calculation of the checksum, using the CHECKSUM-SETUP vector.
We reboot the processor after a given time T.
Read RAM to get the current C checksum.
Repeat steps 3 and 4, each time slightly increasing T.
We recover data from a flash drive by subtracting the previous checksum C from the current one.
However, there was a problem: the Initialize-1 vector, which we have to send after the reload, overwrites KEY1 and KEY2:
This code overwrites our precious checksum by calling Calibrate1 (SROM function 9)... Maybe we can just send the magic number (from the beginning of the above code) to enter programming mode and then read the SRAM? And yes, it works! The Arduino code that implements this attack is quite simple:
Calls the Arduino function Cmnd_STK_START_CSUM (0x85) with a delay in microseconds as a parameter.
Reads the checksum (0xF8 and 0xF9) and the undocumented register 0xF1.
This code is executed 10 times in 1 microsecond. 0xF1 is included here because it was the only register that changed when calculating the checksum. Perhaps this is some kind of temporary variable used by the arithmetic logic unit. Note the ugly hack I use to reset the Arduino using picocom when the Arduino stops beeping (I have no idea why).
7.2. Reading the result
The result of the Python script looks like this (simplified for readability):
However, we have a problem: since we are operating with the actual checksum, the zero byte does not change the read value. However, since the entire calculation procedure (8192 bytes) takes 0,1478 seconds (with small deviations on each run), which is approximately 18,04 Β΅s per byte, we can use this time to check the checksum value at appropriate times. For the first runs, everything is read quite easily, since the duration of the computational procedure is always almost the same. However, the end of this dump is less accurate because the "minor timing deviations" on each run add up to become significant:
134023 D0 02 DD
134023 CC D2 DC
134023 CC D2 DC
134023 CC D2 DC
134023 FB D2 DC
134023 3F D2 DC
134023 CC D2 DC
134024 02 02 DC
134024 CC D2 DC
134024 F9 02 DC
134024 03 02 DD
134024 21 02 DD
134024 02 D2 DC
134024 02 02 DC
134024 02 02 DC
134024 F8 D2 DC
134024 F8 D2 DC
134025 CC D2 DC
134025 EF D2 DC
134025 21 02 DD
134025 F8 D2 DC
134025 21 02 DD
134025 CC D2 DC
134025 04 D2 DC
134025 FB D2 DC
134025 CC D2 DC
134025 FB 02 DD
134026 03 02 DD
134026 21 02 DD
That's 10 dumps for every microsecond delay. The total work time for dumping all 8192 bytes of a flash drive is about 48 hours.
7.3. Flash binary reconstruction
I have not yet finished writing the code that completely reconstructs the program code of the flash drive, taking into account all the deviations in time. However, I have already restored the beginning of this code. To make sure I did it correctly, I disassembled it using m8cdis:
0000: 80 67 jmp 0068h ; Reset vector
[...]
0068: 71 10 or F,010h
006a: 62 e3 87 mov reg[VLT_CR],087h
006d: 70 ef and F,0efh
006f: 41 fe fb and reg[CPU_SCR1],0fbh
0072: 50 80 mov A,080h
0074: 4e swap A,SP
0075: 55 fa 01 mov [0fah],001h
0078: 4f mov X,SP
0079: 5b mov A,X
007a: 01 03 add A,003h
007c: 53 f9 mov [0f9h],A
007e: 55 f8 3a mov [0f8h],03ah
0081: 50 06 mov A,006h
0083: 00 ssc
[...]
0122: 18 pop A
0123: 71 10 or F,010h
0125: 43 e3 10 or reg[VLT_CR],010h
0128: 70 00 and F,000h ; Paging mode changed from 3 to 0
012a: ef 62 jacc 008dh
012c: e0 00 jacc 012dh
012e: 71 10 or F,010h
0130: 62 e0 02 mov reg[OSC_CR0],002h
0133: 70 ef and F,0efh
0135: 62 e2 00 mov reg[INT_VC],000h
0138: 7c 19 30 lcall 1930h
013b: 8f ff jmp 013bh
013d: 50 08 mov A,008h
013f: 7f ret
Looks quite real!
7.4. Finding the storage address of the pincode
Now that we can read the checksum at the times we need, we can easily check how and where it changes when we:
enter the wrong pincode;
change pincode.
First, to find a rough storage address, I took a checksum dump in 10ms increments after a reboot. Then I entered the wrong pincode and did the same.
The result was not very pleasant, because there were many changes. But in the end I managed to establish that the checksum changed somewhere between 120000 Β΅s and 140000 Β΅s of delay. But the "pincode" I found there was completely wrong - due to an artifact of the delayMicroseconds procedure, which does strange things when 0 is passed to it.
Then, after spending almost 3 hours, I remembered that the SROM's CheckSum system call takes an argument as input that specifies the number of blocks for the checksum! That. we can easily localize the storage address of the pincode and the counter of "invalid attempts" - with an accuracy of 64-byte block.
My initial runs gave the following output:
Then I changed the pincode from "123456" to "1234567" and got:
Thus, the pincode and the counter of invalid attempts seem to be stored in block #126.
7.5. We remove the dump of block No. 126
Block #126 should be somewhere around 125x64x18 = 144000Β΅s, from the start of the checksum calculation, in my full dump, and it looks quite plausible. Then, after manually filtering out numerous bad dumps (due to the accumulation of "minor time deviations"), I ended up with these bytes (at a delay of 145527 Β΅s):
It is quite obvious that the pincode is stored unencrypted! These values, of course, are not written in ASCII codes, but as it turned out, they reflect the readings taken from the capacitive keyboard.
Finally, I ran some more tests to find out where the bad attempt counter is stored. Here is the result:
0xFF means "15 attempts" and it decreases for every wrong attempt.
Please note that the delay values ββI used are most likely relevant for one specific PSoC - the one that I used.
8. What's next?
So, let's sum up on the PSoC side, in the context of our Aigo drive:
we can read SRAM even if it is read-protected;
we can bypass the read protection by using a cold-reboot trace attack and reading the pincode directly.
However, our attack has some shortcomings due to synchronization issues. It could be improved as follows:
write a utility to correctly decode the output that is obtained as a result of a cold reload tracing attack;
use FPGA gadgets to create more accurate time delays (or use Arduino hardware timers);
try another attack: enter a knowingly wrong pincode, reboot and dump RAM, hoping that the correct pincode will be saved in RAM, for comparison. However, this is not so easy to do on the Arduino, since the Arduino signal level is 5 volts, while the board we are examining works with 3,3 volt signals.
One interesting thing to try is to play around with the voltage level to bypass the read protection. If this approach worked, we would be able to get absolutely accurate data from the flash drive - instead of relying on a checksum read with inaccurate time delays.
Since SROM probably reads the guard bits via the ReadBlock system call, we could do the same as described on Dmitry Nedospasov's blog - a re-implementation of Chris Gerlinski's attack, announced at the conference Recon Brussels 2017.
Another fun thing that could be done is to grind the case off the chip: to dump SRAM, identify undocumented system calls and vulnerabilities.
9. ΠΠ°ΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅
So, the protection of this drive leaves much to be desired, because it uses a regular (not βhardenedβ) microcontroller to store the pincode ... Plus, I have not yet looked (yet) how things are with data encryption on this device!
What can be advised for Aigo? After analyzing a couple of models of encrypted HDD drives, in 2015 I made presentation on SyScan, which reviewed the security issues of several external HDDs and made recommendations on how they could be improved. π
I spent two weekends and several evenings on this study. A total of about 40 hours. Counting from the very beginning (when I opened the disc) to the end (pincode dump). That same 40 hours includes the time I spent writing this article. It was a very exciting journey.