提示

本文章中所提供的方法仅供开发者调试使用,其使用后产生的任何法律和硬件损坏等后果与本人和马来西亚电讯公司(Telekom Malaysia Berhad)无关。

Alert

The methods provided in this article are intended for developer debugging purposes only. Any legal consequences and hardware damages arising from their use are not the responsibility of the author and Telekom Malaysia Berhad.

Amaran

Kaedah yang disediakan dalam artikel ini hanya untuk tujuan penyahpepijatan pembangun sahaja. Sebarang akibat undang-undang dan kerosakan perkakasan yang timbul daripada penggunaannya tidak menjadi tanggungjawab pengarang dan Telekom Malaysia Berhad.

警告

HP4300-TM型号于2024年11月推送的P2.0.5_20241101版本中,密码被SHA-256加密,导致无法直接取得密码,如果你有使用开发者选项功能的需求,请不要更新到这个版本。如果你更新到了这个版本,请联系我协助测试版本降级。
HP40A3于2024年7月更新到P3.1.4_20240311版本(Android 12)后,密码被修改,需额外处理。

前言

由于搬家到了新的屋子,新的屋子客厅里有个电视,有一天与好友聊到如何利用起这个电视,好友提到他那边有一个闲置的Unifi Plus Box,来自于办理TM家宽的附带产品,历经几日之奔波,这个电视盒子最终到了我手上。

加注

感谢某位网友提供的关键数据,这个分析流程在基于Android12的新版Unifi Plus Box(创维HP43A)也可用,但有可能会被其自带的远控更新设置后变为不可用状态,请恢复出厂设置后再尝试。

特殊

到手后,发现其采用的是Amlogic S905X2的SoC,有4核心A53,主频为1.8Ghz,2GB的运行内存和16GB的闪存,Android版本为9,在大多数电视盒子中都属于较高配置。
首先尝试执行系统更新,没想到居然接收到了更新到Android 10的大版本更新,对于我这种熟悉中国电视和电视盒子市场的人来说是颇为惊讶的,从来没有在这类型的设备上看到过有Android大版本更新。执行更新之后,发现了更多奇妙,于是则开始了以下的流程。

开发者选项密码?

正常情况下,如果想对这个盒子深挖,首要就是要打开USB调试,不管是无线还是有线调试。
然而当我尝试以正常方式打开开发者选项时,其弹出了一个与AOSP完全不同的密码输入界面。
UTVCK_Password1
当然目前是不知道实际的密码是什么的,其为什么会有个密码也不是很明确,也无法进行后续的分析了。

获取系统包

UTVCK_GetBFg
后来一天与另一位朋友闲聊,他跟我说他逆向了Google的OTA分发系统的API,问我有没有需求来测试一下,我就想到这个盒子系统更新时候的界面十分原生,其内置的阉割版本Google Play商店也可以下载到AIDA64来获取到当前软件版本的编译指纹,于是我就把编译指纹发给了这位朋友,于是得到了以下结果。

{
    "code": 200,
    "message": "success",
    "data": {
        "title": "New software version is now available. ",
        "url": "https://android.googleapis.com/packages/ota-api/package/xxxxxxxxxxxxxxxxxxxxxx.zip",
        "size": "803.6 MB",
        "size_num": 842585223,
        "fail": "Upgrade failed!",
        "inst": "Upgrade successfully!",
        "desc": "<b>A new software update is available.</b> <br>1. Fix issue Error code 01999 of unifi tv application <br> 2. Update Android TV security patch<br> <br><b>Note: The update may take up to 10 minutes to complete. You will not be able to use the TV during the update. Thank you for your patience.</b> <br>"
    }
}

(请私信我以取得完整链接以作他用)
成功拿到了完整的系统更新包!!

逆向TvSettings

首先我们打开获取到的ZIP包,依据个人经验,这类密码一般存在于系统设置之本体。
UTVCK_InsideOfPkg
所以首先从SYSTEM分区镜像中找到TvSettings的apk
UTVCK_GetTvSettings
找到之后,我们需要一个特征来找到这个密码部分
尝试在密码输入框内胡乱输入,其输出了一个有“Password is error!!”的Toast信息,我们就以此入手
UTVCK_PasswordErr
在Jadx中以字符串搜索Password is error这个字符串,很快得到了有效回应
UTVCK_JadxSearchStr
打开这个代码段后,得益于Jadx强大的反编译能力,我们可以得到非常清晰的程序逻辑,分析这些判断语句,很容易即可得到密码。
输入进密码输入框,开发者选项成功被启用!
UTVCK_DevSettingsPass

USB调试密码?

但是当尝试打开USB调试时,其又出现了跟前面一样密码输入框,输入上一个判定中得到的密码,又一次提示密码错误。
尝试找到了开发者模式中USB调试开关的逻辑,会发现有一个额外的source判定
再混合前面所提到的逻辑语句得到的字符串密码,成功打开了USB调试!
UTVCK_ADBAuthPass

后记

以上则为这个电视盒子如何打开开发者选项和USB调试的办法了
其实历经以上分析,发现这个盒子还有个很特别的点,其几乎用了可谓是”围追堵截“的办法来屏蔽Netflix,关于这个事情如何解决,请等待我的后续更新了!

前言

既然本篇文的题目是古早味软件合成器,那么必定不会涉及到VSTi等现代插件技术来实现的软件合成器,取而代之的则是Windows9x系统中常见的“虚拟设备驱动程序"(VxD),其对环境的限制也更加苛刻,运行起来也需要更多的准备。
在本文中,我将使用卡西欧于1996年所发布的SW-10软件合成器作为范例进行演示。
CASIO SW-10合成器是附带于CASIO LANA Lite卡拉OK软件(在互联网速度不太优秀的时代,基于MIDI的卡拉OK软件和网站非常的多)之中的赠品,所以自然而然,合成器部分的获取也是免费的,刚好符合了本文的标题“免费”和“古早味”这两个关键字。

环境准备

需要一个Windows98或者Me的虚拟机或物理机
如果你使用虚拟机,目前仅可以使用VirtualBox
系统的安装过程在此不再赘述,推荐安装英文版或者日文版,避免遇到内码错误问题。
VTGSWS_Windows_Prepared

资源准备

由于源程序发布于1997年,其官网链接也早已不存,我们需要使用网页档案馆来获取安装包
LanaSw10.exe
在下载之后,可能会想到直接安装,但是依据个人经验,在非ShiftJIS内码的古早系统上直接安装日文程序,绝不是一个安全的事情。
所以我们选择直接解压其中的文件来获取到VxD驱动安装包
第一步,我们需要尝试知道一下LanaSw10.exe的格式
通过WinRAR方便的拓展,则立即知道了这是一个LZH压缩包
VTGSWS_LANA_Prop
在使用WinRAR解压之后,则得到了一堆文件,在其之中找到最大的文件以供解压
VTGSWS_LANA_3Z
但是直接解压肯定是报错的,所以在互联网尝试搜索,找到了大佬对卡西欧SW-10的全面逆向
M-HT/casio_sw-10
可以使用其中的zextractfile来解压出文件,但是其需要文件名才能解压
历尽一些简单的分析,发现这个Z文件将文件名存于尾部
通过cat指令则得出其中包括有以下文件
VTGSWS_Extract_File_Name
解压后得到
VTGSWS_Extracted

安装步骤

现在我们已经得到了文件,所以接下来的步骤十分简单
只需要进入Windows的控制面板,找到"Add New Hardware"
VTGSWS_ControlPanel
点选添加"Sound, Video and Game controllers"
VTGSWS_DeviceType
点选"Have Disk"
VTGSWS_HaveDisk
选择"OEMSETUP.INF"其后会出现设备选择框,选择"CASIO SW-10"
VTGSWS_ChooseSW10
之后则会弹出卡西欧的配置面板
VTGSWS_SW10Config
重启之后就可以使用任意支持MIDI播放的播放器(比如Windows Media Player)播放你想要听的MIDI文件了

概述

很久很久以前(大约20年7月)写的一个奇奇怪怪的算法,当时的目的是为了从一堆动漫图片中获取其线稿用于描画练习
但是当时还没有现如今流行的各种大模型,根本做不到快速的本地抽出,所以就去尝试了解了一下一些简单的图像处理库
(当时还在用7300HQ老U,做不到基于纯图形或者推测的处理),后来就想了个办法,用色容差和反色+灰度来解决这个问题,出来的效果也暂且不算难看,用于描画练习则是绰绰有余了。
最近翻出来了当时的V1和V2版本代码,仔细阅读后发现写的属实非常烂,就直接尝试对其进行了直接的整体重构
以下为代码,编译时请携带好STB的头,用clang -Os -o OSE3 OSE3.C -lm来执行编译

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

#define GRAYSCALE_RED_WEIGHT 0.3
#define GRAYSCALE_GREEN_WEIGHT 0.59
#define GRAYSCALE_BLUE_WEIGHT 0.11

void imgProcessGray(uint8_t *s, int sx, int sy, int stride, uint8_t *p, int gstride)
{
    for (int y=0; y<sy; y++) {
        for (int x=0; x<sx; x++) {
            p[x] = GRAYSCALE_RED_WEIGHT * s[x*3+0] + GRAYSCALE_GREEN_WEIGHT * s[x*3+1] + GRAYSCALE_BLUE_WEIGHT *s[x*3+2];
        }
        s += stride*3;
        p += gstride;
    }
}

void imgProcessDilate(uint8_t *s, int w, int h, int k, uint8_t *p)
{
    for (int y=1; y<h-1; y++) {
        for (int x=1; x<w-1; x++) {
            uint8_t uc = s[ (y+0)*w + (x+0) ];
            uint8_t ua = s[ (y-1)*w + (x+0) ];
            uint8_t ub = s[ (y+1)*w + (x+0) ];
            uint8_t ul = s[ (y+0)*w + (x-1) ];
            uint8_t ur = s[ (y+0)*w + (x+1) ];
            uint8_t ux = 0;
            if (uc > ux) ux = uc;
            if (ua > ux) ux = ua;
            if (ub > ux) ux = ub;
            if (ul > ux) ux = ul;
            if (ur > ux) ux = ur;
            p[ y*w + x ] = ux;
        }
    }
}

void imgProcessAbsdiff(uint8_t *s, uint8_t *s2, int w, int h, uint8_t *p)
{
    for (int n=0; n<w*h; n++) {
        *p++ = abs(*s++ - *s2++);
    }
}

void imgProcessReverse(uint8_t *s, int w, int h, uint8_t *p)
{
    for (int n=0; n<w*h; n++) {
        *p++ = 255 - *s++;
    }
}

int main(int argc, char* argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Otaku Strokes Extractor v3\n");
        fprintf(stderr, "Usage: %s <image_path>\n", argv[0]);
        return 1;
    }

    char *name = argv[1];

    int w, h, bpp;
    uint8_t *pixels = stbi_load(name, &w, &h, &bpp, 3);
    if (!pixels) {
        fprintf(stderr, "Failed to load image: %s\n", name);
        return 1;
    }

    uint8_t *gray = (uint8_t*) malloc(w*h*4);
    if (!gray) {
        fprintf(stderr, "Failed to allocate memory for image processing.\n");
        stbi_image_free(pixels);
        return 1;
    }

    uint8_t *dilated = gray + w*h;
    uint8_t *diff = dilated + w*h;
    uint8_t *contour = diff + w*h;

    printf("Processing %s...\n", name);
    imgProcessGray(pixels, w, h, w, gray, w);
    imgProcessDilate(gray, w, h, 5, dilated);
    imgProcessAbsdiff(gray, dilated, w, h, diff);
    imgProcessReverse(diff, w, h, contour);
    stbi_write_jpg("result.jpg", w, h, 1, contour, 0);
    printf("Done for %s !\n", name);

    free(gray);
    stbi_image_free(pixels);

    return 0;
}

Windows体验版(使用MSYS2的gcc 11.3.0链静态构建)
OSE3_x64.exe

效果

原图
OSE3_Original
线稿
OSE3_Result
©版权所有者:クール教信者·双葉社/ドラゴン生活向上委員会

利用OpenCV的色容差检测功能,做到了马来西亚国旗——辉煌条纹(Jalur Gemilang)的检测
当前功能仅有检测到Jalur Gemilang时,冻结那一帧,之后可能会再添加一些2333的功能。
由于该程序目标为马来西亚人,所以提示语也采用了马来文🤣

实验效果

OPENCV_PJG

# pjg.py V3
import cv2
import numpy as np

lower_red1 = np.array([0, 100, 100])
upper_red1 = np.array([10, 255, 255])

lower_red2 = np.array([160, 100, 100])
upper_red2 = np.array([180, 255, 255])

lower_white = np.array([0, 0, 200])
upper_white = np.array([180, 30, 255])

lower_yellow = np.array([20, 100, 100])
upper_yellow = np.array([30, 255, 255])

lower_blue = np.array([100, 100, 100])
upper_blue = np.array([130, 255, 255])


def dF(frame):
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    mask_red = cv2.inRange(hsv, lower_red1, upper_red1)
    mask_red2 = cv2.inRange(hsv, lower_red2, upper_red2)
    mask_white = cv2.inRange(hsv, lower_white, upper_white)
    mask_blue = cv2.inRange(hsv, lower_blue, upper_blue)
    mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow)
    mask = cv2.bitwise_or(mask_red, mask_red2, mask_white)
    mask = cv2.bitwise_or(mask, mask_blue)
    mask = cv2.bitwise_or(mask, mask_yellow)
    if cv2.countNonZero(mask_red) > 100 and cv2.countNonZero(mask_white) > 100 and cv2.countNonZero(mask_blue) > 100 and cv2.countNonZero(mask_yellow) > 100:
        return True


cap = cv2.VideoCapture(0)
print("Pengesan Jalur Gemilang\nVersi Aplikasi: V3\nMasukkan 'q' untuk keluar\n")
while True:
    ret, frame = cap.read()
    if dF(frame):
        print('Unsur Jalur Gemilang dikesankan!!!')
        cv2.waitKey(0)

    cv2.imshow('Pengesan Jalur Gemilang', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

    if cv2.getWindowProperty('Pengesan Jalur Gemilang', cv2.WND_PROP_VISIBLE) < 1:
        cap.release()
        cv2.destroyAllWindows()
        break

很久以前接触到日系手机内核里面各式各样的LSM的时候,就对LSM背后到底是什么,产生了一些奇怪的兴趣。
最近有些得空,读了一些关于LSM,钩子,内核相关的东西,再搭上以前对FJSEC这些OEM LSM的逆向的经验。
尝试胡乱弄个LSM出来看看?也许有用也也许没用,又成为了一个慢慢挖坑再填坑的作品。
为啥叫NGLSM?NG是我很早用的一个网名,全称是Nango,不懂当初为什么会想到这个名字,但是既然想到了,那么就这样叫吧。

// 2023.06.08-00.00.01/FIRST RELEASE
// 2023.06.11-00.01.01/ADD PROTECT OF SETSOCKOPT/GETSOCKOPT
// 2025.03.16-00.02.01/ADD PROTECT OF /DATA/LOCAL/TMP FOLDER
#include <linux/lsm_hooks.h>
#include <linux/binfmts.h>
#include <linux/path.h>
#include <linux/cred.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/mount.h>
#include <linux/bprm.h>
#include <linux/socket.h>
#include <linux/seq_file.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/kernel.h>
#include <linux/string.h>

#define STATUS_FILE          "/data/security/lsm/status"
#define STATUS_PERMISSIVE    "PERMISSIVE"
#define STATUS_ENFORCING     "ENFORCING"
#define STATUS_MAX           100
#define TASK_FLAG_RESTRICTED (1 << 30)

static inline bool is_restricted_task(struct task_struct *task)
{
    return (task->flags & TASK_FLAG_RESTRICTED) != 0;
}


static int nglsm_status(void)
{
    struct file *f;
    char buf[STATUS_MAX];
    loff_t pos = 0;
    int status = -1;
    ssize_t ret;

    f = filp_open(STATUS_FILE, O_RDONLY, 0);
    if (IS_ERR(f)) {
        pr_err("[NGLSM] STATUS FILE CAN'T ACCESSABLE: %s\n", STATUS_FILE);
        return status;
    }

    ret = kernel_read(f, buf, STATUS_MAX - 1, &pos);
    if (ret < 0) {
        pr_err("[NGLSM] STATUS FILE CAN'T ACCESSABLE: %s\n", STATUS_FILE);
        filp_close(f, NULL);
        return status;
    }
    buf[ret] = '\0';

    if (strncmp(buf, STATUS_PERMISSIVE, strlen(STATUS_PERMISSIVE)) == 0)
        status = 0;
    else if (strncmp(buf, STATUS_ENFORCING, strlen(STATUS_ENFORCING)) == 0)
        status = 1;

    filp_close(f, NULL);
    return status;
}

static int nglsm_bprm_check(struct linux_binprm *bprm)
{
    const char *path = bprm->filename;
    size_t len = strlen("/data/local/tmp/");
    if (path && (strncmp(path, "/data/local/tmp/", len) == 0)) {
        pr_info("[NGLSM] TEMPORARY EXECUTABLE FILE RESTRICTED: %s\n", path);
        current->flags |= TASK_FLAG_RESTRICTED;
    }
    return 0;
}

static int nglsm_check(void)
{
    int global_status = nglsm_status();
    if (global_status == 0 && !is_restricted_task(current))
        return 0;
    return -EPERM;
}

static int nglsm_sb_mount(const char *dev_name, struct path *path,
                            const char *type, unsigned long flags, void *data)
{
    if (nglsm_check() == 0)
        return 0;
    pr_err("[NGLSM] MOUNT RESTRICTED: global_status=%d, restricted=%d\n",
           nglsm_status(), is_restricted_task(current));
    return -EPERM;
}

static int nglsm_sb_umount(struct vfsmount *mnt, int flags)
{
    if (nglsm_check() == 0)
        return 0;
    pr_err("[NGLSM] UMOUNT RESTRICTED: global_status=%d, restricted=%d\n",
           nglsm_status(), is_restricted_task(current));
    return -EPERM;
}

static int nglsm_sb_pivotroot(struct path *old_path, struct path *new_path)
{
    if (nglsm_check() == 0)
        return 0;
    pr_err("[NGLSM] PIVOTROOT RESTRICTED: global_status=%d, restricted=%d\n",
           nglsm_status(), is_restricted_task(current));
    return -EPERM;
}

static int nglsm_socket_getsockopt(struct socket *sock, int level, int optname,
                                     char __user *optval, int __user *optlen)
{
    if (nglsm_check() == 0)
        return 0;
    pr_err("[NGLSM] GETSOCKOPT RESTRICTED: global_status=%d, restricted=%d\n",
           nglsm_status(), is_restricted_task(current));
    return -EPERM;
}

static int nglsm_socket_setsockopt(struct socket *sock, int level, int optname,
                                     char __user *optval, unsigned int optlen)
{
    if (nglsm_check() == 0)
        return 0;
    pr_err("[NGLSM] SETSOCKOPT RESTRICTED: global_status=%d, restricted=%d\n",
           nglsm_status(), is_restricted_task(current));
    return -EPERM;
}

static int lsm_status_proc_show(struct seq_file *m, void *v)
{
    int status = nglsm_status();
    switch (status) {
    case 0:
        seq_printf(m, "PERMISSIVE\n");
        break;
    case 1:
        seq_printf(m, "ENFORCING\n");
        break;
    default:
        seq_printf(m, "UNKNOWN\n");
    }
    return 0;
}

static int lsm_status_proc_open(struct inode *inode, struct file *file)
{
    return single_open(file, lsm_status_proc_show, NULL);
}

static const struct file_operations lsm_status_proc_fops = {
    .owner   = THIS_MODULE,
    .open    = lsm_status_proc_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = single_release,
};

static struct security_hook_list nglsm_hooks[] __lsm_ro_after_init = {
    LSM_HOOK_INIT(bprm_check, nglsm_bprm_check),
    LSM_HOOK_INIT(sb_mount, nglsm_sb_mount),
    LSM_HOOK_INIT(sb_umount, nglsm_sb_umount),
    LSM_HOOK_INIT(sb_pivotroot, nglsm_sb_pivotroot),
    LSM_HOOK_INIT(socket_getsockopt, nglsm_socket_getsockopt),
    LSM_HOOK_INIT(socket_setsockopt, nglsm_socket_setsockopt),
};

static int __init nglsm_init(void)
{
    if (!proc_create("lsm_status", 0444, NULL, &lsm_status_proc_fops)) {
        pr_err("[NGLSM] PROC ACCESS FAILED\n");
        return -ENOMEM;
    }

    security_add_hooks(nglsm_hooks, ARRAY_SIZE(nglsm_hooks), "nglsm");
    pr_info("[NGLSM] NGLSM HOOKED\n");
    return 0;
}

security_initcall(nglsm_init);