In case you missed previous episodes:

  • Part 1 where you can read about this DualShock 4 totally messed up that I’m trying to repair
  • Part 2 where I figure out which commands can be sent via USB and dump the flash
  • Part 3 where I play with test commands, DFU mode and list all supported GET commands

Welcome to Part 4! :smiling_imp:

As said at the end of Part 3, there is a leaked reverse-engineered code available online. Enough with blackbox reverse-engineering, we can start now a pseudo white-box analysis by reading the code.

The quality could be better, this file is probably the copy-paste of what you get when you open the binary in a decompiler. To those not used to decompilers: no, it cannot be recompiled, its not even close to a valid C file.

Anyway, a good text editor is enough to dig into that 45000-lines file and understand a bit about the behavior of the DS4.

Looking at the strings

When I start to look at a new binary, the first thing I do is looking for interesting strings, 10/10 low hanging fruits. :cherries:

In this binary there are lots of printf() and lots of strings, this makes me think that there should be a way to debug using a serial port.

Anyway, this one below seems the version of this firmware. Compiled on Aug 3 2013, way older than the one running in my controller.

char aCopyrightC2012SonyComput[64] = "Copyright(C) 2012 Sony Computer Entertainment Inc.              ";
version_info_t stru_8040 = {
  { 'A', 'u', 'g', ' ', ' ', '3', ' ', '2', '0', '1', '3', '\0', '\0', '\0', '\0', '\0' },
  "07:01:12", 256, 12544, 3, 73, 5, 229376
};

And few lines below, there is something that seems a command line interface:

char aTeppouMonitorVer1_0[70] = "\r\nTEPPOU Monitor Ver 1.00\r\n (C) 2013 Sony Computer Entertainment Inc.";
char aCopyrightC2013SonyComput[74] = "Copyright(C) 2013 Sony Computer Entertainment Inc. All rights reserved.\r\n";
struc_7 dbg_cmds[40] =
{
  { "INFO",                   0x9E63 },
  { "db",                     0xA255 },
  { "dh",                     0xA255 },
  { "dw",                     0xA255 },
  { "sb",                     0xA30B },
  { (const char *)0x31678,    0xA30B },
  { "sw",                     0xA30B },
  { "extI2c",                 0xA73F },
  { "sysI2c",                 0x16C35 },
  { "spiAcc",                 0xC7E5 },
  { "spiBmi055",              0xD0F1 },
  { "jediLed",                0x14C73 },
  { "mtr",                    0x151D7 },
  { "iEep",                   0x155CF },
  { "adc",                    0xF7D9 },
  { "fLock",                  0x15A91 },
  { "audio",                  0x10797 },
  { "btenable",               0x183B9 },
  { "btdisable",              0x183BF },
  { "btshowladdr",            0x183C5 },
  { "btwladdr",               0x183CB },
  { "btunpair",               0x183D1 },
  { "btshowreg",              0x183D7 },
  { "btshowdevinfo",          0x183DD },
  { "btpost",                 0x183E3 },
  { "btshowtask",             0x183E9 },
  { "btshowsema",             0x183EF },
  { "btshowstack",            0x183F5 },
  { "btsniff",                0x183FB },
  { "btexitsniff",            0x18401 },
  { "bat",                    0xE16D },
  { "tc7710",                 0xEFAF },
  { "factory",                0x165D1 },
  { "pwr",                    0x99B3 },
  { "flash",                  0x9E9D },
  { "sbcDec",                 0x117F1 },
  { "sbcEnc",                 0x118B1 },
  { "tp",                     0xC40D },
  { "auth",                   0x20DDD },
  { (const char *)0x316F7,    &dword_0 }
};

Just brainstorming: maybe also JDM-055 has test pins? My friend with the good electronic lab is on vacation, so I’ll go down this path in future. Let me know if you ever heard of this interface on a DS4! :cowboy_hat_face:

Anyway, let’s not get distracted and go back to repairing the DS4.

Oh wait.

void dbg_wait_password()
{
  int v0; // r3
  char v5; // [sp+4h] [bp-2Ch]
  memcpy32(&v5, (int *)"~~ REDACTED, A RANDOM PASSWORD HERE ~~", 0x18u, v0);
  /* ... */
}

void debug_task()
{
  signed int v0; // r0
  dbg_wait_password();
  dbg_print_welcome();
  while ( 1 )
  {
    do
    {
      printf("\r\n>:");
      dbg_get_args(dbg_cmdline, &dbg_argc, (char **)dbg_argv);
    }
    while ( dbg_argc <= 0 );
    v0 = dbg_get_cmd_idx((const char *)dbg_argv[0]);
    if ( v0 < 0 )
      printf("\r\nIllegal Command.\r\n");
    else
      dbg_cmds[v0].func(dbg_argc, (char **)dbg_argv);
  }
}

LOL

We can spend days just by looking at all the functionalities in this binary, but let’s go back to the main task: make my DS4 work again. :nerd_face:

IMU Calibration

This C file has just few functions with a meaningful name, all the others are named sub_xxxx() where xxxx is the address of the function.

Speaking of USB HID reports, void usb_hid_get_report(int a1); and void usb_hid_set_report(signed int a1, unsigned int a2, int a3); immediately caught my attention.

Both functions have a huge switch-case inside, where part of it is compiled as a binary search using if/else, this is why you will see below sometimes case 2 and sometimes else if (a1 == 123)

We should start from somewhere, so.. let’s have a look about the implementation of GET 0x02 (Get Calibration Data):

void usb_hid_get_report(int a1) {
  /* ... */
  case 2:
    usb0_ep0_send_buf[0] = 2;
    *(_WORD *)&usb0_ep0_send_buf[1]  = *(_WORD *)&flash_4000_mirror[514];
    *(_WORD *)&usb0_ep0_send_buf[3]  = *(_WORD *)&flash_4000_mirror[516];
    *(_WORD *)&usb0_ep0_send_buf[5]  = *(_WORD *)&flash_4000_mirror[518];
    *(_WORD *)&usb0_ep0_send_buf[7]  = *(_WORD *)&flash_4000_mirror[520];
    /* .... omitted for clarity .... */
    *(_WORD *)&usb0_ep0_send_buf[29] = *(_WORD *)&flash_4000_mirror[544];
    *(_WORD *)&usb0_ep0_send_buf[31] = *(_WORD *)&flash_4000_mirror[540];
    *(_WORD *)&usb0_ep0_send_buf[33] = *(_WORD *)&flash_4000_mirror[546];
    *(_WORD *)&usb0_ep0_send_buf[35] = *(_WORD *)&flash_4000_mirror[548];
    usb0_ep0_send_buf_len = 37;
    usb0_ep0_send_buf_ptr = (int)usb0_ep0_send_buf;
    break;
  /* ... */
}

Well, seems pretty easy: it sets the first output byte to 0x02, so that the output message becomes a response to GET 0x02 and then copies some values from the memory to the USB output buffer.

Now, I don’t know what flash_4000_mirror means, it was named in this way by the anonymous reverser that published that file. But, given that name, I can think of these things:

  • There is a flash memory in the DS4
  • It is mapped to 0x4000
  • It’s mirrored in RAM, like that at some point you call flush() and in that moment is written permanently
  • The IMU calibration data in memory start at position 514 (which in HEX is 0x202)

Let’s see where are the calibration bytes in the memory dump obtained in Part 2:

$ hexdump -Cv dump_official.bin  
00000000  91 4a 00 00 00 00 4e 81  8e 75 a9 2a 01 00 00 b4  |.J....N..u.*....|
[...]
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000200  00 00 ff ff 00 00 05 00  90 22 81 22 98 22 6b dd  |........."."."k.|
00000210  85 dd 77 dd 1c 02 1c 02  19 1e 3e 20 3c 21 c3 de  |..w.......> <!..|
00000220  b8 df 47 e1 05 00 00 00  00 00 00 00 00 00 00 00  |..G.............|
00000230  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000240  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000250  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000260  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
[...]

They start exactly at 0x202. :smile:

Perfect, so flash_4000_mirror is what has been zeroed out in my broken controller.

Ok, can we change the calibration in some way? Let’s see where else is used flash_4000_mirror[514].

void usb_hid_set_report(signed int a1, unsigned int a2, int a3) {
  /* ... */
  case 4:
    *(_WORD *)&flash_4000_mirror[514] = *(_WORD *)((char *)&dword_0 + a3 + 1);
    *(_WORD *)&flash_4000_mirror[516] = *(_WORD *)((char *)&dword_0 + a3 + 3);
    *(_WORD *)&flash_4000_mirror[518] = *(_WORD *)((char *)&off_4 + a3 + 1);
    *(_WORD *)&flash_4000_mirror[520] = *(_WORD *)((char *)&off_4 + a3 + 3);
    /* Omitted for clarity */
    *(_WORD *)&flash_4000_mirror[544] = *(_WORD *)((char *)&off_1C + a3 + 1);
    *(_WORD *)&flash_4000_mirror[540] = *(_WORD *)((char *)&off_1C + a3 + 3);
    *(_WORD *)&flash_4000_mirror[546] = *(_WORD *)((char *)&off_20 + a3 + 1);
    *(_WORD *)&flash_4000_mirror[548] = *(_WORD *)((char *)&off_20 + a3 + 3);
    flash_4000_mirror[512] |= 3u;
    flash_mirror_flush();
    break; 
  /* ... */
}

Oh nice! :grin: Way easier than I thought! So SET 0x04 changes the calibration data.

I have a working DS4 that can be used to obtain valid calibration data:

ffff0000050090226bdd812285dd982277dd1c021c02ab1f55e04320bddffb1f06e00500

It’s not perfect, I know, this is a calibration for another DS4, but it’s way better than all zeros.

Let’s try if SET 0x04 works, by adding this code in jedi_tool.py:

import binascii
# [...]

new_calib="ffff0000050090226bdd812285dd982277dd1c021c02ab1f55e04320bddffb1f06e00500"
           ffff0000050090226bdd812285dd982277dd1c021c02ab1f55e04320bddffb1f06e00500

# Print the calibration before
dump_req(0x02, 41) 

# Try to change IMU calibration
hid_set_report(dev, 0x04, binascii.unhexlify(new_calib))

# Print the new calibration
dump_req(0x02, 41) 

And cross the fingers! :crossed_fingers:

$ python3 jedi_tool.py
RID:0x02 data: 000000000000000000000000000000000000000000000000000000000000000000000000
RID:0x02 data: ffff0000050090226bdd812285dd982277dd1c021c02ab1f55e04320bddffb1f06e00500

Seems working, OMG! :astonished:

Test the fix

In Part 1 I described the bug found in the hid-sony driver. I added a workaround to my driver to prevent the crash. The workaround prints a message on dmesg if the calibration is zero and changes the denominator to 1.

When my broken DS4 is connected, this is what usually happens:

[...] usb 1-2: new full-speed USB device number 3 using xhci_hcd
[...] usb 1-2: New USB device found, idVendor=054c, idProduct=09cc, bcdDevice= 1.00
[...] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[...] usb 1-2: Product: Wireless Controller
[...] usb 1-2: Manufacturer: Sony Interactive Entertainment
[...] sony 0003:054C:09CC.0082: Invalid calibration data for axis (3), disabling calibration.
[...] sony 0003:054C:09CC.0082: Invalid calibration data for axis (4), disabling calibration.
[...] sony 0003:054C:09CC.0082: Invalid calibration data for axis (5), disabling calibration.
[...] sony 0003:054C:09CC.0082: Invalid calibration data for axis (0), disabling calibration.
[...] sony 0003:054C:09CC.0082: Invalid calibration data for axis (1), disabling calibration.
[...] sony 0003:054C:09CC.0082: Invalid calibration data for axis (2), disabling calibration.
[...] input: Sony Interactive Entertainment Wireless Controller Touchpad as [...]
[...] input: Sony Interactive Entertainment Wireless Controller Motion Sensors as [...]
[...] input: Sony Interactive Entertainment Wireless Controller as [...]

You can see all the lines with Invalid calibration data that are emitted because the IMU calibration is zeroed-out.

Well, after calling SET 0x04 and setting the calibration data, this is what happens when the DS4 is reconnected:

[...] usb 1-2: new full-speed USB device number 4 using xhci_hcd
[...] usb 1-2: New USB device found, idVendor=054c, idProduct=09cc, bcdDevice= 1.00
[...] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[...] usb 1-2: Product: Wireless Controller
[...] usb 1-2: Manufacturer: Sony Interactive Entertainment
[...] input: Sony Interactive Entertainment Wireless Controller Touchpad as [...]
[...] input: Sony Interactive Entertainment Wireless Controller Motion Sensors as [...]
[...] input: Sony Interactive Entertainment Wireless Controller as [...]

No more Invalid calibration data :smile: which means that the calibration is loaded correctly by the driver. :grin:

A call to GET 0x02 confirms that the calibration stays there even after a reboot:

$ python3 jedi_tool.py
RID:0x02 data: ffff0000050090226bdd812285dd982277dd1c021c02ab1f55e04320bddffb1f06e00500

Ohh yes. The Dualshock4 IMU is now calibrated. :man_dancing:

Party Hard

Side effect of SET 0x04

SET 0x04 does another thing though: it executes this instruction:

flash_4000_mirror[512] |= 3u;

which enables two flags to the word just before the calibration data.

I’m still not sure about the meaning, searching for mirror[512] does not produce interesting results, it seems not used anywhere except for the GET 0x10 which returns the values of flash[512,513] and flash[256,257].

void usb_hid_get_report(int a1) {
  /* ... */
  case 16:
    usb0_ep0_send_buf[0] = 16;
    *(_WORD *)&usb0_ep0_send_buf[1] = *(_WORD *)&flash_4000_mirror[512];
    *(_WORD *)&usb0_ep0_send_buf[3] = *(_WORD *)&flash_4000_mirror[256];
    usb0_ep0_send_buf_len = 5;
    usb0_ep0_send_buf_ptr = (int)usb0_ep0_send_buf;
  /* ... */
}

Maybe the controller keeps track of the recalibration happened using SET 0x04?

I bet the meaning in the original firmware is this: :joy:

  flash->flags |= FLAG_SOMEONE_MESSED_UP | FLAG_EXPIRE_WARRANTY;

Bluetooth MAC

Another issue about this DS4 is the Bluetooth MAC Address wiped out.

The output of GET 0x81 (Get BT Addr) is: 000000000000.

Again, let’s go back in usb_hid_get_report() and search for 129 (0x81):

void usb_hid_get_report(int a1) {
  /* ... */
  if ( v1 == 129 ) 
  {     
    sub_15C5A(usb0_ep0_send_buf);
    usb0_ep0_send_buf[0] = -127; 
    usb0_ep0_send_buf_len = 7;
    usb0_ep0_send_buf_ptr = (int)usb0_ep0_send_buf;
    goto LABEL_34;
  }     
  goto def_17406;
  /* ... */
}

Ok, it does not directly copy the MAC address, but passes the buffer to sub_15C5A().

Hello sub_15C5A(), nice to meet you:

_BYTE *sub_15C5A(_BYTE *result)
{
  result[1] = flash_4000_mirror[1818];
  result[2] = flash_4000_mirror[1819];
  result[3] = flash_4000_mirror[1820];
  result[4] = flash_4000_mirror[1821];
  result[5] = flash_4000_mirror[1822];
  result[6] = flash_4000_mirror[1823];
  return result;
}

So the MAC address is located at flash[1818]. As we did before, let’s see who else uses this and hopefully one result will be related to usb_hid_set_report(): :crossed_fingers:

void usb_hid_set_report(signed int a1, unsigned int a2, int a3) {
  /* ... */
  else if ( a1 == 128 )
  {
    flash_4000_mirror[1818] = *((_BYTE *)&dword_0 + a3 + 1);
    flash_4000_mirror[1819] = *((_BYTE *)&dword_0 + a3 + 2);
    flash_4000_mirror[1820] = *((_BYTE *)&dword_0 + a3 + 3);
    flash_4000_mirror[1821] = *((_BYTE *)&off_4 + a3);
    flash_4000_mirror[1822] = *((_BYTE *)&off_4 + a3 + 1);
    flash_4000_mirror[1823] = *((_BYTE *)&off_4 + a3 + 2);
    flash_4000_mirror[1824] = 0;
    flash_4000_mirror[1825] = 0;
    flash_mirror_flush();
  }
  /* ... */
}

Bingo! So SET 0x80 (128) updates the device MAC address. Let’s try to change it to one similar to the working DS4:

dump_req(0x81, 8)
dump_req(0x12, 15) 

print("Change MAC to 28:51:65:11:ae:a0")
hid_set_report(dev, 0x80, b'\x28\x51\x65\x11\xae\xa0')

dump_req(0x81, 8)
dump_req(0x12, 15)

Output:

RID:0x81 data: 000000000000
RID:0x12 data: 000000000000082500000000000000
Change MAC to 28:51:65:11:ae:a0
RID:0x81 data: 28516511aea0
RID:0x12 data: 28516511aea0082500000000000000

Yeess!! :tada: :tada: :tada: MAC Address changed!

Dance

Flash Diff

Now, let’s inspect the differences between the flash of the broken DS4 and the working one. Above the working DS4, below the broken one:

Diff

The flash memory of the broken one is getting better.

There is still a lot of work to do, for example:

  • Huge chunk of bytes that is all zero at lines D8, 10E and 144
  • Some bytes still at 0 also in the first line.
  • Line 1E6 has a 03 caused by the SET 0x04, but at the moment does not seem a problem

It is curious that the IMU calibration written in flash differ in some bytes from the other flash, even though appears correct if read using GET 0x02.

An unknown request

Another interesting request is SET 0x85, which takes 6 bytes and stores them starting at flash[6]. The same data can be read using GET 0x86. I don’t know what is the content. It is used only by SET 0xa0 11 2 where puts that somewhere else (maybe the USB output buffer?), haven’t had time to investigate yet.

void sub_15C08(int a1)
{
  unsigned int v1; // r1
  v1 = 0;
  do
  {
    flash_4000_mirror[v1 + 6] = *(_BYTE *)(a1 + v1 + 1);
    ++v1;
  }
  while ( v1 < 6 );
  flash_mirror_flush();
}

void usb_hid_set_report(signed int a1, unsigned int a2, int a3) {
  /* ... */
  case 133:
    sub_15C08(a3);
    break;
  /* ... */
}

Anyway, my working DS4 has this value 4e818e75a92a so I cloned it into the broken controller.

Diff of the flash contents before and after SET 0x85: Diff after 0x85

Checksum

At this point it’s quite clear that the first two bytes of the flash are a checksum of the contents, and the source confirms my hypothesis:

void flash_mirror_flush_internal()
{
  // ...
  *(_WORD *)flash_4000_mirror = flash_mirror_calc_checksum();
  // ...
}

Cool! And how is this checksum used?

void flash_mirror_load()
{
  /* ... */

  if ( flash_mirror_calc_checksum() == *(unsigned __int16 *)flash_4000_mirror )
  {
    LOBYTE(word_1FFE01E0) = 1; // Everything ok
  }
  else
  {
    LOBYTE(word_1FFE01E0) = 0;
    v3 = flash_4000_mirror;
    LOWORD(v4) = 0;
    do
    {
      *(_DWORD *)v3 = 0;
      v3 += 4;
      v4 = (unsigned __int16)(v4 + 1);
    }
    while ( v4 < 0x200 ); // The magic loop
  }
  /* ... */
}

Oh nice, if the checksum is not valid, there is a loop that clears out the flash, 4 bytes per iteration, from the beginning to 0x800.

I suspect (but haven’t tried yet) that if the battery of the controller dies while it’s writing on the flash, at the next startup the checksum does not match and the DS4 gets totally messed up like this one.

Flash Mirror

At this point the first line starts being more similar to the working one: Diff after 0x85 w. original

Look at the byte 12, there is a 0x01 in the original DS4 and a 0x00 in the messed-up one. What is that?

I found out that SET 0xA0 0x0A is the responsible for this, a strange command:

  • SET 0xA0 0x0A 1 writes 0x01 at flash[12]
  • SET 0xA0 0x0A 2 3e717f89 writes 0x00 at flash[12]

Why is so difficult to write 0 there? Is that a passcode or what?

The behavior was unknown to me, until I left the 0x01 there and the DS4 started acting strangely: at every reboot my changes were gone.

So, well, this is the real meaning of “flash mirror”. :smile: 0x01 enables the mirror in only one direction.

Conclusion

That’s it guys! At least for now :)

The controller is still not working :joy:, but now the broken DS4 has its own MAC Address and the IMU calibration back in place.

There are lots of other features that should be investigated, I’ll continue my research and write a new post in future with other updates.

If you have any knowledge about the DS4 that can help me, please let me know! :pray: :smile: :pray:

Thank you for reading! :heart:

EDIT: Part 5 here

~~~

If you want to get in touch, drop me an email at ds4@the.al or ping me on IRC (the_al@freenode|libera|hackint) or Discord (the_al).