Do you like my hacking? If so, please consider leaving something in the
Fediverse (Mastodon etc): @Sprite_tm@social. spritesmods.com
Ofcourse, if I wanted to change something in the cache, I couldn't scan the complete 64MB of RAM every time: I needed to know how the cache works. For that, I would need to dump, disassemble and understand the hard disk firmware at least enough to make sense of the caching functions.
Disassembling this firmware is not a trivial task. First of all, the code mixes ARM and thumb-style instructions, which is irritating if you don't have a disassembler which can automatically switch between the two. Furthermore, something that usually makes disassembling software a lot easier is absent: Usually, routines are coded to spit out messages like "Couldn't open logfile! when something goes wrong. These messages are a huge help in figuring out what a routine does. This firmware, however, has none of these strings: you need to figure out what a routine does purely by the code. The codebase seems to be a bit old, though, and sometimes the disassembly feels like some features have been 'bolted on' to the code later, making everything a bit more complicated.
There also are a few things that make life easier, though. First of all, it seems Western Digital hasn't been intentionally obfuscating the code: no tricks like jumping in the middle of an instruction have been used. Also, because the JTAG-interface is available, you can meddle with the code, set breakpoints or change it on-the-fly, making figuring out what routine gets run when immensely easier.
After a long time of staring at the code, trying to make sense of things
and sometimes jumping into the debugger to see if a guess was correct,
I managed to get to the core of the caching system: a table in RAM
I call the 'cache descriptor table':
Every entry in the cache descriptor table describes a block in the cache. It contains the start LBA of the disk sectors that are or should be cached, how much of the cache actually is filled with disk data, some flags indicating the state of the cache entry and a number indicating where in memory the cached data resides.
Now, with the secrets of the cache descriptor table unraveled, could I intercept a disk read before it'd go out the SATA-port to the PC? To do that, I'd need to be able to execute my own code on the hard disk controller. Moreover, I would have to make sure the code would get run on the correct time: if it modified the cache too soon, the data wouldn't be in there yet; if it modified the cache too late, the data would've already gone to the PC.
The way I did this was by hooking an existing routine. My hack would be in Feroceon 2, and that CPU did all the SATA transfers, so there must be some routine that's responsible for setting up the SATA hardware to pick up the data from cache. If I could find this routine, I could perhaps run my own code before it.
After a lot of browsing, setting breakpoints, failing and trying again, I finally found some routine that fit the bill. I modified it to run my code before it by hooking it. Here's the original code:
000167BE ; r0 - slot in sata_req 000167BE sub_0_167BE: 000167BE PUSH {R4-R7,LR} 000167C0 MOVS R7, R0 000167C2 LSLS R1, R0, #4 000167C4 LDR R0, =sata_req 000167C6 SUB SP, SP, #0x14 000167C8 ADDS R6, R1, R0 000167CA LDRB R1, [R6,#0xD] 000167CC LDR R2, =stru_0_40028DC 000167CE STR R1, [SP,#0x28+var_1C] 000167D0 LDRB R0, [R6,#(off_0_FFE3F108+2 - 0xFFE3F0FC)] 000167D2 LDRB R5, [R6,#(off_0_FFE3F108 - 0xFFE3F0FC)] 000167D4 LSLS R0, R0, #4And here's what happens when the code is hooked to call my code:
000167BE ; r0 - slot in sata_req 000167BE sub_0_167BE: 000167BE PUSH {R4-R7,LR} 000167C0 MOVS R7, R0 000167C2 LD R6, =hookedAddr 000167C4 BX R6 000167C6 .dw checksumFix 000167C8 .dd hookedAddr 000167CC LDR R2, =stru_0_40028DC 000167CE STR R1, [SP,#0x28+var_1C] 000167D0 LDRB R0, [R6,#(off_0_FFE3F108+2 - 0xFFE3F0FC)] 000167D2 LDRB R5, [R6,#(off_0_FFE3F108 - 0xFFE3F0FC)] 000167D4 LSLS R0, R0, #4 ... FFE3F000 PUSH {R0-R12, LR} FFE3F004 BX changeThingsInCache FFE3F008 POP {R0-R12, LR} FFE3F00C LSLS R1, R0, #4 FFE3F010 LDR R0, =sata_req FFE3F014 SUB SP, SP, #0x14 FFE3F018 ADDS R6, R1, R0 FFE3F01C LDRB R1, [R6,#0xD] FFE3F020 BX 0x167CC
As you can see, some original instructions are replaced with a jump to new code in an otherwise unused bit of ram at address 0xFFE3F000 and an extra word to make sure the checksum of the code region still is valid. If this isn't done, the HD will try to load a backup from its platters, which isn't what we want. The code that's jumped to executes a routine called changeThingsInCache and then does what the replaced code would've done. It then continues execution in the original routine like nothing has happened.
Now all I need to write was a routine to modify the cached data. For a first test, I decided on a routine that in pseudocode went something like this:
void hook() { foreach (cache_struct in cache_struct_table) { if (is_valid(cache_struct)) { foreach (sector in cache_struct.sectors) { sector[0]=0x12345678; } } } }
This little bit of code would replace the first 4 bytes of every
sector in cache with 0x12345678 every time it's called, so if I uploaded
all this to the hard disk, I should see that number on the start of
every sector I read. I uploaded the bits of code over JTAG...