调用monalisa.wasm获取字幕解密key

1221天前 · 分享 · 1839次阅读

说点什么

复现某网站字幕加密的wasm分析过程,并使用python完成对wasm的调用以获取字幕解密key

之前其实有写一篇wasm调用的文章,但是没有公开

既然现在有一篇现成的,那不如就拿它做个演示~

环境和工具

  • chrome-standalone-v91.zip 最新免安装版Chrome

注意,文章中的代码均为完整代码的片段

过程

首先打开网址,观察一下它的请求

可以看到wasm和字幕是挨着加载的

这个时候,推荐使用polyv加密key解密过程定位教程里提到的方法来定位加密字幕的解密位置

关键点追踪异常定位法搭配使用

首先点开字幕请求的Initiator,点开第一个位置,在这里后一行下断点,刷新网页

然后会停在这里,点下一步

可以看到这里的一些东西就是字幕请求结果的东西

然后在e.changeSuccess()下断点,跳到这里

显然这里就是加密字幕内容了

现在是时候展现异常定位法的效果了,通过下面的代码将加密的内容设置为连续0的字符串

e._data.forEach((item) => {console.log(item.sub = "00000000000000000000000000000000000000000000000000000000000000000000000000")});

去掉断点,让代码接着运行

很好!现在有报错提示了,如果没有可能是当前播放的时间点没有字幕要显示,自行拉一下进度条

直接点过去看看

在这个位置下断点,很快会停在这里,这个时候看不出来解密位置,不慌跟着调用栈看看

哗的一下,很快啊,就定位到关键位置了

d是修改后的“加密字幕内容”内容,经过函数s后就出现了看到的异常提示,显然它是在解密

在函数s这里下断点,继续运行代码,停在断点这里之后,单步跟进去

ccall,那是wasm解密没跑了,这个关键词是浏览器使用wasm的一个标志性关键词

浏览器上JavaScript和WebAssembly具体怎么交互,这里就不详细展开了,请自行查阅MDN的文档

这里主要说下js调用wasm的几个关键流程

  • 首先开辟一块内存

    wasmMemory = new WebAssembly.Memory({
        "initial": INITIAL_INITIAL_MEMORY / WASM_PAGE_SIZE,
        "maximum": 2147483648 / WASM_PAGE_SIZE
    })
  • 初始化缓冲区和视图

    function updateGlobalBufferAndViews(buf) {
        buffer = buf;
        Module["HEAP8"] = HEAP8 = new Int8Array(buf);
        Module["HEAP16"] = HEAP16 = new Int16Array(buf);
        Module["HEAP32"] = HEAP32 = new Int32Array(buf);
        Module["HEAPU8"] = HEAPU8 = new Uint8Array(buf);
        Module["HEAPU16"] = HEAPU16 = new Uint16Array(buf);
        Module["HEAPU32"] = HEAPU32 = new Uint32Array(buf);
        Module["HEAPF32"] = HEAPF32 = new Float32Array(buf);
        Module["HEAPF64"] = HEAPF64 = new Float64Array(buf)
    }
    updateGlobalBufferAndViews(buffer);
  • 将动态基址写入特定位置

    HEAP32[DYNAMICTOP_PTR >> 2] = DYNAMIC_BASE;
  • 定义一些外部函数,即wasm内调用js这边的函数,用处很多,比如交换数据,检查环境等等
  • 将外部函数、内存等作为一个对象传递给WebAssembly.instantiate,完成初始化

    var asmLibraryArg = {
        "a": ___sys_fcntl64,
        "d": ___sys_ioctl,
        "e": ___sys_open,
        "f": ___sys_rmdir,
        "g": ___sys_unlink,
        "h": _clock,
        "l": _emscripten_memcpy_big,
        "m": _emscripten_resize_heap,
        "j": _emscripten_run_script,
        "r": _emscripten_run_script_int,
        "q": _emscripten_run_script_string,
        "n": _environ_get,
        "o": _environ_sizes_get,
        "c": _fd_close,
        "p": _fd_read,
        "k": _fd_seek,
        "b": _fd_write,
        "memory": wasmMemory,
        "table": wasmTable,
        "i": _time
    };
    var info = {
        "a": asmLibraryArg
    };
  • 将wasm的导出函数和一些有意义函数名进行绑定,这一步可有可无

    var ___wasm_call_ctors = Module["___wasm_call_ctors"] = function() {
        return (___wasm_call_ctors = Module["___wasm_call_ctors"] = Module["asm"]["s"]).apply(null, arguments)
    };
    var _monalisa_set_canvas_id = Module["_monalisa_set_canvas_id"] = function() {
        return (_monalisa_set_canvas_id = Module["_monalisa_set_canvas_id"] = Module["asm"]["t"]).apply(null, arguments)
    };
  • 然后就是调用交互了

    function ccall(ident, returnType, argTypes, args, opts) {
        var toC = {
            "string": function(str) {
                var ret = 0;
                if (str !== null && str !== undefined && str !== 0) {
                    var len = (str.length << 2) + 1;
                    ret = stackAlloc(len);
                    stringToUTF8(str, ret, len)
                }
                return ret
            },
            "array": function(arr) {
                var ret = stackAlloc(arr.length);
                writeArrayToMemory(arr, ret);
                return ret
            }
        };
        function convertReturnValue(ret) {
            if (returnType === "string")
                return UTF8ToString(ret);
            if (returnType === "boolean")
                return Boolean(ret);
            return ret
        }
        var func = getCFunc(ident);
        var cArgs = [];
        var stack = 0;
        if (args) {
            for (var i = 0; i < args.length; i++) {
                var converter = toC[argTypes[i]];
                if (converter) {
                    if (stack === 0)
                        stack = stackSave();
                    cArgs[i] = converter(args[i])
                } else {
                    cArgs[i] = args[i]
                }
            }
        }
        var ret = func.apply(null, cArgs);
        ret = convertReturnValue(ret);
        if (stack !== 0)
            stackRestore(stack);
        return ret
    }

有了上述知识后,那么想用python调用,只要完成上面的部分过程即可

也就是下面这些问题

  • 内存给多大,怎么给?
  • python怎么定义导入函数?
  • 初始化参数准备好了怎么初始化?
  • 初始化完成了怎么调用导出函数?
  • 调用了函数怎么拿到返回结果?

好在已经有比较完善的python库可以完成上述过程,它就是wasmer-python

详细的案例可以参考

首先初始化一个存储器

store = Store(engine.JIT(Compiler))

接着载入wasm,因为需要修改wasm内容,所以这里读取的是wat,后面会讲

wasm = Path(r'iq_wasm/files/libmonalisa-v3.0.6-browser.wasm').read_bytes()
module = Module(store, wasm)

然后初始化内存

memory = Memory(store, MemoryType(256, 256, shared=False))

最后是生成导入对象,初始化实例

import_object = ImportObject()
import_object.register('a', _import_object)
asm = Instance(module, import_object=import_object)

现在测试一下正不正常

然而报错了,这是为什么呢,根据我的经验,似乎这是因为wasmer-python仍然有一些地方不完善

如果导入的对象中,有table,那就会这个报错,虽然提供了Table类型的函数,但是有一个变量一直有问题

基于这个原因,需要修改wasm,当然不是直接改wasm,而是修改对应的wat文件

用wabt进行转换

..\\..\\wabt\\bin\\wasm2wat.exe libmonalisa-v3.0.6-browser.wasm -o libmonalisa-v3.0.6-browser.wat

然后把table这一行改一下

(table $a.table (;0;) 48 48 funcref)

这样就不用导入这个了

对应的,现在用另外一种方式读取载入wasm

wasm = wat2wasm(Path(r'iq_wasm/files/libmonalisa-v3.0.6-browser.wat').read_text(encoding='utf-8'))

现在有两个关键点

  • 导入对象中的具体函数怎么写
  • 怎么调用导出函数才能和浏览器结果一样

先说导入对象的函数,以_environ_get为例

这是python中的写法,简单来说wasm和js交互的时候就是在改变其内存区域的数据,按对应的逻辑操作内存数据就行了

def _environ_get(self, __environ, environ_buf):
    print('call _environ_get', __environ, environ_buf)
    bufSize = 0
    strings = self.getEnvStrings()
    for index, string in enumerate(strings):
        ptr = environ_buf + bufSize
        self.memory.int32_view()[__environ + index * 4 >> 2] = ptr
        self.writeAsciiToMemory(string, ptr)
        bufSize += len(string) + 1
    return 0

def writeAsciiToMemory(self, string, buffer, dontAddNull: int = 0):
    for num in list(string.encode('utf-8')):
        self.memory.int8_view()[buffer >> 0] = num
        buffer += 1
    if dontAddNull == 0:
        self.memory.int8_view()[buffer >> 0] = 0

然后按下面这样用Function包裹函数,然后构建一个字典

实例化ImportObject后通过register完成导入对象的设置,最后进行wasm实例化

self.import_object = ImportObject()
self.import_object.register('a', _import_object)
self.asm = WasmerInstance(module, import_object=self.import_object)

Function后面的FunctionType怎么来的呢

这个可以通过查看wat代码对应,准备好wasm相关的工具wabt

先用wasm2wat将wasm转换为wat文件

这里可以看到导入函数的参数与结果类型,按这个去写就可以了

另外导入函数只要定义了就行,不一定需要具体返回什么

为了达到调用目的,还需要写一个python的ccall

先看看js这里怎么写的,翻译一份,ccall部分如下

def ccall(self, func_name: str, returnType: 'type', *args):
    def convertReturnValue(_ptr: int):
        if returnType == str:
            return self.UTF8ToString(_ptr)
        elif returnType == bool:
            return bool(returnType)
        return _ptr
    stack = 0
    _args = []
    for arg in args:
        if isinstance(arg, str):
            if stack == 0:
                stack = self.stackSave()
            max_write_length = (len(arg) << 2) + 1
            ptr = self.stackAlloc(max_write_length)
            self.stringToUTF8(arg, ptr, max_write_length)
            _args.append(ptr)
        elif isinstance(arg, list):
            ptr = self.stackAlloc(len(arg))
            ptr = self.writeArrayToMemory(arg, ptr)
            _args.append(ptr)
        else:
            _args.append(arg)
    ptr = self.export_configs[func_name](*_args)
    ret = convertReturnValue(ptr)
    if stack != 0:
        self.stackRestore(stack)
    return ret

其他子函数参见完整代码

现在就可以传递参数了,但在此之前需要知道浏览器初始化wasm的时候都做了哪些动作

所以先在所有的导出函数下断点,刷新调试,下图是一部分

导入部分的函数调用不下断点是因为可以通过python调用的时候看出来

刷新网页,等停在断点

可以看到依次调用了

  • ___wasm_call_ctors
  • _monalisa_context_alloc
  • stackSave

stackSave的时候是从ccall来的,那么这之前应该先调用上面两个函数

___wasm_call_ctors是直接调用的,且没有参数

_monalisa_context_alloc同上

那么初始化函数就这样写

def doinit(self):
    self.___wasm_call_ctors()
    self._monalisa_context_alloc()

然后是stackSave,这个时候根据调用栈,可以知道这里是在设置license

切换下调用栈,参数有4个

按下面的代码调用

def _moSetLicense(self):
    license = 'AIUACgMAAAAAAAAAAAQChgACATADhwAnAgAg3UBbUdVCWXAjkgoUgmICmHvomvZai0jGglWe+oaQC+MCAAAAA4gANwEAMHStDt3ZksUi3Q7ZpevUL0ce2NA73SNiqOJW3Wc0P6B62xYg8yiWFF92KXEGjljOeQEAAgD/iQAkAQAAIKgivL0LDGFSYlwcToEO6LCRYFZjE3lycoiZDPxiNXFo'
    ret = self.ccall('monalisa_set_license', int, self._ctx, license, len(license), '0')
    print('_moSetLicense', ret)

下一个是_monalisa_set_canvas_id,这里参数也比较明了

def _moSetCanvasId(self):
    _canvasId = 'subtitlesubtitle'
    ret = self.ccall('_monalisa_set_canvas_id', int, self._ctx, _canvasId, len(_canvasId))
    print('_moSetCanvasId', ret)

同理,接着是_monalisa_version_get

def _moGetVersion(self):
    ret = self.export_configs['_monalisa_version_get']()
    print('_moGetVersion', ret)

然后是monalisa_get_line_number

def get_line_number(self):
    enc_text = 'F6vjGQAlaXWf/rYLFkGHQLok+aI/X+7MBJI6EmzYtZ4='
    ret = self.ccall('monalisa_get_line_number', int, self._ctx, enc_text, len(enc_text), '(?:<br\\/>|\\n)')
    print('get_line_number', ret)

然后跑python代码出了异常

这是因为在_emscripten_run_script_int这里没有给返回结果

分析js确定这里实际上返回的是解密后字幕的行数,一般都是1,直接返回1就行

def _emscripten_run_script_int(self, param_0):
    text = self.UTF8ToString(param_0)
    # 原函数这里是运行一段js代码 这里是python 所以运行不了
    # 按执行对应的结果给返回即可 必须这里是根据<br>返回字幕行数的
    # eval(text)
    print('call _emscripten_run_script_int', text)
    return 1

然后是调用_monalisa_set_canvas_font,这个时候后面就没有必要继续模拟了

因为这个时候明文已经有了,所以达成了调用目的了

上面调用过程说的差不多了,那解密key在哪儿呢

首先参考渔哥的帖子,先把wasm编译为.o文件

这里用逍遥一仙的工具

然后拖入IDA

根据已知信息,可以知道w2c_f94的参数一就是解密的密钥

那到浏览器对应位置下断点看看,开始几轮没有,然后就有了

那解密key就是

  • 1dd28b0cb4ba5926ed75b9821d0235b4

monalisa_set_license后保存一下全部内存,搜索一下

可以看到key在内存中

由于wasm的运算机制,输入不变,那中间产生的变量啊什么的,都会在固定位置

那完全可以在调用完monalisa_set_license之后读取这个位置的内容就能拿到key了

至此调用wasm获取解密key完成

👍 5

none

还没有修改过

评论

贴吧 狗头 原神 小黄脸
收起

贴吧

狗头

原神

小黄脸

目录

avatar

未末

迷失

126

文章数

275

评论数

7

分类