全站升级:正式全面启用 HTTP/3(QUIC)
从今天起,我的博客全站(包括所有子域名)已经全面迁移至 HTTP/3(基于 QUIC 协议)!
HTTP/1.1 陪伴了我整个程序开发生涯的起步;HTTP/2 把我带入了TLS的时代;而现在,HTTP/3 则将为更远的协议演进提供了更好的支持。
谨以此文,纪念一次技术演进的足迹。
Powered by Nginx/1.28.0
Nothing Here...
从今天起,我的博客全站(包括所有子域名)已经全面迁移至 HTTP/3(基于 QUIC 协议)!
HTTP/1.1 陪伴了我整个程序开发生涯的起步;HTTP/2 把我带入了TLS的时代;而现在,HTTP/3 则将为更远的协议演进提供了更好的支持。
谨以此文,纪念一次技术演进的足迹。
Powered by Nginx/1.28.0
在雅虎拍卖上捡老日机的垃圾,不可避免的会捡到一些有Felica锁定的机器,虽然只影响NFC一个功能不可用,但是还是会觉得很膈应,而且因为其Felica的NV是单独存储的,所以恢复出厂设置并不影响Felica锁定。
最近就收到了一台有Felica锁定的富士通F-01F,在逆向其Felica锁定管理器的时候,发现了一个神奇的供店头使用的解锁后门。故写一下这篇分析梗概,以供使用。
锁定状态,NFC完全不可用
从设备内提取了管理Felica锁定的APK
整个APK拖入JADX,一眼就看到了一个Felica_Reset_Lock函数
感谢JADX的强大反编译能力,一下就看到了一个可读性很高的解锁码校验函数
大概读了一下就发现这只是个简单的数字替换算法(标准的日本Security through obscurity思想代码)
立即可用的JS移植版本网页版实现:App/FjFelicaUnl
现在有解锁码的生成方法之后,需要找到在哪里输入,以及有什么先决条件。
接着随着Felica_Reset_Lock函数的调用情况,追到了其MainActivity,果然发现其必须在开机后120秒内在指定输入框内输入有前缀和后缀的解锁码才行。
所以在生成解锁码后,我们需要重启
然后立即在指定输入框输入解锁码
立即出现“解除しました。”!!
再打开NFC/おサイフケータイ設定,果然变成了全新状态!
今天帮朋友在分析索尼电脑的恢复镜像,其使用一种奇怪的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)
YuWSE(Yu Web Synth Engine)是一个简单的封装了AudioContext声音合成器引擎的前端库,发源于MzkBtn小软件和ToneCAPTCHA引擎中的声音合成部分,旨在方便开发者以最简单的方式用非采样和仅代码方式产生有效的声音。
且同时包括了部分前端功能实现。其中包括了波形显示器,五线谱绘制,简五谱互转,触摸钢琴键盘扫描,节拍器显示,简易和复杂音序器(YUUWSEJSONV1/V2),预置音色库等功能。
最早的玩具项目,也是最早版本的合成引擎,最早实现了波形显示
体验版
基于音高的全自动区分计算机和人类的图灵测试
第二版本的合成引擎,加入了基于Web Canvas图形的即时五线谱绘制功能和简谱五线谱输入转换功能
文章
体验版
第三版本的合成引擎和前端实现,新增触摸钢琴键盘和音序器功能
体验版
简易的网页节拍器,基于LePad的Lite分支
体验版
Babi Sound Generator
全新的独立分支,用于测试捏音色这个新功能,当前可以产生猪叫声
B.S.G