分类 默认分类 下的文章

起因

前段时间手痒,看到了跟了自己很久的绿联读卡器,想折腾折腾。但是又不知道它的详细硬件情况,于是就在网上搜常见的USB3读卡器方案商,逐个尝试固件更新工具,然后成功使用了创惟科技的固件升级工具读到了版本号确定了其是使用了GL3224主控。便接下来了开始的作死流程:首先直接点击了刷入,1536版本固件是随官方固件更新随工具附带的,刷入后自然而然也没有任何问题,然而我开始在互联网上寻找是否有更新的固件。而后在数码之家的一篇帖子中找到了随一款新的读卡器附带的1539版本,立即来了兴趣,就花了家元下载。下载后发现原作者提供的是FLASH的直接Dump,与官方工具内的固件结构和大小都不相同,于是选择了照猫画虎的办法,依照已有的1536版本官方固件进行裁剪,裁剪后直接就进行了刷入。刷入了之后,设备仅能认出是USB大容量存储设备,没有了分区磁盘设备,插入SD卡后也没了反应,打开官方的更新工具也直接提示无设备(但在此处可以看到是1539的固件版本,应该是校验错误导致的不启动)。
GLF_Error1539
GLF_NoDeviceUpdate

“修复”

在弄坏了这个读卡器之后,因为读卡器这类东西价格并不是很高,所以立即就重新购买了新的,这个坏的则被放在了一边。
今天突然想起来还有这么一个事情,便尝试开始了修复。
首先,在创惟科技官方网站上找到了这个读卡器芯片的Block Diagram
GL3224_QFN48_BLK_DGM
发现其由于支持低成本设计,FLASH并不是强制要求,其晶圆上还有内部ROM
现在我们的目的就是让内部ROM上的版本固件跑起来,从而先恢复正常启动的功能,而后再继续尝试刷入固件到SPI闪存。

“破坏”

这个读卡器的外壳使用的是超声波焊接工艺,没有螺丝也没有可以下翘片的地方,按照网络上的教程,拆解超声波焊接的设备只需使用毛巾裹住,然后向地上砸去即可。我也照做了,的确拆出了主板,但是忘记了这个读卡器买了很久了,塑料有些脆化,当我打开毛巾之时,外壳已经碎裂成了很多半。
GLF_BrokenCase
不过最终还是成功得到了主板,主板也很简单,一片VSOP封装的SPI闪存,一片GL3224主控,一粒晶振以及两个卡连接器就构成了整体设计
GLF_RawBoard
在SPI闪存中,依据个人经验,短接CS(片选信号)脚和DO(输出信号)脚即可暂时让主控读不到SPI
GLF_FlashPin
在当前情况下,我们这样短接,再插入电脑,即可使用ROM版本的固件来启动了
GLF_SpiShort
果然成功启动!
GLF_RomBootDevice
再通过一样的方法检查固件版本,发现其为1532版,因此可以确定其为ROM版本固件!
GLF_RomBootComplete
此时我们再使用官方的固件更新工具,可以看到其成功识别到了设备,且可以执行更新!
GLF_1532to1536
执行更新,等待提示成功,此时拔下再连接,读卡器完全恢复了正常!
GLF_1532to1536_Success

“不死心”

当然,对于没有成功更新到1539版本,我还是耿耿于怀。于是再一次尝试对FLASH Dump版本的认真裁剪,成功的将其升级到了真正的1539版本,且可以直接通过固件更新工具更新。
GLF_1536to1539

下载

创惟科技(GENSYS) GL3224的固件更新工具和两个版本的固件,有1536和1539两个版本可供选择
GL3224_Update.7z

版权

此頁面中的Block Diagram圖片及韌體和韌體更新工具均為創惟科技股份有限公司版權所有
此頁面中的SPI閃存Pin Diagram圖片由華邦電子股份有限公司版權所有,來自W25Q80DV晶片July,2015版本的Datasheet

介绍

最近总有需求表示需要通过电脑给一些支持网络调试的设备安装软件
虽然每次我都建议直接使用platform-tools里面的ADB工具,但显然并不是所有人都有类似的能力来操作命令行。
所以快速的使用批处理脚本写了个小型安装器,只需要简单的输入和拖拽即可安装APK。
当前版本基于Platform-Tools r34

截图

EAI_Start
EAI_Main

版权

Platform-Tools来自Google,Inc

下载

下载解压后打开eai.bat即可使用
下载

最近有切换网盘程序的需求,本来使用的是Cloudreve,其文件夹结构和文件名都与上传时候有颇多差异。
写了这个小工具来帮助解决,敬请使用。

#!/bin/bash

CLEAN_EMPTY=0
CLEAN_THUMB=0
CLEAN_FILENAME=0

while [ "$#" -gt 0 ]; do
    case $1 in
        --clsempty) CLEAN_EMPTY=1 ;;
        --clsthumb) CLEAN_THUMB=1 ;;
        --clsfilename) CLEAN_FILENAME=1 ;;
        *) break ;;
    esac
    shift
done

if [ "$#" -ne 1 ]; then
    echo "Cloudreve Cleaner 1.0"
    echo "Usage: ./clscrve.sh [--clsempty] [--clsthumb] [--clsfilename] <directory>"
    exit 1
fi

DIR=$1

if [ ! -d "$DIR" ]; then
    echo "Directory not found: $DIR"
    exit 1
fi

if [ "$CLEAN_FILENAME" -eq 1 ]; then
    echo "Cleaning filenames..."
    find "$DIR" -type f | while read -r file; do
        dir=$(dirname "$file")
        base=$(basename "$file")
        originname=$(echo "$base" | awk -F'_' '{ $1=$2=""; sub(/^  /,""); print }')
        if [ "$base" == "$originname" ]; then
            continue
        fi
        mv -n "$file" "$dir/$originname"
        echo "${base} -> ${originname}"
    done
fi

if [ "$CLEAN_EMPTY" -eq 1 ]; then
    echo "Cleaning empty directories..."
    find "$DIR" -type d -empty -delete
fi

if [ "$CLEAN_THUMB" -eq 1 ]; then
    echo "Cleaning thumbnail files..."
    find "$DIR" -type f -name '*. thumb' -delete
fi

echo "SUCCESS IN $(date '+%Y-%m-%d %H:%M:%S')"

前言

朋友的网站DemoDB近来被刷评分的问题所困扰,导致其排行榜内容失真。在一天的无聊讨论之中,我突然想到可以基于网站本身的特色来制作一个特别的验证码——音高验证码,即符合了网站本身的主题,又达到了防护的效果,无障碍的标准也得到了满足。

一般原理

利用了硬编码的频率值,使得可以使用JavaScript的AudioContext来基于随机Challenge产生音频并且保存Challenge的值,而后基于Challenge值使用HTML Canvas图形来绘制五线谱和音符,再基于前面保存的Challenge与前台用户输入的Challenge作比较来判断用户状态。

测试样本

可在线体验
Tone CAPTCHA Demo
ToneCAPTCHA_Demo

采用实绩

DemoDB的示范曲评分功能

版本更新

1.5
Jan.05-2024
添加ADSR包络支持以优化听感
1.4
Jan.04-2024
合入来自dgy18787的音符显示错误修复补丁
修改调试日志展示
添加标准音播放功能
1.3
Dec.17-2023
优化Demo界面UI
1.2
Dec.10-2023
添加加线功能
添加简谱输入转换器功能
修复音符漂移问题
1.1
Dec.09-2023
首次发布

前言

最近在一个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(虽然其已在法理上消失)