前言

最近在一个Discord群里跟一群大佬们折腾老的日系传统平台手机,有群友发现SH-03E这个MOAP(Symbian)设备使用的是eMMC,并且成功进行了Dump,历经一些研究,我和另一位群友一起研究了ROFS相关内容,成功解压了它的Z分区,Z分区中很多图符资源都是使用MBM(MultiBitmap/多重位图)格式打包并压缩的,需要使用塞班SDK里面的bmconv小工具来解压,但是其使用逻辑很奇怪,所以这次尝试写了一个图形界面。又由于群内很多人使用的是Linux,所以这个小软件支持Windows/Linux双平台使用。

注意

请使用ARCHIVE.ORG取得塞班SDK,安装之后在<安装目录>\epoc32\bin下找到bmconv的Windows和Linux可执行文件

截图

Windows下
MBME_Windows
Linux下
MBME_Linux

代码

import tkinter as tk
from tkinter import filedialog, messagebox, Toplevel, Text, Scrollbar
from tkinter.ttk import Progressbar
from subprocess import Popen, PIPE
from PIL import Image, ImageTk
import os
import re


class mbmExtractor:
    def __init__(self, root):
        self.root = root
        self.uiInit()
        self.mbmFile = ""
        self.currentImageIndex = 0
        self.bmpFiles = []
        self.tempDir = "temp"
        if not os.path.exists(self.tempDir):
            os.makedirs(self.tempDir)

    def uiInit(self):
        self.root.title("MultiBitMap Extractor")
        self.root.geometry('800x600')

        self.previewAreaPanel = tk.Label(self.root, text="Preview Area", relief=tk.SUNKEN)
        self.previewAreaPanel.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        navigationFrame = tk.Frame(self.root)
        navigationFrame.pack(side=tk.TOP, fill=tk.X)

        self.counterLabel = tk.Label(navigationFrame, text="No Bitmaps")
        self.counterLabel.pack(side=tk.LEFT, padx=5)

        self.prevButton = tk.Button(navigationFrame, text="Prev", command=self.prevImage)
        self.prevButton.pack(side=tk.LEFT, padx=5)

        self.nextButton = tk.Button(navigationFrame, text="Next", command=self.nextImage)
        self.nextButton.pack(side=tk.LEFT, padx=5)

        actionFrame = tk.Frame(self.root)
        actionFrame.pack(side=tk.BOTTOM, fill=tk.X)

        self.openButton = tk.Button(actionFrame, text="Open", command=self.askOpenFile)
        self.openButton.pack(side=tk.LEFT, padx=5)

        self.extractButton = tk.Button(actionFrame, text="Extract", state='disabled', command=self.extractMbm)
        self.extractButton.pack(side=tk.LEFT, padx=5)

        self.debugButton = tk.Button(actionFrame, text="Debug", command=self.debugInfoForm)
        self.debugButton.pack(side=tk.LEFT, padx=5)

        self.progressBar = Progressbar(actionFrame, orient=tk.HORIZONTAL, length=100, mode='determinate')
        self.progressBar.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)

    def askOpenFile(self):
        self.mbmFile = filedialog.askopenfilename(
            title="Choose MBM file",
            filetypes=(("MultiBitMap File", "*.mbm"), ("All Files", "*.*"))
        )
        if self.mbmFile:
            self.extractButton['state'] = 'normal'
            mbmFilename = os.path.basename(self.mbmFile)
            self.root.title(f"MultiBitMap Extractor - {mbmFilename}")

    def extractMbm(self):
        if not self.mbmFile:
            messagebox.showerror("ERROR", "No MBM file selected.")
            return

        bitmapCount = self.counterBitmap()
        if bitmapCount > 0:
            bmpArgs = " ".join(f'"{os.path.join(self.tempDir, f"{i}.bmp")}"' for i in range(1, bitmapCount + 1))
            for i in range(1, bitmapCount + 1):
                self.bmconvAdapter(f'/u "{self.mbmFile}" {bmpArgs}')
                self.progressBar['value'] = (i / bitmapCount) * 100
                self.root.update_idletasks()
            self.loadBitmap()
            self.displayBitmap(self.currentImageIndex)
            self.progressBar['value'] = 0
        else:
            messagebox.showinfo("INFO", "No bitmaps found in the MBM file.")

    def counterBitmap(self):
        output = self.bmconvAdapter(f'/v "{self.mbmFile}"')
        match = re.search(r'containing (\d+) bitmaps', output)
        return int(match.group(1)) if match else 0

    def loadBitmap(self):
        self.bmpFiles = self.sortNumber(os.listdir(self.tempDir))
        self.currentImageIndex = 0
        self.updateNavigationButtonStatus()
        self.updateCounterLabel()

    @staticmethod
    def sortNumber(data):
        convert = lambda text: int(text) if text.isdigit() else text.lower()
        alphanumKey = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
        return sorted(data, key=alphanumKey)

    def displayBitmap(self, index):
        if 0 <= index < len(self.bmpFiles):
            imgPath = os.path.join(self.tempDir, self.bmpFiles[index])
            try:
                img = Image.open(imgPath)
                imgtk = ImageTk.PhotoImage(image=img)
                self.previewAreaPanel.config(image=imgtk)
                self.previewAreaPanel.image = imgtk
            except IOError as e:
                messagebox.showerror("ERROR", f"Can't open bitmap file {imgPath}:\n{e}")

    def prevImage(self):
        if self.currentImageIndex > 0:
            self.currentImageIndex -= 1
            self.displayBitmap(self.currentImageIndex)
            self.updateNavigationButtonStatus()
            self.updateCounterLabel()

    def nextImage(self):
        if self.currentImageIndex < len(self.bmpFiles) - 1:
            self.currentImageIndex += 1
            self.displayBitmap(self.currentImageIndex)
            self.updateNavigationButtonStatus()
            self.updateCounterLabel()

    def updateCounterLabel(self):
        text = f"{self.currentImageIndex + 1}/{len(self.bmpFiles)}"
        self.counterLabel.config(text=text)

    def updateNavigationButtonStatus(self):
        self.prevButton['state'] = 'normal' if self.currentImageIndex > 0 else 'disabled'
        self.nextButton['state'] = 'normal' if self.currentImageIndex < len(self.bmpFiles) - 1 else 'disabled'

    def debugInfoForm(self):
        debugForm = Toplevel(self.root)
        debugForm.title("Debug Info")
        debugForm.geometry('400x300')

        scrollbar = Scrollbar(debugForm)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        text = Text(debugForm, yscrollcommand=scrollbar.set)
        text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.config(command=text.yview)

        debugOutput = self.bmconvAdapter(f'/v "{self.mbmFile}"')
        text.insert(tk.END, debugOutput)

    def bmconvAdapter(self, command):
        bmconvLocalPath = os.path.join(os.path.dirname(__file__), 'bmconv')
        process = Popen(f'{bmconvLocalPath} {command}', stdout=PIPE, stderr=PIPE, shell=True)
        output, error = process.communicate()
        if error:
            messagebox.showerror("ERROR",
                                 f"Not Found BMCONV Executable File! Please Download S60 SDK and Copy bmconv to {bmconvLocalPath}")
        return output.decode("utf-8")

    def getBinaryVersion(self):
        output = self.bmconvAdapter('')
        match = re.search(r'version (\d+)', output)
        return match.group(1) if match else 'Unknown'

    def clearTempDir(self):
        for file in os.listdir(self.tempDir):
            os.remove(os.path.join(self.tempDir, file))
            os.rmdir(self.tempDir)

    def Starter(self):
        version = self.getBinaryVersion()
        self.root.title(f"MultiBitMap Extractor - BMCONV Version {version}")
        self.root.mainloop()

if __name__ == "__main__":
    root = tk.Tk()
    exec = mbmExtractor(root)
    exec.Starter()
    exec.clearTempDir()

整合包

整合包是一个彩蛋
可在OBJect找到
其中的可执行文件来自于Symbian^3 SDK for Belle
感谢Symbian Foundation(虽然其已在法理上消失)


解锁完成之后对其系统更新包产生了兴趣
仔细看了下发现结构简单,所以写了个基于python的解包器
有空再看Cramfs里面是什么吧~哈哈哈哈

import struct
import sys

def extractCramfs(fwData, offset):
    sizeStruct = struct.unpack('<I', fwData[offset+4:offset+8])[0]
    return fwData[offset:offset+sizeStruct]

def extractKernel(fwData, offset):
    headerSize = 64
    imageSize = struct.unpack('>I', fwData[offset+12:offset+16])[0]
    return fwData[offset:offset+headerSize+imageSize]

def extractLinuxVersion(uImageHeader):
    verStartSize = 32
    versionInfo = uImageHeader[verStartSize:64].decode('utf-8', 'ignore')
    return versionInfo

if __name__ == "__main__":
    if len(sys.argv) != 3 or sys.argv[1] != '--firm':
        print("Usage: python uroad_extractor.py --firm <path/to/firmware>")
        sys.exit(1)

    firmwarePath = sys.argv[2]
    with open(firmwarePath, 'rb') as firmwareFile:
        fwData = firmwareFile.read()

        cramfsData = extractCramfs(fwData, 32)
        with open('rootfs.cramfs', 'wb') as cramfsFile:
            print("Cramfs Size:", len(cramfsData))
            print("Cramfs Offset:", 32)
            print("Cramfs End:", 32 + len(cramfsData))
            cramfsFile.write(cramfsData)

        kernelStartAddr = 32 + len(cramfsData)
        uImageData = extractKernel(fwData, kernelStartAddr)
        with open('uImage.bin', 'wb') as uImageFile:
            print("Kernel Size:", len(uImageData))
            print("Kernel Offset:", kernelStartAddr)
            print("Kernel End:", kernelStartAddr + len(uImageData))
            print("Kernel CRC:", hex(struct.unpack('>I', uImageData[16:20])[0]))
            uImageFile.write(uImageData)

        linuxVersion = extractLinuxVersion(uImageData[:64])
        print("Linux Version:", linuxVersion)
        
        print("Extract Complete!")

前言

最近在逛某宝的“老王电子数码DIY"库存品店,发现了一个6块6毛钱的随身WIFI,觉得挺便宜的就购入了,到手之后做了一些很简单的分析,发现其可以解锁并且正常使用,以下为分析和解锁的流程。

初步

以下为刚刚到手的样子,很明显是某出境随身WIFI租赁公司租赁韩国SKT运营商的产品。
收到时候没有电池,不过这个设备支持完全USB供电,仅需5V2A即可。
URoadUnl_Preview
翻到背面,可以看到型号为LFM300R和SN,IMEI等信息
URoadUnl_Infos
通过型号,成功通过网页时光机找到了这个机器的官方指南
URoadUnl_OfficialDoc
在这里请留意你的设备IMEI的最后四位数和IMEI上方的SN,之后解锁流程中需要使用

初探

直接插入我们自己的SIM卡,由于店家在商品介绍里面就有写是网络锁,所以肯定是不能直接使用的。
URoadUnl_SimErrLight
设备上的指示灯也显示是告警状态
然后我们通过http://192.168.1.1来登录到用户面板

用户面板用户名为user,密码为左侧贴纸上的Wi-Fi PSK密码

进入用户面板之后可以在右上方点击ENG切换到英文界面
URoadUnl_SimErrWeb
在这里我们可以看到已经读卡,但是仅限于读到了ICCID,网络状态为Inactive无法使用

管理员面板

在官方指南里面其实有提到如何进入管理员面板,但是并未提到里面的详细设定,仅仅表明了可以手动安装系统更新,漏洞就出现在这里
我们只需要一点点后壳上的信息即可进入管理员面板

http://192.168.1.1:4XXXX (XXXX为你设备的IMEI最后四位数)
管理员面板用户名为admin,密码为IMEI上方的SN序列号

URoadUnl_Login
在上方栏中找到Basic -> Account & System Setting -> Manual Update
URoadUnl_AdminPanelUpdate
选择更新包后即可开始更新
URoadUnl_Updating
更新完成后会提示以下界面,等待设备重新启动即可更新到2017年8月编译的最新版本
URoadUnl_UpdateOk

解锁

在管理员面板中稍微做了下发掘,就找到了网络锁解锁选项,我觉得可能韩国人根本没有想好好锁这个机器吧
URoadUnl_SimSettings
修改以下两个选项
LTE -> LTE Management -> Ignore USIM 改为Enable
LTE -> LTE Management -> Connection Setting 改为Automatic
选项需要一个一个进行修改,改完一个之后需要按一次Apply然后等待系统重启后再修改下一个选项
URoadUnl_Restart
执行完成这两个修改之后这个设备的网络锁即告解除

设定APN

继续进入管理员面板
URoadUnl_AuthSettings
找到LTE -> Authentication Setting -> APN Profile
即可新增自己的APN并且应用了,应用之后重启再次进入管理员面板,即可看到已经连上了数据网络
URoadUnl_LTEInfo

LTE调试

这个设备也可以看到很详细的LTE基站/频点等信息
在LTE -> LTE Debug Screen点选ON即可看到
URoadUnl_LTEDebug.png

5Ghz Wi-Fi

这个设备也支持5Ghz频段,但是只可双频单发,可以让Wi-Fi协商速度到300Mbps
可在Wireless -> Basic Setting -> Frequency中改为5Ghz,也可以修改是否开启省电模式等设置
URoadUnl_5GWifi

配置

SoC: GCT GDM7243M 单核1Ghz ARMv7
RAM: 256MB
WLAN: Marvell SD8897 SDIO总线
LTE频段: FDD-LTE B3/B5(LTE Cat.4)
隐藏频段: FDD-LTE B4/B13
AP系统: Linux 3.10.38 uClibc构建
BP系统: T-KERNEL (ITRON)
支持作为USB RNDIS网卡

下载

官方文档(韩文):OV6NZ5_URoad-LFM300_Upgrade_Manual.pdf
LFM300 V13011版本固件:OV6NZ5_URoad-LFM300_SKT_WEB_v13011.enc
LFM300R V12811版本固件:OV6NZ5_URoad-LFM300R_SKT_WEB_v12811.enc
手机APP(无法兼容新的Android版本):URoad_LFM300_1.3.0.apk


Description

There's no further technical introduction needed; those who understand this code will naturally know its use.

import struct

def makeKcjPropQuery(src, buffer):
    totalSize = len(src) + len(buffer) + 14
    dataBLK = bytearray(totalSize)
    struct.pack_into('I', dataBLK, 0, 547486795)
    dataBLK[4] = 1
    struct.pack_into('H', dataBLK, 5, len(src))
    struct.pack_into('H', dataBLK, 7, len(buffer))
    dataBLK[9:9+len(src)] = src.encode()
    dataBLK[9+len(src):9+len(src)+len(buffer)] = buffer
    dataBLK[-1] = 0x7E
    return dataBLK.hex()

kcjPropName = "rw_eut_su_time"
buffer = bytearray([0] * 1)
hexData= makeKcjPropQuery(kcjPropName, buffer[1:])
print("Query Command for " + kcjPropName + " is " + hexData)