<转>Se99er V9 分析

对逆向V9甚至V10真的很有帮助
原帖地址 [http://www.amobbs.com/forum.php?mod=viewthread&tid=5653964]



首先是个最简单的办法,不用拆机,没有风险,原理很简单,很多年前论坛上的大牛就发现并公布了这个办法,不过据说后来论坛浮云过一回,资料丢了。这个办法利用的是jlink自带的一个命令,这个命令能读取jlink自身的内存,我们只是需要用这个命令把bootloader部分的内容读取出来就可以了。


在进入这个具体命令之前,我们来看一下jlink的操纵方法,比较普遍的做法是调用jlinkarm.dll公开的接口,再有sdk的情况下,调用这些接口并不麻烦,但是如果没有sdk的话,c/c++语言要调用这些接口显得特别的麻烦,所以这里我们使用更为底层的办法越过jlink的dll,直接和jlink的驱动打交道。


首先,我们需要找到系统里面的jlink这个设备.

GUID classGuid = {0x54654E76, 0xdcf7, 0x4a7f, 0x87, 0x8A, 0x4E, 0x8F, 0x0CA, 0x0A, 0x0CC, 0x9A};
auto devInfoSet = SetupDiGetClassDevsW(&classGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

用上面的代码先找到jlink的class然后我们需要枚举这个devInfoSet里面的全部成员,依次取得各种信息,最终拿到jlink驱动提供的设备路径

SP_DEVICE_INTERFACE_DATA interfaceData = {0};
interfaceData.cbSize = sizeof(interfaceData);
for(DWORD i = 0; ; i ++)
{
    if(!SetupDiEnumDeviceInterfaces(devInfoSet, nullptr, &classGuid, i, &interfaceData))
        break;

    DWORD requiredSize = 0;
    SetupDiGetDeviceInterfaceDetailW(devInfoSet, &interfaceData, nullptr, 0, &requiredSize, nullptr);
    void* tempBuffer = new uint8_t[requiredSize];
    PSP_DEVICE_INTERFACE_DETAIL_DATA interfaceDetailData = static_cast(tempBuffer);
    interfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
    if(!SetupDiGetDeviceInterfaceDetailW(devInfoSet, &interfaceData, interfaceDetailData, requiredSize, &requiredSize, nullptr))
        continue;

到了这里interfaceDetailData->DevicePath这个里面就是jlink的设备路径,我们打开它

    HANDLE deviceFile = CreateFileW(interfaceDetailData->DevicePath, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if(deviceFile == INVALID_HANDLE_VALUE)
        continue;

这个打开的文件句柄主要是给jlink发送一些控制命令,真正读写的是需要打开另外两个句柄的,pipe00用来读,pipe01用来写,我们也打开它们

    wchar_t pipeFileName[1024] = {0};
    wcscpy_s(pipeFileName, interfaceDetailData->DevicePath);
    wcscat_s(pipeFileName, L"\\pipe00");
    HANDLE readPileFile = CreateFileW(pipeFileName, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if(readPileFile == INVALID_HANDLE_VALUE)
        continue;
    wcscpy_s(pipeFileName, interfaceDetailData->DevicePath);
    wcscat_s(pipeFileName, L"\\pipe01");
    HANDLE writePileFile = CreateFileW(pipeFileName, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if(writePileFile == INVALID_HANDLE_VALUE)
        continue;

到了这里我们拿到了三个句柄,接下来我们就可以用这三个句柄来操控jlink了


首先介绍一下jlink的各种协议,这个协议有一部分公开了在它官网上,还有一个部分没有公开,大家可以去官网看看那个公开的文档,这里简单介绍一下。jlink的协议比较简单,当我们需要做某件事情的时候,就往jlink发送一个命令过去,jlink解析我们发送的命令,处理,并返回一个结果(也有许多命令不返回结果)我们发送给jlink的命令通过WriteFile函数调用写入到pipe01里面,然后通过读取pipe00获得命令结果。jlink的数据使用stream的模式读写,在满足一定的延时要求的情况下,我们可以将一个命令分成几截写入,也可以按批次读取结果,下面我们就用一个简单的命令来举个例子。这个命令用来获取jlink的固件版本,是一个公开的命令。首先我们发送单字节的01到jlinkjlink会返回0x72个字节的内容给我们,前面两个字节是le格式的长度=0x70,表示这之后还要0x70个字节,这0x70个字节就是真正的内容。

bool jlinkCommandReadFirmwareVersion(HANDLE readPipeFile, HANDLE writePipeFile, void* dataBuffer)
{
            uint8_t commandBuffer[1] = {0x01};
            uint16_t leftLength = 0;
            if(!jlinkSendCommand(readPipeFile, writePipeFile, commandBuffer, sizeof(commandBuffer), &leftLength, sizeof(leftLength)))
                        return false;
            return jlinkContinueReadResult(readPipeFile, dataBuffer, leftLength);
}

这里使用了两个函数,这两个函数下面会介绍,先看看这个函数的内容。首先我们调用jlinkSendCommand发送1个字节的0x01给jlink,并读回两个字节的内容,这两个字节的内容=0x70,也就是剩余的数据大小,然后我们调用jlinkContinueReadResult把剩下的内容读全了。注意,上面说过jlink使用的是stream模式,这就意味着,如果我没有未读完的数据还在jlink的缓冲区里面,那么这些内容会出现在下一条命令的返回结果里面,这里要特别小心。

bool jlinkSendCommand(HANDLE readPipeFile, HANDLE writePipeFile, void const* commandBuffer, uint32_t commandLength, void* resultBuffer, uint32_t resultHeaderLength)
{
     if(!WriteFile(writePipeFile, commandBuffer, commandLength, nullptr, nullptr))
        return false;
    if(!resultHeaderLength)
        return true;
        return !!ReadFile(readPipeFile, resultBuffer, resultHeaderLength, nullptr, nullptr);
}

bool jlinkContinueReadResult(HANDLE readPipeFile, void* resultBuffer, uint32_t resultLength)
{
        return !!ReadFile(readPipeFile, resultBuffer, resultLength, nullptr, nullptr);
}

这两个函数非常简单,一目了然。


有了这些辅助函数,我们来看下一个命令。

bool jlinkCommandReadEmulatorMemory(HANDLE readPipeFile, HANDLE writePipeFile, uint32_t address, uint32_t length, void* dataBuffer)
{
    uint8_t commandBuffer[9] =
    {
         0xfe,
         static_cast(address), static_cast(address >> 8), static_cast(address >> 16), static_cast(address >> 24),
         static_cast(length), static_cast(length >> 8), static_cast(length >> 16), static_cast(length >> 24),
    };

    return jlinkSendCommand(readPipeFile, writePipeFile, commandBuffer, sizeof(commandBuffer), dataBuffer, length);
}

这就是关键命令了,id=0xfe,读取jlink自身的内存区域,命令id之后是4个字节的地址,然后是4个字节的长度,都是le格式。在使用这个命令的时候要注意,jlink有缓冲区大小限制,我们不能一次发送太多的数据到jlink,我们也不能一次读取太多的数据,这个缓冲区限制大小是64k,我们也可以使用下面两个函数获取这个值。

uint32_t jlinkGetReadBufferSize(HANDLE deviceFile)
{
            uint32_t readBufferSize = 0;
            jlinkDeviceControl(deviceFile, 0x220460, nullptr, 0, &readBufferSize, sizeof(readBufferSize));
            return readBufferSize;
}

uint32_t jlinkGetWriteBufferSize(HANDLE deviceFile)
{
            uint32_t writeBufferSize = 0;
            jlinkDeviceControl(deviceFile, 0x220464, nullptr, 0, &writeBufferSize, sizeof(writeBufferSize));
            return writeBufferSize;
}

而jlinkDeviceControl这个函数只是一个简单的封装

bool jlinkDeviceControl(HANDLE deviceFile, uint32_t controlCode, void* inputBuffer, uint32_t inputSize, void* outputBuffer, uint32_t outputSize, uint32_t* actualSize = nullptr)
{
            DWORD resultSize = 0;
            if(!DeviceIoControl(deviceFile, controlCode, inputBuffer, sizeof(inputSize), outputBuffer, sizeof(outputSize), &resultSize, nullptr))
                        return false;

            if(actualSize)
                        *actualSize = static_cast(resultSize);
            return true;
}

函数都全了,接下来就进入主题了。首先我们知道jlink的bootloader占用的是0x08000000到0x08010000之间的64k内容,其中从0x0800b700开始到0x0800c000的部分占据0x900个字节,jlink管他称为ots,大概是什么one time section的缩写?这部分放的大约有3个东西,序列号,license,然后还有一段签名,这个贴的最后有部分ots的介绍。这部分jlink并没有提供擦出功能,只能写入,你只能把某个位从1变成0,不能反过来(所以才叫one time?)然后是从0x0800c000开始到0x0800c900同样也是0x900个字节的内容,这个部分jlink管他叫config data,这部分jlink能够擦出,并且能用jlink自带的jlinkconfig.exe修改。他里面主要放的是一些配置信息,比如昵称,是否打开虚拟串口,是否使用jlink给目标板供电什么的。
我们在读取bootloader的时候完全可以跳过ots和config部分,读取前面的0xb700个字节就可以了(实际上bootloader只有不到0x4000个字节)

    uint8_t bootloader[0xb700] = {0};
    jlinkCommandReadEmulatorMemory(readPipeFile, writePipeFile, 0x08000000, sizeof(bootloader), bootloader);

就是这么简单,我们就拿到了bootloader。


但是不要着急,我们跳过了ots和config,这两个部分也挺重要的。如果我们只是希望把挂掉的jlink救活,并且我们的jlink本身就是正版(正版会丢固件吗?我有个盗版丢了),那我们也需要把ots也读出来,但是不要把ots发给其他人,ots里面有唯一的设备签名。把坏掉的jlink擦除掉,然后把这个bootloader刷回去,通电,jlink能识别这个设备,即使这个设备里面只有bootloader,jlink会显示出bootloader的版本来。大约是“J-Link V9 compiled Oct 12 2012 BTL”这样的。然后我们可以使用jlinkconfig.exe从新写入固件,并不需要自己手动去jlink的dll里面导出固件再手动往里面刷。
如果我们想做个一摸一样的复制品(序列号也原样复制),那也把ots读出来。
如果我们想干坏事,批量生产一波,那么还需要了解一下ots这个东西。ots有两个部分,从0xb700开始的0x100字节是一段数字签名,很不幸我这盗版jlink里面全是ff,正版用户可以看看这256个字节的数字签名。jlink使用的是salt长度为4的rsa-pss算法来生成这段签名的。至于rsa-pss算法大家可以google一下。签名的原始数据长度16个字节,前面4个字节是序列号,后面12个字节是stm32提供的设备唯一id。rsa算法长度2048,很不幸现在没啥希望能算出他的私钥,所以大部分(全部?)的盗版这个签名部分都是0xff吧。这段签名即使全是0xff也不影响jlink的功能,只是能用这个签名判断出是否是正版jlink,判断办法如下。

bool jlinkCommandVerifySignature(HANDLE readPipeFile, HANDLE writePipeFile)
{
            uint8_t commandBuffer[] =
            {
                        0x18,
                        0x01,
                        0x01, 0x00, 0x00, 0x00,
                        0x00, 
                        0x00,
            };

            int32_t isValid                                                                                                                = 0;
            if(!jlinkSendCommand(readPipeFile, writePipeFile, commandBuffer, sizeof(commandBuffer), &isValid, sizeof(isValid)))
                    return false;
            return isValid > 0;
}

这个函数返回true的时候,那签名就是合法的,否则就是非法的,我大致看了看jlink的dll,似乎并没有使用这个判断?签名这个0x100个字节我们先放一边(不放一边也没办法,2048位的rsa束手无策的)rsa的e是常客0x10001,n在下面


其实知道了也没啥用。


接下来我们来看看序列号,他位于bf00,这个序列号用的地方有两个,一个自然就是序列号,另外一个是硬件版本,是的你没有看错。硬件版本是用序列号来计算的,首先把序列号除以100000,得到的商再除以10,得到的余数如果大于等于8则取2,得到的就是子版本号。比如我这个盗版设备,sn=59101308,他的硬件版本显示为9.10sn先除以100000,商591,再除以10,余1,所以就是9.10当序列号这个位置4个字节都是ff的时候,我们可以使用exec setsn=xxx来设置一个序列号,如果序列号已经有值了,这个命令就不能用了。


然后就是license,他从bf20开始,每个license占16个字节,他们就是普通的ascii字符串,包括结尾的0。这个部分可以使用exec addfeature来添加license。
所以如果数字签名本身就是无效的话,那么ots部分我们可以完全不用管他,全部擦除成ff,然后用jlink的命令写入需要的值就可以了。当然jlink也提供了两个命令来更新ots,这里介绍一个简单的,他只是更新bf00开始的0x100个字节,这个区域里面包括序列号和license。

int32_t jlinkCommandWriteOneTimeSettings(HANDLE readPipeFile, HANDLE writePipeFile, void const* dataBuffer)
{
            uint8_t commandBuffer[0x10d] = {0x13};
            uint32_t tempValue = crc32(0xffffffff, static_cast(dataBuffer), 0x100) ^ 0xffffffff;
            commandBuffer[0x101] = static_cast(tempValue);
            commandBuffer[0x102] = static_cast(tempValue >> 8);
            commandBuffer[0x103] = static_cast(tempValue >> 16);
            commandBuffer[0x104] = static_cast(tempValue >> 24);
            commandBuffer[0x105] = 0x49;
            commandBuffer[0x106] = 0x44;
            commandBuffer[0x107] = 0x53;
            commandBuffer[0x108] = 0x45;
            commandBuffer[0x109] = 0x47;
            commandBuffer[0x10a] = 0x47;
            commandBuffer[0x10b] = 0x45;
            commandBuffer[0x10c] = 0x52;
            memcpy(commandBuffer + 1, dataBuffer, 0x100);
            if(!jlinkSendCommand(readPipeFile, writePipeFile, commandBuffer, sizeof(commandBuffer), &tempValue, sizeof(tempValue)))
                    return -1;

            return static_cast(tempValue);
}

发送过去的命令长度0x10d,第一个字节是命令id=0x13,接下里0x100个字节就是新的ots数据,然后是4个字节的crc32(根据crc32实现的默认初始值不同,我们可能需要调整0xffffffff这个值为0),接下来8个字节是个常量,他等于IDSEGGER的ascii码。这个命令返回一个错误代码,如果成功写入了,返回的是0,否则返回一个负值。注意,这个ots并不是我们想改成什么样子就能改成什么样子的,我们发送过去的0x100字节的新数据必须满足三个条件。第一,如果原来的ots里面已经有一个非ffffffff的序列号了,那么我们的对应的序列号必须要和原始序列号相同,也就是说我们只有一次机会把序列号从0xffffffff改成其他的。第二,我们发送过去的0x100数据里面的license不能是空的,具体的说就是0x100的ots数据的偏移量0x20这个地方不能是0第三,我们只能把原始数据里面是1的位变成0,不能反过来。如果我们发送过去的数据不满足这三个条件,jlink都会返回错误。至于原来的bf00这部分的数据是什么,我们可以使用上面那个jlinkCommandReadEmulatorMemory函数来读取,或者可以使用下面这个专属命令。

 bool jlinkCommandReadOneTimeSettings(HANDLE readPipeFile, HANDLE writePipeFile, void* dataBuffer)
{
            uint8_t commandBuffer[1] = {0xe6};
            return jlinkSendCommand(readPipeFile, writePipeFile, commandBuffer, sizeof(commandBuffer), dataBuffer, 0x100);
}

这个命令他只能读取固定的0x100个字节的数据(bf00到c000)
当然也有进阶的读取和更新全部0x900数据的命令,但是用处不大(我们盗版用户也没办法生成新的数字签名),这里就不多介绍了,感兴趣的朋友可以自己逆向一下jink的固件当前版本的固件大约是这样的


接下来我们来看一个麻烦一点的办法,这个有风险要拆机,我的bootloader就是用这个办法弄出来的。这也是论坛上各位大牛早就研究过的办法,就是写一段木马进去把bootloader用串口的办法发送出来。这里我们使用一个相对安全的办法来dump出bootloader。我们在jlink的固件里面找到一段不使用代码,插入我们的一段小程序进去,然后把启动向量指向我们的小程序,这段小程序在上电的时候先通过读取某gpio管教的电平高低来决定继续执行还是跳转到原始jlink的启动向量。这样一来,我们的这段小程序不会影响到jlink本身的功能,jlink可以正常使用,只有在上电的时候我们短接某个管脚,才会进入dump模式。只要我们的程序在判读管脚高低电平的时候没有出错,即使接下里的代码里面有问题,也没关系,因为jlink是可以正常使用的,我们可以修改再来过。
因为我们的代码要和jlink的代码共存,所以,我们需要使用汇编语言来生成这段代码,这样能做到比较短小,毕竟jlink里面废代码并不多


首先我们需要找到“废代码”的位置,这个挺容易的,中断表里面通常有很多中断都是不使用的,对应的中断处理函数的代码都是一个死循环。我们可以直接覆盖掉这些中断处理函数,因为他们本身就不会被触发,为了安全我们可以保留一个这样的函数,并把其他的中断向量都指向到这个函数上,这里我偷懒并没有这样做。那么这段废代吗在什么地方呢?就在0802cff0的地方,我们可以看到这里都是一堆的jump指令,好了,覆盖他们


ldr                r0, =0x40023830
ldr                r1, [r0]
orr                r1, r1, #7
str                r1, [r0]                                // enable GPIOA, GPIOB, GPIOC

ldr                r5, =0x40020400
ldr                r0, [r5]
bic                r0, r0, #0xf00000
orr                r0, r0, #0x200000
str                r0, [r5]                                // PB10 = af, PB11 = input

add                r2, r5, #0x10                        // read PB11
ldr                r0, [r2]
tst                r0, #0x800                                // if PB11 is high then jmp to jlink
bne                finished

add                r4, r5, #0x24
ldr                r0, [r4]
bic                r0, r0, #0xf00
orr                r0, r0, #0x700                        // sete PB10 as af7 = USART3_TX
str                r0, [r4]

ldr                r0, =0x40023840                        // enable USART3 clock
ldr                r1, [r0]
orr                r1, r1, #0x40800                // enable USART3 + WWDG
str                r1, [r0]

ldr                r4, =0x40004800                        // r4 = USART_SR
add                r5, r4, 0x0c                        // r5 = USART_CR
ldr                r0, [r5]
bic                r0, #0x2000                                // UE = 0, disable USART first
str                r0, [r5]

mov                r0, #0x0008                                // TE = 1, enable tx
str                r0, [r5]
add                r3, r4, #0x10                        // r3 = USART_CR2
mov                r0, #0
str                r0, [r3]

add                r3, r4, #0x08                        // r3 = USART_BRR
mov                r0, #0x104                                // 30Mhz / 115200 / 16 = 16.25
str                r0, [r3]

ldr                r0, [r5]
orr                r0, #0x2000                                // UE = 1, enable USART
str                r0, [r5]

add                r5, r4, #4                                // r5 = USART_DR

ldr                r6, =0x40002c00                        // r6 = WWDG_CR
mov                r7, #0x7f

 dump_start:
ldr                r1, =0x08000000                        // r1 = current address
mov                r2, #0x10000                        // r2 = left count

wait_txe:
ldr                r0, [r4]
tst                r0, #0x80                                // test TXE
beq                wait_txe

str                r7, [r6]                                // reload watchdog
ldrb              r0, [r1], #1                        // load current byte and output to USART3
str                r0, [r5]

wait_tc:
ldr                r0, [r4]
tst                r0, #0x40                                // wait TC
beq                wait_tc

subs               r2, r2, 1
bne                wait_txe
b                   dump_start

finished:
ldr                pc, =0x0802cf61    // jump to jlink's reset handler

代码简单直接,就不多描述了,我是用的时arm-gcc编译器,大家也许需要做做移植工作才能在别的编译器下面编译。这里面也许唯一需要说的就是串口的波特率计算,怎么知道当前的时钟频率究竟是多少。当然也可以不管三七二十一初始化一趟RCC,不过这样会增加许多的代码量,我这里比较偷懒,首先假定时钟频率是16MHz,然后按照115200的波特率算出一个值来,接着让它开始dump,然后接入一个能自动分析波特率的逻辑分析仪,逻辑分析仪能汇报一个不太准确的波特率,没关系,用这个不太准确的波特率回推出一个不太准确的时钟频率,我这里算出来是个30.13MHz,那么显然实际的时钟就是30MHz然后在修改成30MHz的参数实际的跑一下,顺利拿到bootloader。


然后还有一个,这个固件里面的初始化数据经过压缩,也不知道是谁家的编译器(ti家的编译器有这个压缩功能,不过我没详细看过),大家还需要一份代码来解压缩。

uint8_t const* in8 = 0x0802D2BC;
uint8_t* out8 = 0x20000008;
while(out8 < 0x20000008 + 0x814)
{
    uint32_t c  = *in8 ++;
    uint32_t a = c & 3;
    if(!a)
        a = 3 + static_cast(*in8 ++);

    uint32_t b = c >> 4;
    if(b == 0x0f)
        b = 0xf + static_cast(*in8 ++);

    while(-- a)
        *out8 ++ = *in8 ++;

    if(b)
    {
        uint32_t e = *in8 ++;
        uint32_t d = (c >> 2) & 0x03;
        if(d == 3)
            d = *in8 ++;

        d = (d << 8) + e;
        uint8_t* back8 = out8 - d;
        for(uint32_t j = 0; j < b + 2; j ++)
            *out8 ++ = *back8 ++;
        }
    }

也很简单的,当前这个版本,压缩之前的代码在0802D2BC,长度0x668,解开到20000008,长度0x814解开的数据可以用ida再加载进去。方便分析。

你可能感兴趣的:(<转>Se99er V9 分析)