今天刚好看到看雪上有人在求解包这个东西,并且引用了本博客的一篇博文作为启发点,作为互联网上为数不多真的研究过这个引擎的人,今天把以前逆向的部分结果继续逆向,最终得出此文。看雪原始链接:https://bbs.kanxue.com/thread-285879-1.htm

0x00 前言

如果你折腾过日本手机或某些车机系统的固件,你一定遇到过 .mld、.vbe、.dat 或 .delta 后缀的文件。这些是 RedBend的vDirect Mobile设备MDM管理方案所产生差分升级包。
与普通的 zip 不同,RedBend vDM并不只是简单的压缩,它是一个基于块的差分引擎。想要从中提取差分包或镜像数据,通常的做法是逆向其解压算法。
但当我把从某平板中得到的二进制扔进IDA后,发现事情并没有那么简单。

0x01 劝退现场:为什么不尝试还原算法?

一开始,按照思维惯性,在得到的刷机包里看到了一个名为rb_ua的可执行文件和一个名为UpatePackage.mld的更新包本体(MAGIC为RedBendEFDPackage),就知道这把玄了(虽然至少证明了这是RB的公版实现,其对厂商应该是SDK交付,日本厂商一般都会进行大量二开)。不过还好这个平板采用的是Intel Atom SoC,其本身就是x86_64架构,为直接拉起这个二进制和后续分析提供了方便。
在基于updater-script里面的传参进行分析之后,在IDA里追来追去,找到了其核心差分引擎的函数(sub_41FF10),发现RedBend vDM并不是一个我之前认为的简单的换了字典的LZMA解压器,而是一个完整的针对 Delta 数据流的虚拟机
更可怕的是,为了在移动设备上达到极致的更新速度,RedBend在开发中大量使用了SIMD指令集来进行数学运算!(虽然实质在Atom平台上反而拖累了性能),以下是劝退现场的伪代码。

// 此处是高度优化的向量化加法循环 Target = Source + Delta
while ( 1 ) 
{
  // 加载Source和Delta数据到128位寄存器
  v173 = _mm_shuffle_epi32(_mm_cvtsi32_si128(dword_54AD20), 0);
  v74 = _mm_shuffle_epi32(_mm_cvtsi32_si128(v70), 0);

  // 使用SSE/AVX指令进行并行差分还原
  // 这里的逻辑极其晦涩,不仅有加法,还有shuffle和乘法
  *(__m128i *)(v171 - 16) = _mm_add_epi32(
      _mm_mullo_epi32(
          (__m128i)_mm_shuffle_ps(
              si128,
              (__m128)_mm_add_epi64(
                  _mm_load_si128((const __m128i *)&xmmword_4F09C0),
                  (__m128i)si128),
              136),
          v173),
      v175
  );
}

其中出现的_mm_add_epi32、_mm_mullo_epi32和_mm_shuffle_ps直接证明了其差分运算是在寄存器层面进行的并行数学计算
那么就代表如果我要使用Python来写一个解包器,我不光光要去处理那个坑人的非标字典LZMA,还要处理复杂的SSE/AVX调用。
所以证明了,直接还原算法这条路走不通。

0x03 伪造运行环境欺骗RB_UA

既然逆向算法实在太难,为什么不直接利用官方提供的rb_ua程序呢?它自己肯定知道怎么解压,否则它怎么装?
现在只需要解决两个问题:
1.它通常运行在 Android 环境下,依赖特定的分区(/dev/block/mmcblk...)。
2.它在更新前会进行一系列严苛的环境检查。

逻辑一:环境检查的死穴(MISC分区)

rb_ua为了防止更新中途断电变砖,会在启动前向misc分区写入一个非标的Recovery Flag,所以必须给它一个可以写的地方才能通过验证。

// Force Misc Check
if ( !fstab->has_partition("misc") ) {
    log_error("Cannot find partition 'misc'");
    return ERROR; 
}
write_recovery_flag(fstab->get("misc"), "UPDATING");

对策:如果找不到misc它就罢工。那我们就给它造一个假的misc分区!

逻辑二:绕过哈希校验 (No Scout)

默认情况下,作为OTA方案,它会校验源分区的哈希。因为我们需要把把空文件变成新文件,哈希100是对不上的。(此时感谢该厂商提供的是“完整包”)

if ( operation_mode == "scout_update" ) {
    // 校验哈希,不匹配则报错退出
    if ( check_hash(target_file) != expected ) error();
} 
else if ( operation_mode == "no_scout" ) {
    // 隐藏的跳过哈希校验参数
    log_info("Skipping hash check...");
    vDirect_Engine_Run(source, delta, output);
}

对策:使用隐藏参数 --update_operation no_scout,强制它闭嘴干活。

0x03 实战:搭建环境

我们在WSL环境下,通过文件映射来欺骗rb_ua。

1.伪造空白分区

# System分区给大点,4GB较好
dd if=/dev/zero of=./dummy_system.img bs=1M count=4096
# 内核通常64MB
dd if=/dev/zero of=./dummy_boot.img bs=1M count=64
# Android x86的bootloader分区,通常64MB
dd if=/dev/zero of=./dummy_bootloader.img bs=1M count=64
# 伪造misc分区,骗过环境检查
dd if=/dev/zero of=./dummy_misc.img bs=1M count=1

2. 伪造分区表 (fake_fstab)

/system     emmc    /mnt/d/Temp/PANA_RB/dummy_system.img
/boot       emmc    /mnt/d/Temp/PANA_RB/dummy_boot.img
/bootloader emmc    /mnt/d/Temp/PANA_RB/dummy_bootloader.img
/misc       emmc    /mnt/d/Temp/PANA_RB/dummy_misc.img

0x04 执行!

执行以下命令,启动rb_ua。注意单独传入的--update_operation no_scout参数,这是成功的关键。

# 清理其自动产生的结果文件,避免无法启动
rm -f ./result.txt

# 启动“更新”
./rb_ua \
  -w ./work_temp \
  -d ./UpdatePackage.mld \
  --partitions_list ./fake_fstab.txt \
  --update_flavor std \
  --update_operation no_scout \
  --result_file ./result.txt \
  --no_ui \
  -l debug:extract.log

RBOTA_START
日志中开始出现如下状态,则代表已经开始解压!
RBOTA_LOG

< 10 > RB emmc: Opened device .../dummy_misc.img...
< 10 > Redbend PL: Added 0 args to misc
...
< 10 > RB emmc: Opened device .../dummy_system.img...
< 00 > Redbend PL: Performing system partition update (op 1)
< 00 > Redbend PL: system partition update done, ret 0  <--- 成功解压了SYSTEM分区!

此时再打开我们创建的dummy_system.img等文件,内部已经是正常的EXT4/FAT32分区数据了!
RBOTA_RESULT

0x05 总结

逆向并不总是意味着要硬啃汇编和反汇编。有时候,理解程序的运行逻辑(如环境检查、模式选择)比理解它的计算逻辑(SIMD算法)来的更有效率。
通过构建一个符合程序预期的“楚门的世界”,我们成功让官方工具充当了我们的解包器,兵不血刃地拿下了完整固件。
Happy Hacking by Yuu!

起因

最近从朋友那搞到了一台松下的“加拉帕戈斯”笔记本——Let's Note RZ5。这机器哪哪都好,超迷你超轻机身和全功能接口,就是这个侧边的 无线拨动开关在Linux下完全是个摆设。按道理来说,作为一个硬核日系本,这种物理开关应该能在底层直接切断网卡电源,但在 Linux下rfkill没找到一点设备,acpi_listen也是一片死寂。
利用官方的驱动安装指南,发现在Windows下这个开关通过松下专有的ACPI\MAT0028接口工作。但在Linux的世界里,信号似乎被某种神秘的力量拦截了,一点点反应都没有。

寻踪

首先是翻阅DSDT表,找到这个硬接口相关的ACPI实现。
需要利用sysfs导出DSDT表后反编译回ASL源代码

sudo cat /sys/firmware/acpi/tables/DSDT > dsdt.bin
iasl -d dsdt.bin

搜索MAT0028,找到了以下代码段

    Scope (_SB)
    {
        Device (WLSW)
        {
            Mutex (HDMX, 0x00)
            Name (_HID, EisaId ("MAT0028"))  // _HID: Hardware ID
            Name (HINP, Zero)
            Name (HOUP, Zero)
            Name (HDAT, Buffer (0x20){})
            Name (SIFR, Package (0x02)
            {
                Zero, 
                One
            })
            Name (DRVS, Zero)
            Name (WNSW, One)
            Method (_STA, 0, NotSerialized)  // _STA: Status
            {
                Local0 = Zero
                If (CondRefOf (\_OSI, Local6))
                {
                    If (_OSI ("Windows 2012"))
                    {
                        Local0 = 0x0F
                    }
                }

                Return (Local0)
            }

            Method (_INI, 0, NotSerialized)  // _INI: Initialize
            {
            }

            Method (SQTY, 0, NotSerialized)
            {
                If ((DRVS == Zero))
                {
                    DRVS = One
                    WNSW = One
                    HRES ()
                }

                Return (0x02)
            }

            Method (HIND, 1, Serialized)
            {
                Acquire (HDMX, 0xFFFF)
                HDAT [HINP] = Arg0
                HINP++
                HINP %= 0x20
                If ((HINP == HOUP))
                {
                    HOUP++
                    HOUP %= 0x20
                }

                Release (HDMX)
            }

            Method (HINF, 0, Serialized)
            {
                Acquire (HDMX, 0xFFFF)
                If ((HINP == HOUP))
                {
                    Local0 = Zero
                }
                Else
                {
                    Local0 = DerefOf (HDAT [HOUP])
                    HOUP++
                    HOUP %= 0x20
                }

                Local1 = (HINP != HOUP)
                Release (HDMX)
                If (Local1)
                {
                    Notify (WLSW, 0x80) // Status Change
                }

                Return (Local0)
            }

            Method (HSAV, 0, Serialized)
            {
                If (DRVS)
                {
                    Acquire (HDMX, 0xFFFF)
                    Release (HDMX)
                }
            }

            Method (HRES, 0, Serialized)
            {
                If (DRVS)
                {
                    Acquire (HDMX, 0xFFFF)
                    SHRF ()
                    Release (HDMX)
                }
            }

            Method (SINF, 0, Serialized)
            {
                SIFR [Zero] = SGET (Zero)
                SIFR [One] = SGET (One)
                Return (SIFR) /* \_SB_.WLSW.SIFR */
            }

            Method (SGET, 1, Serialized)
            {
                Local0 = Zero
                If ((Arg0 == Zero))
                {
                    Local0 = Zero
                    If (WSST ())
                    {
                        If (CMST ())
                        {
                            Local0 = One
                        }
                    }
                }

                If ((Arg0 == One))
                {
                    Local0 = WNSW /* \_SB_.WLSW.WNSW */
                }

                Return (Local0)
            }

            Method (SSET, 2, Serialized)
            {
                If ((Arg0 == Zero)){}
                If ((Arg0 == One))
                {
                    WNSW = Arg1
                    SHRF ()
                }

                Return (Zero)
            }

            Method (NOTF, 0, Serialized)
            {
                HIND (0x50)
                Notify (WLSW, 0x80) // Status Change
                SHRF ()
            }

            Method (WSST, 0, Serialized)
            {
                Local0 = ^^HKEY.SGET (0x0B)
                If (((Local0 & 0xC0) == 0xC0))
                {
                    Return (One)
                }

                Return (Zero)
            }

            Method (CMST, 0, Serialized)
            {
                Return (One)
            }

            Method (SHRF, 0, Serialized)
            {
                Local0 = Zero
                Local1 = Zero
                If ((WNSW & One))
                {
                    If ((SGET (Zero) & One))
                    {
                        Local0 |= One
                        Local1 = One
                    }
                }

                Local0 |= 0x10
                Local0 |= 0x20
                Local0 |= 0x0100
                Local0 |= 0x0200
                Local0 |= 0x00010000
                Local0 |= 0x00020000
                Local0 |= 0x1000
                Local0 |= 0x2000
                ASRV (0x0F, 0x03, Local0)
            }
        }
    }

通读这段ASL代码后发现这个开关并不是标准的物理断电,而是一个触发Notify(WLSW, 0x80)的ACPI事件设备,通过调用HKEY下的SGET(0x0B)来读取物理状态。

欺骗

在尝试编写驱动之前,遇到了第一个坑:MAT0028设备在Linux下默认居然不启动!查看 _STA 方法发现,这玩意儿居然有系统检测!

                    If (_OSI ("Windows 2012"))
                    {
                        Local0 = 0x0F
                    }

居然需要Windows8以上才初始化这个设备!
按照使用Linux的惯例,直接去grub添加acpi_osi欺骗,添加后重启系统再cat /sys/bus/acpi/devices/MAT0028/status,果然成功返回了 15 (0x0F),活了!

驱动

虽然现在设备是活了,但是没有任何驱动来认领它,对于系统而言,其只是一个莫名其妙的设备,没有信息,没有内容。本来去Linux的主干分支找到了一个名为panasonic-laptop的驱动,但是仔细阅读后发现其过于古早,不如直接自己手搓一个内核模块来的快。
好!说干就干!
驱动的逻辑很简单:绑定ACPI\MAT0028,注册一个标准的rfkill设备。当拨动开关触发ACPI Notify时,驱动去调用DSDT里的WSST方法读取开关位置,并同步给内核的无线电管理子系统。
以下是测试可用的内核模块代码,为了考虑到跟Linux主干的驱动兼容性,故其不叫PanasonicXX驱动,而是叫MatsushitaRadioSwitch,这样以便区分。

#include <linux/module.h>
#include <linux/acpi.h>
#include <linux/rfkill.h>

#define MODULE_NAME "MatRadioSwitch"

struct mat_radio_data {
    struct rfkill *rfk;
};

static int mat_rfkill_set_block(void *data, bool blocked) { return 0; }

static const struct rfkill_ops mat_rfkill_ops = { .set_block = mat_rfkill_set_block };

static void mat_update_status(struct acpi_device *device, struct rfkill *rfk) {
    unsigned long long result = 0;
    if (ACPI_SUCCESS(acpi_evaluate_integer(device->handle, "WSST", NULL, &result)))
        rfkill_set_hw_state(rfk, !result);
}

static void mat_acpi_notify(struct acpi_device *device, u32 event) {
    struct mat_radio_data *data = acpi_driver_data(device);
    if (event == 0x80 && data && data->rfk)
        mat_update_status(device, data->rfk);
}

static int mat_acpi_add(struct acpi_device *device) {
    struct mat_radio_data *data;
    unsigned long long res = 0;

    acpi_evaluate_integer(device->handle, "SQTY", NULL, &res);

    data = devm_kzalloc(&device->dev, sizeof(*data), GFP_KERNEL);
    if (!data) return -ENOMEM;

    device->driver_data = data;
    data->rfk = rfkill_alloc("matsu_radio_sw", &device->dev, 
                             RFKILL_TYPE_WLAN, &mat_rfkill_ops, data);
    
    if (!data->rfk || rfkill_register(data->rfk)) {
        if (data->rfk) rfkill_destroy(data->rfk);
        return -ENOMEM;
    }

    mat_update_status(device, data->rfk);
    return 0;
}

static void mat_acpi_remove(struct acpi_device *device) {
    struct mat_radio_data *data = acpi_driver_data(device);
    if (data && data->rfk) {
        rfkill_unregister(data->rfk);
        rfkill_destroy(data->rfk);
    }
}

static const struct acpi_device_id mat_device_ids[] = { { "MAT0028", 0 }, { "", 0 } };
MODULE_DEVICE_TABLE(acpi, mat_device_ids);

static struct acpi_driver mat_radio_driver = {
    .name = "mat_radio_driver",
    .class = "pcc",
    .ids = mat_device_ids,
    .ops = { .add = mat_acpi_add, .remove = mat_acpi_remove, .notify = mat_acpi_notify },
};

module_acpi_driver(mat_radio_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Yuu");
MODULE_DESCRIPTION("Matsushita Radio Switch ACPI Driver");

总结

成功编译后,insmod,拨动开关,WIFI果然被成功开启和关闭了!这次驱动开发十分成功!

从今天起,我的博客全站(包括所有子域名)已经全面迁移至 HTTP/3(基于 QUIC 协议)!
HTTP/1.1 陪伴了我整个程序开发生涯的起步;HTTP/2 把我带入了TLS的时代;而现在,HTTP/3 则将为更远的协议演进提供了更好的支持。
为了更好的接入Internet,现在本站已启用IPv6。
谨以此文,纪念一次技术演进的足迹。
Powered by Nginx/1.28.0

20250915更新: 在F-08E和F-06F上测试也可用
20250902更新: 在F-02L上测试也可用
20250810更新: 在F-06E上测试也可用

在雅虎拍卖上捡老日机的垃圾,不可避免的会捡到一些有Felica锁定的机器,虽然只影响NFC一个功能不可用,但是还是会觉得很膈应,而且因为其Felica的NV是单独存储的,所以恢复出厂设置并不影响Felica锁定。
最近就收到了一台有Felica锁定的富士通F-01F,在逆向其Felica锁定管理器的时候,发现了一个神奇的供店头使用的解锁后门。故写一下这篇分析梗概,以供使用。
FjFelicaUnl_LockStatus
锁定状态,NFC完全不可用
从设备内提取了管理Felica锁定的APK
FjFelicaUnl_APK.png
整个APK拖入JADX,一眼就看到了一个Felica_Reset_Lock函数
FjFelicaUnl_FoundFunction
感谢JADX的强大反编译能力,一下就看到了一个可读性很高的解锁码校验函数
FjFelicaUnl_UnlockCodeCheck
大概读了一下就发现这只是个简单的数字替换算法(标准的日本Security through obsecurity思想代码)
立即可用的JS移植版本网页版实现:App/FjFelicaUnl
现在有解锁码的生成方法之后,需要找到在哪里输入,以及有什么先决条件。
接着随着Felica_Reset_Lock函数的调用情况,追到了其MainActivity,果然发现其必须在开机后120秒内在指定输入框内输入有前缀和后缀的解锁码才行。
FjFelicaUnl_PreCheck
所以在生成解锁码后,我们需要重启
FjFelicaUnl_Reboot
然后立即在指定输入框输入解锁码
FjFelicaUnl_InputPwd
立即出现“解除しました。”!!
FjFelicaUnl_Unlocked
再打开NFC/おサイフケータイ設定,果然变成了全新状态!
FjFelicaUnl_BrandNew

今天帮朋友在分析索尼电脑的恢复镜像,其使用一种奇怪的MOD格式作为存储设备驱动和附带软件的格式。
有些MOD文件用HEX编辑器打开可以看到一些明文,有些甚至可以使用7z打开看到目录结构,但是解压时候却提示是损坏的CAB格式。
所以针对恢复盘镜像中的安装器做了些许逆向,一开始依据代码和字符串找到的是一个很复杂的基于"Sony Corporation"字符串的XOR算法,但是写了个小工具把算法利用到MOD文件上后反而导致其彻底打不开了。

一时间想到了放弃,但是准备放弃之前,鼠标往下一滑动,发现MOD文件的尾部都有WIM信息,突然灵光乍现想到这不会是改了MAGIC的WIM镜像吧?接着尝试替换掉了文件最前面的16字节,果然都可以完整解压和打开了。

以下就是更为方便的Python代码,其支持直接对恢复盘中的data目录进行遍历恢复,可将所有mod文件恢复为正常的wim文件。

代码

import os
import argparse

SIGNATURE = b"oratdnn"
PATCH = b"MSWIM\x00\x00\x00\xd0\x00\x00\x00\x00\r\x01\x00"

def process_file(file_path):
    with open(file_path, 'rb') as f:
        file_header = f.read(16)
        remaining_data = f.read()
    
    if SIGNATURE in file_header:
        print(f"Detected!! {file_path}")
        
        with open(file_path, 'wb') as f:
            f.write(PATCH)
            f.write(remaining_data)
        
        new_file_path = file_path.replace(".mod", ".wim")
        os.rename(file_path, new_file_path)
        
        print(f"Patched!! {file_path} -> {new_file_path}")
        return True
    else:
        print(f"Skipped!! {file_path} (no matching header)")
        return False

def traverse_directory(directory):
    for root, _, files in os.walk(directory):
        for filename in files:
            file_path = os.path.join(root, filename)
            
            if filename.endswith(".mod"):
                process_file(file_path)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SONY Recovery MODule File Patching Tool")
    parser.add_argument("directory", type=str, help="Path to data folder")
    args = parser.parse_args()
    
    traverse_directory(args.directory)