起因

最近从朋友那搞到了一台松下的“加拉帕戈斯”笔记本——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)

概要

在Android的FOTA更新包中,因为现在大家都去用payload.bin了,所以旧的Block更新的Sparse Image转Raw Image工具就没人更新了
之前留意到Brotli压缩版本的还有人写工具,但是最近发现在一些低性能电视盒子上使用的是lzma压缩方式就没有人在意了。
所以把这整个工具重构了一遍,使其更加好用且性能更好。

库安装

pip3 install Brotli

代码

#!/usr/bin/env python3
import sys
import os
import lzma
import brotli
from pathlib import Path
from typing import List, Tuple, BinaryIO

BLOCK_SIZE = 4096

def isTransferList(file_path: str, sample_size: int = 1024) -> bool:
    try:
        with open(file_path, 'rb') as f:
            return b'\0' not in f.read(sample_size)
    except IOError:
        return False

def decompressLzmaDat(input_file: str, output_file: str):
    with lzma.open(input_file, 'rb') as lzma_file:
        with open(output_file, 'wb') as decompressed_file:
            decompressed_file.write(lzma_file.read())

def decompressBrotliDat(input_file: str, output_file: str):
    with open(input_file, 'rb') as br_file:
        with open(output_file, 'wb') as decompressed_file:
            decompressed_file.write(brotli.decompress(br_file.read()))

def rangeSet(src: str) -> List[Tuple[int, int]]:
    src_set = [int(item) for item in src.split(',')]
    if len(src_set) != src_set[0] + 1:
        raise ValueError(f'Error parsing data to rangeSet: {src}')
    return [(src_set[i], src_set[i+1]) for i in range(1, len(src_set), 2)]

def parseTransferList(path: str) -> Tuple[int, int, List]:
    if not isTransferList(path):
        raise ValueError(f"The file '{path}' does not appear to be a valid transfer list file.")

    with open(path, 'r') as trans_list:
        version = int(trans_list.readline())
        new_blocks = int(trans_list.readline())

        if version >= 2:
            trans_list.readline()
            trans_list.readline()

        commands = []
        for line in trans_list:
            cmd, *params = line.split()
            if cmd in ['erase', 'new', 'zero']:
                commands.append([cmd, rangeSet(params[0])])
            elif not cmd[0].isdigit():
                raise ValueError(f'Command "{cmd}" is not valid.')

    return version, new_blocks, commands

def processFile(new_data_file: BinaryIO, output_img: BinaryIO, commands: List, max_file_size: int):
    for command in commands:
        if command[0] == 'new':
            for block in command[1]:
                begin, end = block
                block_count = end - begin
                print(f'Copying {block_count} blocks into position {begin}...')

                output_img.seek(begin * BLOCK_SIZE)
                output_img.write(new_data_file.read(block_count * BLOCK_SIZE))
        else:
            print(f'Skipping command {command[0]}...')

    if output_img.tell() < max_file_size:
        output_img.truncate(max_file_size)

def main(transfer_list_file: str, new_data_file: str, output_image_file: str):

    version, new_blocks, commands = parseTransferList(transfer_list_file)

    android_versions = {
        1: 'Android 5.0',
        2: 'Android 5.1',
        3: 'Android 6.0',
        4: 'Android 7.0 or Higher'
    }
    print(f'{android_versions.get(version, "Unknown")} Version Image Detected')

    output_img_path = Path(output_image_file)
    if output_img_path.exists():
        raise FileExistsError(f'Output file "{output_img_path}" already exists')

    decompressed_file = None
    if 'lzma' in new_data_file.lower():
        print("LZMA file detected. Decompressing...")
        decompressed_file = new_data_file + '.decompressed'
        decompressLzmaDat(new_data_file, decompressed_file)
        new_data_file = decompressed_file
        print("Decompression Completed!")
    elif new_data_file.lower().endswith('.br'):
        print("Brotli file detected. Decompressing...")
        decompressed_file = new_data_file + '.decompressed'
        decompressBrotliDat(new_data_file, decompressed_file)
        new_data_file = decompressed_file
        print("Decompression Completed!")

    with open(new_data_file, 'rb') as new_data, output_img_path.open('wb') as output_img:
        all_block_sets = [i for command in commands for i in command[1]]
        max_file_size = max(pair[1] for pair in all_block_sets) * BLOCK_SIZE

        processFile(new_data, output_img, commands, max_file_size)

    print(f'Done! Output image: {output_img_path.resolve()}')

    if decompressed_file:
        os.remove(decompressed_file)
        print("Temporary decompressed file removed.")

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print('Usage: sdat2img_v2.py <transfer.list> <dat|dat.lzma|dat.br> [raw.img]')
        print('<transfer.list>:Transfer List File')
        print('<dat|dat.lzma|dat.br>:New Dat File (Support Uncompressed, LZMA or Brotli)')
        print('[raw.img]:Output File Name of Raw Image\n')
        sys.exit(1)

    transfer_list_file = sys.argv[1]
    new_data_file = sys.argv[2]
    
    if len(sys.argv) > 3:
        output_image_file = sys.argv[3]
    else:
        base_name = os.path.basename(sys.argv[1]).split('.')[0]
        output_image_file = f"{base_name}.raw.img"

    try:
        main(transfer_list_file, new_data_file, output_image_file)
    except Exception as e:
        print(f"An error occurred: {e}", file=sys.stderr)
        sys.exit(1)

可执行文件

For Windows x64