逆向工程破解Pokémon GO Plus的OTA签名验证

本文详细分析了Pokémon GO Plus设备的OTA更新机制,发现并利用了一个关键漏洞绕过固件签名验证,成功提取设备密钥。文章深入探讨了SPI闪存操作、SHA256哈希验证和内存地址操纵等技术细节。

逆向工程Pokémon GO Plus第二部分:OTA签名绕过

自从我发布Pokemon Go Plus的发现已经过去近6个月,到目前为止还没有人发布他们的Pokemon Go Plus密钥。其中一个原因是从OTP(一次性可编程)内存中提取密钥需要精密焊接技术。在我写文章几周后,我在/r/pokemongodev上提出了一个想法,通过空中(OTA)更新来提取Pokemon Go Plus密钥。

这个想法基于两个事实: 我们可以使用SPI编程器刷写任何镜像,而且没有签名检查,只需要正确的校验和。SPI闪存包含相同固件的两个副本(有2个固件库)。这对OTA很重要:如果固件传输不正确,引导加载程序(位于OTP中)将引导另一个有效的固件

计划如下: 创建我们的自定义固件通过空中将新固件刷写到Pokemon Go Plus的Bank 1。此时将有两个固件:我们的固件和pokemon go plus固件通过BLE(蓝牙低功耗)使用新固件提取密钥通过向新固件发送特殊请求恢复原始固件。这是通过读取bank 2上的原始固件并覆盖我们在Bank 1中的固件来实现的

计划实施

不幸的是,我没有时间实现它。我没有DA14580开发板,所以无法通过JTAG正确调试。我不想为一个只用于这个项目的板子花40美元。我收到了大约30美元的捐款,我用它买了另一个Pokemon Go Plus克隆版,它有相同的蓝牙MAC地址。

两个月前,Reddit用户jesus-bamford联系我,提到他将实现我提出的想法。一切似乎都按计划进行: 他可以创建从OTP提取密钥的固件他可以使用SPI编程器编写他的固件他可以通过空中发送他的固件(使用Dialog Semiconductor提供的Android应用程序)

但好消息到此为止:如果固件是通过OTA编写的,它将无法启动。引导加载程序认为固件无效,它将启动Pokemon Go Plus固件的原始副本。他发现软件更新中添加了一个额外的检查,这在DA14580 SDK提供的源代码中不存在。但他无法弄清楚检查是什么或如何绕过它。

在泰国泼水节期间,我有一些空闲时间,所以我尝试逆向工程引导加载程序。他是对的,确实添加了一个额外的检查: 当更新过程开始时,设置一个标志表明此固件镜像尚未有效。如果更新失败,引导加载程序可以引导另一个有效的固件。在此过程中,初始化SHA256哈希对于写入SPI闪存的每个传入数据,哈希都会更新在更新结束时,基于SHA256和OTP中的一些数据执行签名检查。如果一切有效,则设置固件镜像为有效

我没有详细研究签名检查算法,我知道它使用大数计算,可能是RSA,但我没有验证。我也不觉得我会在那里找到bug。重要的是接下来发生的事情:如果签名有效,则设置一个标志表明镜像是有效的。

所以要明确: 如果我们使用SPI编程器修改固件,我们只是覆盖现有固件,有效标志保持不变如果我们使用OTA修改固件,我们需要在更新过程结束时将其设置为有效

我确定一件事:更新过程还需要OTP区域中的特定密钥。所以如果Niantic要进行更新,该更新需要连接到他们的服务器以获取特殊密钥。

寻找漏洞

让我们回头看看是否有其他方法将镜像设置为有效。这是DA14580 SDK中app_spotar_img_hdlr.c的原始源代码:

编译后的二进制文件有一个额外的调用来更新SHA256,但现在这不重要。让我们只关注这一行:

1
ret = spi_flash_write_data(spota_all_pd, (spota_state.mem_base_add + spota_state.suota_img_idx), spota_state.suota_block_idx);

变量spota_all_pd包含从更新程序应用程序发送的数据。第二个参数指定写入数据的位置(SPI块中的地址),第三个参数是大小。

从视觉上,我们可以看到mem_base_add指向SPI闪存中的起始地址,suota_img_idx指向当前块。

当写入成功时,我们通过以下方式递增地址:

1
spota_state.suota_img_idx += spota_state.suota_block_idx;

这看起来没问题。我寻找可能的缓冲区溢出以进行代码执行,但找不到。但如果我们能修改spota_state.mem_base_add,那么我们将能够在SPI闪存中的任何位置写入,包括镜像有效标志。

我找到了一个名为app_spotar_read_mem的函数,它应该在我们完成写入所有数据后被调用。这也是在执行最终检查之前写入的地方,

 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
void app_spotar_read_mem(uint32_t mem_dev, uint32_t* mem_info)
{  
    *mem_info = 0;
    uint8_t mem_dev_cmd = (uint8_t) ((mem_dev & SPOTAR_READ_MEM_DEV_TYPE) >> 24);
    
    // 如果有效内存设备,存储内存设备类型和基地址
    if(mem_dev_cmd < SPOTAR_MEM_INVAL_DEV)
    {
        spota_state.mem_dev = (uint8_t) ((mem_dev & SPOTAR_READ_MEM_DEV_TYPE) >> 24);
        spota_state.mem_base_add = mem_dev & SPOTAR_READ_MEM_BASE_ADD;
        
        // ... 这里删除了一些不重要的代码 ...
        
        // 存储请求的镜像库
        if((spota_state.mem_dev == SPOTAR_IMG_I2C_EEPROM) ||
           (spota_state.mem_dev == SPOTAR_IMG_SPI_FLASH))
        {
            if(spota_state.mem_base_add <= 2)
            {
                spota_state.suota_image_bank = spota_state.mem_base_add;
            }
            else
            {
                // 无效的镜像库
                spotar_send_status_update_req((uint8_t) SPOTAR_INVAL_IMG_BANK);
                return;
            }
        }
    }  
    // 更多代码在这里
}

这段代码很奇怪:它使用spota_state.mem_base_add来存储一个临时值,用于设置spota_state.suota_image_bank,而不是使用临时变量。当spota_state.mem_base_add大于2时,此函数将失败。

而这正是我们需要的:一种修改mem_base_add的方法。所以我们需要做的是: 照常通过OTA发送数据,这将按预期写入SPI在发送最后一部分数据之前,发送一个请求调用app_spotar_read_mem,并将mem_base_add设置为我们固件头的开头,即有效标志所在的位置发送最后一部分数据,这将覆盖固件头。这最后的数据是我们想要的头部

上移mem_base_add

这就是想法。Jesus-bamford在实现这个想法方面做得很好,我很高兴它有效。这是他的软件运行情况。固件已经在以下位置开源: https://github.com/Jesus805/PGP_Suota

但更新程序尚未准备好发布。他目前正在为Android重新实现OTA软件,因为他当前的代码基于SDK代码。当他完成后,任何人都应该能够提取他们的Pokemon Go Plus密钥而无需打开它。

我知道我应该等到他的工作完成后再发布这个,但我也希望其他人能帮助他。可能为不同的iOS实现更新,或开始为其他设备实现Pokemon Go Plus。

也可能使用EdXposed(这是适用于Pokemon Go的XPosed分支)实现Pokemon Go Plus,或通过向iOS版本添加拦截BLE API调用的库来实现。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计