Hacking the Kyocera KY-42C

Introduction

In this post, I will share my experience unlocking the bootloader and modding the Kyocera KY-42C (DIGNO Keitai 4), a Japanese feature phone. This device has been known by the community for being considered unrootable after a firmware update, but as always, that won’t stop someone from trying :).

Kyocera A202KC, similar device to KY-42C A Kyocera A202KC, almost same device as KY-42C, picture from garahowiki

After my blog post about Carbonara, a MediaTek Download Agent exploit, a user known as @760ceb3b9c0ba4872cadf3ce35a7a494 left a comment asking for ideas on whether it was possible to unlock the bootloader of the KY-42C after the firmware update that supposedly made it unrootable. I was interested in this challenge, so I decided it was worth investigating.

Background

The KY-42C runs on a MediaTek MT6761 SoC, known by the market name of ā€œHelio A22ā€. Being a MediaTek device (an old one too), some ideas already came to my mind. I had previously worked on unlocking MediaTek devices, so I was familiar with how to approach this.

Before diving into the specific details of the device, it’s worth mentioning some specific concepts about MediaTek devices.

MediaTek devices include a USB rescue mode for flashing firmware, found in Preloader and BROM.

Brom is the masked ROM in the chip, which is the first code that runs when the device is powered on. It is responsible for initializing the basic hardware and loading the Preloader, a second stage bootloader whose job is to initialize the rest of the hardware (like DRAM) and load the main bootloader (LK), which then loads the Android OS.

MediaTek Boot Process

Both BootROM and Preloader allow loading a Download Agent (DA), a binary that allow flashing firmware through specialised tools, such as SP Flash Tool, Penumbra or mtkclient. For knowing more about Download Agents, I suggest reading R0rt1z2 blog post about heapb8 and penumbra documentation.

Often OEMs don’t provide the DA needed for their devices, and if they do, they are usually locked to only allow flashing signed firmware. Furthermore, because of BROM exploits such as kamakiri or linecode, OEMs have started to block BROM USBDL altogether, and only allow flashing through the Preloader. This is the case of the KY-42C, which has a locked BROM and no available DA for flashing firmware.

Investigation

In the case of the KY-42C, what we care is that the device, after the 1.090XX firmware update, locked BootROM USBDL. This means that we cannot use any BROM exploits to boot an unsigned Download Agent, and we are limited to the Preloader download mode.

However, I had a feeling that BROM USBDL wasn’t really permanently locked, and that it was possible to access it temporarily.

MediaTek implemented two ways to block this mode: one is to disable it via an efuse, a permanent solution that cannot be reverted, and the other is to set a flag in the Preloader GFH header.

During boot up, the BootROM determines whether USBDL is allowed by checking a few flags:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
bool check_brom_cmd_disabled(void)

{
  int iVar1;
  GFH_BROM_SEC_CFG_v1 *brom_sec_cfg;
  GFH_BROM_CFG_v3 *brom_cfg;
  ushort code2;
  ushort uVar2;
  bool locked;
  
  code2 = 0;
  uVar2 = 0;
  iVar1 = efuse_is_brom_cmd_disabled();
  locked = iVar1 != 0;
  if (locked) {
    code2 = 0x8000;
  }

  /* Preloader header is made of various structs, all under the common
   * name of GFH (General File Header).
   * Each GFH struct has a purpose, and the ones we care about are GFH_BROM_SEC_CFG and GFH_BROM_CFG.
   */
  brom_sec_cfg = get_bl_gfh(GFH_BROM_SEC_CFG);
  if ((brom_sec_cfg != NULL) && (brom_sec_cfg->m_cmd_mode_permanent_dis == 0xc975e033)) {
    code2 = code2 | 0x4000;
    locked = true;
  }

  /*
   * GFH_BROM_CFG allows to disable specific communication modes, in this case UART1 and USB.
   * This means that even if BROM USBDL is enabled, it can be allowed only through means other than USB.
   */
  brom_cfg = get_bl_gfh(GFH_BROM_CFG);
  if ((brom_cfg != NULL) && (brom_cfg->m_brom_cmd_via_uart1_disable_magic == 'R')) {
    code2 = code2 | 1;
  }
  if ((brom_cfg != NULL) && (brom_cfg->m_brom_cmd_via_usb_disable_magic == 'U')) {
    code2 = code2 | 2;
  }

  ...

  iVar1 = efuse_is_brom_cmd_disabled();
  if (iVar1 != 0) {
    code2 = code2 | 0x8000;
    locked = true;
  }
  cmd_dis_bitfield = code2 | uVar2 | cmd_dis_bitfield;
  if ((cmd_dis_bitfield & 0x3fff) == 3) {
    cmd_dis_bitfield = cmd_dis_bitfield & 0xfffe;
  }

  ...
  
  return locked;
}

And later on the BootROM checks whether USBDL is allowed, and what communication mode is allowed.

My feeling was that the efuse itself was not blown, and instead download mode has been disabled through Preloader flags. To confirm my theory, I asked 760ceb3b9c0ba4872cadf3ce35a7a494 to run a little script to dump fuses from Preloader USBDL (yes, MediaTek allows this).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# The script is based on moto-experiments:
# https://github.com/R0rt1z2/moto-experiments
    ...
    
    device = Device(args.port)

    logging.info('Waiting for device...')

    device.find_device()

    if not args.skip_handshake:
        device.handshake()

    device.identify()
    hrid = device.read32(0x11c50000, 0x1000)
    with open('efuse.bin', 'wb') as f:
        f.write(hrid)

Thanks to MTK whitelisting devinfo base in read32 cmd, we were able to dump the whole efuse region, especially the sec-ctrl fuse, at offset 0x60.

sec-ctrl fuse

By looking at the BootROM code, we can easily determine which bit is the one that disables BROM USBDL.

1
2
3
uint efuse_is_brom_cmd_disabled(void) {
  return (uint)(_DAT_11c50060) >> 8 & 1;
}

So, the bit in question is bit 8, which in our case evaluates to 0!

1
2
>>> 0x46 >> 8 & 1
0

So, the efuse is not blown, meaning that we could in theory just erase Preloader or short the test point to be able to load an unsigned DA, thanks to linecode exploit.

Unfortunately, after many attempts to locate a test point, and without a way to erase Preloader because of the device locked down fastboot, only one option was left: to hopefully find a way to unlock the bootloader through software, preferrably through a Preloader or lk exploit.

Firmware Analysis

I initially started by analyzing lk, which 760ceb3b9c0ba4872cadf3ce35a7a494 had lying around from an old backup.

With the experience I got from analyzing lk images from occasionally contributing to kaeru, I was able to quickly find the fastboot_init, which is where all fastboot commands are registered. I was looking for some kind of unlock command, but it looks like Kyocera decided to remove it completely:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
...
fastboot_register("flash:",0x4802b461,1,0);
iVar1 = codecheck();
if (iVar1 == 0) {
  fastboot_register("erase:",0x4802b1f1,1,1);
}
else {
  fastboot_register("erase:",0x4802b1f1,1,0);
}
fastboot_register("oem printk-ratelimit",0x48025175,1,0);
iVar1 = codecheck();
if (iVar1 == 0) {
  fastboot_register("continue",&LAB_4802508c+1,0,0);
}
else {
  fastboot_register("continue",&LAB_4802508c+1,1,0);
}
fastboot_register("download:",0x4802b899,1,0);
fastboot_publish("max-download-size",0x480baa9c);
FUN_48000188();
iVar1 = codecheck();
if (iVar1 == 0) {
  fastboot_register("oem p2u",0x4802522d,1,1);
}
else {
  fastboot_register("oem p2u",0x4802522d,1,0);
}
fastboot_register("oem dump_pllk_log",0x480250fd,1,0);
fastboot_register("oem off-mode-charge",&LAB_48025388+1,0,0);
iVar1 = codecheck();
if (iVar1 == 0) {
  fastboot_register("oem key",0x4802b905,1,1);
}
else {
  fastboot_register("oem key",0x4802b905,1,0);
}
iVar1 = codecheck();
if (iVar1 == 0) {
  fastboot_register("oem lks",0x4802b961,1,1);
}
else {
  fastboot_register("oem lks",0x4802b961,1,0);
}
...

fastboot_register("oem ultraflash:",0x4802b4cd,1,1);
fastboot_register("oem ultraflash_en",0x4802b5b9,1,1);
fastboot_register("ultraflash:",0x4802b4cd,1,1);

and unfortunately, most of the oem commands are gated behind a flag in the chkcode partition, making them inaccessible on secure devices.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int codecheck(void) {
  uint uVar1;
  int local_810;
  int local_80c;
  
  if (ALLOWED? < 0) {
    uVar1 = partition_read("chkcode",0x800,0,0,&local_810,0x800);
    if (((uVar1 == 0) || (local_810 != 0x544f4f4c)) || (local_80c != 0x4b434642)) {
      ALLOWED? = 0;
      return 0;
    }
    ALLOWED? = 1;
  }
  return ALLOWED?;
}

This, combined with a strict sec policy, narrowed down the possibilities of an unlock exploit in lk (or at least, that I was willing to invest time into).

So, it was time to move on to the Preloader.

Getting a preloader image was quite a challenge, because the dump 760ceb3b9c0ba4872cadf3ce35a7a494 had didn’t include it. I waited for them to get me the required files to continue. They were able to get both an old and new image from an OTA dump, which was perfect for diffing.

Having both images, I was able to, first of all, confirm my initial theory that the Preloader was indeed responsible for disabling BROM USBDL.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Old preloader

  BROM_SEC_CFG (v1, 48 bytes)
    JTAG: disabled
    Debug: disabled
    BROM Cmd Perm Dis: no

# New preloader

  BROM_SEC_CFG (v1, 48 bytes)
    JTAG: disabled
    Debug: disabled
    BROM Cmd Perm Dis: yes

Then, I started loading the two images into Ghidra, and started looking for some exploitable paths.

Exploiting Preloader

During USBDL in Preloader, the device will enter an infinite loop, waiting for a command from the host. The command is sent through USB, where it will be matched in a switch statement and execute the corresponding function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// From `download.c`, found on GitHub
int usbdl_handler(struct bldr_comport *comport, u32 hshk_tmo_ms)
{
	u8 cmd = 0;

  ...

#if CFG_PRELOADER_AS_DA
	usbdl_init_image();
#endif

	while (1) {
		platform_wdt_kick();

		usbdl_get_byte(&cmd);
		if (cmd != CMD_GET_BL_VER)
			usbdl_put_byte(cmd);

		switch (cmd) {
		  ...
			case CMD_SEND_DA:
				DBGMSG("%s CMD_SEND_DA\n", MOD);
				usbdl_send_da();
				break;
			case CMD_JUMP_DA:
				DBGMSG("%s CMD_JUMP_DA\n", MOD);
				usbdl_jump_da();
				break;
#if CFG_PRELOADER_AS_DA
			case CMD_SEND_IMAGE:
				DBGMSG("%s CMD_SEND_IMAGE\n", MOD);
				usbdl_send_image();
				break;
			case CMD_BOOT_IMAGE:
				DBGMSG("%s CMD_BOOT_IMAGE\n", MOD);
				usbdl_boot_image();
				break;
#endif
			case CMD_READ32:
				DBGMSG("%s CMD_READ32\n", MOD);
				usbdl_read32(FALSE);
				break;
			case CMD_WRITE32:
				DBGMSG("%s CMD_WRITE32\n", MOD);
				usbdl_write32(TRUE);
				break;
			...
			default:
				DBGMSG("%s Unhandled CMD 0x%x\n", MOD, cmd);
				break;
		}
	}
	return 0;
}

While looking at the Preloader code and the one I have loaded in Ghidra, I got caught by the ā€œCMD_SEND_IMAGE\nā€ debug message. This command is only available if CFG_PRELOADER_AS_DA is defined, which seems to be the case for KY-42C.

In my experience, this command and CMD_BOOT_IMAGE are not present on production preloaders. So, I thought, if MTK doesn’t enable these commands by default, and decided to also remove them all together, perhaps they had some reason to.

Reading the code, I realized that, as I suspected, MediaTek forgot to implement a check for signature verification in the cmds:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static void usbdl_send_image(void) {
	u32 img_addr = 0;
	u32 img_len = 0;
	image_index_t id;
	u16 status = 0;
	u8 img_name[64] = {0};
	u32 checksum32 = 0;
	u32 my_checksum32 = 0;
	u8 debug_buf[32] = {0};

	usbdl_get_data(img_name, 64);
	usbdl_get_dword(&img_len);

	for (id = IMAGE_LK_ID; id < IMAGE_MAX_NUM; id++) {
		if ((!strcmp(img_name, g_image_list[id].partition_name)) && (img_len > 0) && (img_len <= g_image_list[id].length)) {
			pal_log_info("%s SEND: Verify PASS\n", MOD);
			if(id == IMAGE_ATF_ID)
				img_addr = (u32)tee1_buf;
			else
			img_addr = g_image_list[id].start_addr;
			break;
		}
	}
	if (!img_addr) {
		pal_log_err("%s Unknown image\n", MOD);
	}

	usbdl_put_word(status);

  /* Here the device downloads the image data from the host, and
  * only performs a checksum verification, without ever verifying
  * the signature.
  */
	usbdl_get_data((u8 *)img_addr, img_len);
	my_checksum32 = checksum_plain((u8 *)img_addr, img_len);
	usbdl_get_dword(&checksum32);

	if (my_checksum32 != checksum32) {
		pal_log_err("%s checksum mismatch!\n", MOD);
		return;
	}


  if(id == IMAGE_ATF_ID) {
      // Relocate ATF and TEE
  }
}

static void usbdl_boot_image(void) {

	u8 img_name[64] = {0};
	u32 jump_arg;
	u16 status = 0;

	usbdl_get_data(img_name, 64);

	trustzone_pre_init();
	g_boot_mode = FASTBOOT;
	platform_set_boot_args();
	trustzone_post_init();

	jump_arg = (u32)&bootarg;

	if (!strcmp(img_name, lk)) {
		usbdl_put_word(status);
		pal_log_err("%s Jump to LK\n", MOD);
		/* Hi MTK, thanks for this */
		bldr_jump(g_image_list[IMAGE_LK_ID].start_addr + PART_HDR_BUF_SIZE, jump_arg, sizeof(boot_arg_t));
	} else if (!strcmp(img_name, atf)) {
		usbdl_put_word(status);
		pal_log_err("%s Jump to ATF\n", MOD);
		bldr_jump64(g_image_list[IMAGE_LK_ID].start_addr + PART_HDR_BUF_SIZE, jump_arg, sizeof(boot_arg_t));
	} else {
		status = 1;
		usbdl_put_word(status);
		pal_log_err("%s Unknown Jump\n", MOD);
	}
}

This means that we can get EL3 Arbitrary Code Execution with just a few lines of python code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    path = 'bin/unlock.bin'
    with open(path, 'rb') as f:
        data = f.read()
        
    device = Device(None)
    
    logging.info('Waiting for device...')
    
    device.find_device()
    device.handshake()
    device.identify()
    
    device.send_image('lk', data)
    device.boot_image('lk')

Worth mentioning that this exploit has already been used publicly in the past in lgk10exploit, so it’s nothing new, and has been patched for a few years by now. That said, it was still nice to figure this out by myself before discovering this project.

Unlocking the bootloader

Initially, we tried to boot a patched lk image, but unfortunately the device would crash after a few seconds, and we couldn’t figure out why. Because of this, and because this lk didn’t include the sec_set_lock_state function at all, I decided to make my own payload, which uses the framework of my mtk-payloads project.

You can see the full code for the payload here, but the main idea is to create a new seccfg, encrypt its hash and write it to the seccfg partition, and while at it, also unlock fastboot capabilities in chkcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(void) {
    ...

    seccfg.lock_state = LKS_UNLOCK;
    seccfg.dm_verity = DM_VERITY_OK;

    sha256_hash(hash, &seccfg, 0x1C);

    params.length = HASH_SZ;
    params.anti_clone = true;
    params.encrypt = true;

    sp_sej_enc(hash, hash, params);

    memcpy(seccfg.hash, hash, HASH_SZ);

    mmc_write_block(&g_mmc_dev, seccfg_start, &seccfg);

    u32 chkcode_block = gpt_get_start(&gpt, "chkcode") + 4;

    mmc_read_block(&g_mmc_dev, chkcode_block, chkcode_buf);

    *(u32 *)&chkcode_buf[0] = CHKCODE_MAGIC1;
    *(u32 *)&chkcode_buf[4] = CHKCODE_MAGIC2;

    mmc_write_block(&g_mmc_dev, chkcode_block, chkcode_buf);
}

I sent the payload and python script to 760ceb3b9c0ba4872cadf3ce35a7a494, and they were able to successfully unlock the bootloader of their device!

Ā  Ā 
orange_state firmware_version

While at it, I also made a smaller payload to patch Preloader security, jump back to it, and allow loading unsigned DAs, so that we could get a full dump of the device (available here with only firmware partitions).

Modding attempt

A few days after unlocking the bootloader, a new cert exploit (similar to CVE-2023-20696) was made public, which would allow us to boot a patched lk image regardless of the Preloader sec policy. Thanks to lkpatcher, I was able to quickly patch the lk image, and send it to 760ceb3b9c0ba4872cadf3ce35a7a494 to test it on their device. Suprisingly, the device would not boot, and we were unable to grab any logs from the device.

expdb logs being all zeros

I suspect expdb logs were not being written during boot for some reason, and without UART access, we couldn’t figure out what was going on, so we gave up on this attempt for the time being.

Conclusions

Investigating this device was a fun and unusual experience from my usual MediaTek reverse engineering work, and was a good opportunity to test both my knowledge and payload framework.

Thanks to @760ceb3b9c0ba4872cadf3ce35a7a494 for taking the time to test all the payloads and scripts that I’ve sent them. Thanks also to @R0rt1z2 for some of the code snippets and scripts I used in my payload, as well for lkpatcher, which was a great help in the modding attempt.

All instructions for unlocking the bootloader are available in the GitHub repository!

I hope you enjoyed reading this post! See ya!