某网站字幕加密的wasm分析

某网站字幕加密的wasm分析

js层动态分析

网站地址:aHR0cHM6Ly93d3cuaXEuY29tL3BsYXkvMmZhcWJkMTV1YWM=(需要【台】的ip)

首先打开网址,f12 抓包开起来,播放视频后在过滤器中搜索【.xml】

某网站字幕加密的wasm分析

可以看到sub标签里面的字幕内容被加密了,先给字幕下一个xhr断点

某网站字幕加密的wasm分析

这里可以看到当字幕文件加载完成后,会执行t函数,跟进去

某网站字幕加密的wasm分析
这里可以看到对字幕文件进行简单的解析,还没开始进行解密,当解析完成后会执行changeSuccess函数,继续跟下去

某网站字幕加密的wasm分析

这里继续往下,p参数这一行有一个分之,查看p的值是1,说走前面的代码。然后有一个a._encrypt.show()引起了注意,从这里继续跟进去

某网站字幕加密的wasm分析

这里跟着执行setStyle函数,继续跟下去

某网站字幕加密的wasm分析

前面进行了一大段的计算,和函数名相符,这里设置了一些字幕的样式,但是还没有涉及字幕本身的解密,最后的setText函数,看起来就和文本有关,有可能解密就在里面完成,那就继续跟进去看看

某网站字幕加密的wasm分析

断点断下后就可以看到字幕的密文了,d参数就是需要解密的内容,复制去验证一下

某网站字幕加密的wasm分析

确实可以在xml文件中找到这一段密文,接着就是看看调用到这段密文的函数

某网站字幕加密的wasm分析

这里可以看到,调用了一个s函数后返回一个f,然后这个f就用来设置字幕的宽度。既然这个f能用来计算字幕的宽度,说明了s函数内部已经解密的明文,那么才能计算宽度的,说明要继续跟进s函数里面

某网站字幕加密的wasm分析

这次跟进去后,发现并没有那么顺利,直接来到了call的函数,这是js层调用了wasm的一个函数的特征,如果需要继续分析的话,那么就得对wasm进行分析了。过滤器中搜索wasm文件下载下来。

wasm初步处理

根据这篇文件的介绍【一种Wasm逆向静态分析方法】,可以使用wabt工具【项目地址:wabt】中的wasm2c,将wasm的二进制文件转换为c文件

wasm2c wasm.wasm -o wasm.c

此时可以得到wasm.c和wasm.h,然后将wabt项目内的wasm-rt.h,wasm-rt-impl.c,wasm-rt-impl.h三个文件放到同一个文件夹,通过gcc得到编译的o文件

gcc -c wasm.c -o wasm.o

此时的o文件就可以放进IDA进行反汇编分析了。如果觉得上面的步骤繁琐的话,可以使用逍遥一仙大佬封装的一键工具。可以直接将wasm得到o文件

wasm一键转c

某网站字幕加密的wasm分析
将最后得到的o文件加载到IDA中

某网站字幕加密的wasm分析

加载完发现有两百多个函数,肯定不可能一个一个函数去分析。首先肯定是要先找到js层调用的是哪个函数,然后再重点去分析对应的函数

浏览器动态分析与IDA静态分析合作

回到浏览器,call函数的第一个参数就是指明需要调用wasm中的哪个函数

某网站字幕加密的wasm分析
可以看到函数名是monalisa_get_line_number,但是在wasm中的函数名称窗口搜索,却搜索不到这个函数,因为这个只是js层的函数名,还要看是绑定在wasm中的哪个导出函数,在同一个js文件中搜索这个函数名

某网站字幕加密的wasm分析

这时就可以清楚的看到是wasm中的v函数

某网站字幕加密的wasm分析

有了函数名,现在还需要知道分别传入的参数是什么,这里就要回到前面的s函数了
某网站字幕加密的wasm分析
第一个参数n是前面获取的一个上下文,搜索一下这个_ctx

某网站字幕加密的wasm分析

可以看到ctx是通过moAlloc函数获取的,相当于monalisa_context_alloc,看到alloc可以确定这是一个申请内存的c库函数,所以不需要继续分析,简单可以理解成申请一段内存,返回的是这段内存起始的指针

第二个参数就是字幕的密文字符串,第三个参数就是字符串的长度,第四个参数就是一段固定的字符串,那么这时可以将IDA的变量名改一下

某网站字幕加密的wasm分析

字幕内容只有传入到w函数,其他都没有用到,那么就可以进入到w函数分析。回到浏览器,在w函数前面下一个断点断下来

某网站字幕加密的wasm分析
这里可以看到这个函数传入了5个i32类型的参数,但是IDA识别的不正确

某网站字幕加密的wasm分析

这时可以在函数名邮件,设置项目类型,修改成正确的,顺便将变量名修改一下

某网站字幕加密的wasm分析
某网站字幕加密的wasm分析
跟着就来到了w2c_f24函数
某网站字幕加密的wasm分析
某网站字幕加密的wasm分析

这里可以看到返回值就是w2c_J,从js中可以查看到,又是一个申请内存的函数,然后就是w2c_f93函数

某网站字幕加密的wasm分析
第一个参数就是前面刚刚申请的内存,说明极大可能是用来放函数的返回值,并且将密文传了进去,说明这个函数肯定是一个关键点,那么来看看返回值是什么,首先单步进入函数

某网站字幕加密的wasm分析

第一个参数是一个指针,地址是6066376,然后直接结束这个函数,然后去查看这个地址
某网站字幕加密的wasm分析

可以看到一段16字节的内容,仔细观察一下其实可以发现,这个函数实际就是把密文进行了base64解码

某网站字幕加密的wasm分析
但是接下来静态分析并不能知道v11的值,所以继续在浏览器单步运行

某网站字幕加密的wasm分析

可以看到浏览器跟着运行的是call $func71,继续跟进去。w2c_f24是申请内存,前面已经分析过了,然后是w2c_f23

某网站字幕加密的wasm分析

直接去到函数的结尾,函数的返回值就是第一个参数,实际上这个函数就是在做内存的复制

某网站字幕加密的wasm分析
复制后的内存就只有w2c_f95用到,那么就肯定要跟进去这个函数

某网站字幕加密的wasm分析
进到w2c_f95后发现并没有那么顺利,里面只有w2c_f94和w2c_f149,里面的运算都比较复杂,这时我卡壳了。

c层aes算法特征分析

解密的话常见的就三种情况,异或加位运算、对称加密以及非对称加密。这个时有个地方引起了我的注意

某网站字幕加密的wasm分析

128、192、256这三个数字不就是aes算法的三种密钥长度,如果推论为加密用的是aes的话,那么里面的a4a与v40又恰好可以认为是密文的密钥长度和轮换次数。再去xml里面看一下密文,果然密文全部都是16字节的倍数,那么就已经可以确定是用的aes算法了

某网站字幕加密的wasm分析

知道了算法以后,还需要知道几个关键的值,分别是密钥及其长度,算法模式,偏移。密钥长度可以看到就是a5,它分别和128、192、256进行对比,a5是传进去来参数,是固定的128,那么现在还剩下密钥、算法模式、偏移

接下来需要寻找密钥,根据文章【常见加密算法】中的讲述,aes算法首先通过的是initial_round,然后是9个rounds,接着最后一个round少一个步骤

某网站字幕加密的wasm分析
进入w2c_f149,其中的a4就是前面传进来的轮换次数10

某网站字幕加密的wasm分析

可以看到每次循环自减1,一共轮换9次,剩下的是第十次,这与算法完全吻合,但是key应该在哪里获取呢,可以看到initial_round步骤就用到了key,这时自然就想到了w2c_f149前面的w2c_f94

某网站字幕加密的wasm分析

某网站字幕加密的wasm分析

从高级加密标准AES-FIPS197中可以知道,initial_round执行的就是轮密钥加(AddRoundKey( ))变换,这是需要与密钥进行异或,那么就肯定要先把密钥取出来,自然想到一开始的循环就是将密钥取出来,然后进行异或,这里的a1就是前面传进来的值,先在浏览器看看密钥是什么,也就是w2c_f94的第一个参数

某网站字幕加密的wasm分析

不知道哪里来的一段16字节,不管那么多,先试试能不能解密,现在还没有iv,所以先用ECB的模式试试

def decrypt_zimu():
    enc_text = 'by5JecM7CKHaHHUd0C2wupB2A/X+CE2JRSbc8LK9p/U='
    crypto = AES.new(key=bytes([29,210,139,12,180,186,89,38,237,117,185,130,29,2,53,180]), mode=AES.MODE_ECB)
    print(crypto.decrypt(base64.b64decode(enc_text.encode())).decode(errors='ignore'))
    # 怎麼樣啊 醫t6x 

可以看到前16字节可以解密,但是后面的无法解密,说明模式错了,应该是CBC,那么这时还需要一个正确的iv,不然前16字节是错误的。CBC的iv是在最后进行异或的,自然想到了最后的一段函数

某网站字幕加密的wasm分析
那么这里的a2就是偏移,和密钥一样去浏览器获取,可以知道16字节都是0,加上iv重新解密

def decrypt_zimu():
    enc_text = 'by5JecM7CKHaHHUd0C2wupB2A/X+CE2JRSbc8LK9p/U='
    crypto = AES.new(key=bytes([29,210,139,12,180,186,89,38,237,117,185,130,29,2,53,180]), mode=AES.MODE_CBC, iv=bytes([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]))
    print(unpad(crypto.decrypt(base64.b64decode(enc_text.encode())), AES.block_size).decode())
    # 怎麼樣啊 醫生

这时就完全解密出字幕了,接下来如果能知道key怎么来的,那么就大功告成了。

密钥的参数一直往外追,实际是v函数的第一个参数偏移4,也就是一开始的ctx偏移4,那么就是说在执行解密之前,执行了其他函数来设置了密钥,因为不可能申请内存里面就有密钥了。这时可以将除了内存处理之外的所有导出函数都下一个断点,在这个wasm中就是所有monalisa开头的导出函数,然后刷新,会在_monalisa_set_license的地方断下

某网站字幕加密的wasm分析

这个函数的第一个参数也传入了ctx,这里的参数可以在dash接口里面找到

某网站字幕加密的wasm分析
继续让这个函数运行,接着就在前面的函数断下了,那就充分说明这个就是获取密钥的函数,接下来也按照前面的方法,找函数,一步一步分析,那么理论上就可以获取到密钥了,但是实际并没有那么简单,中间分析的过程就忽略了,因为和上面是大同小异。当我分析到w2c_f129的时候,这个函数结束,密钥生生成了,但是这个函数超长,仅仅定义变量就有1000多个,这明显加了混淆了。

既然不能直接分析出算法,那么能不能用魔法来打败魔法呢?nodejs可以加载wasm运行,如果可以调用nodejs来得到密钥,那不就可以省下很多功夫了。

wasm调用代码扣取与异步加载处理

一般网页加载wasm的话,都有一个对应名称的js,把与wasm同名的js下载下来,并且与wasm放在同一文件夹内

某网站字幕加密的wasm分析
下载格式化后发现,这个Module被放到一个自执行函数的里面,那么外部就无法调用,那么就需要将这个自执行函数的代码放到外面,让全局中可以找到Module这个变量,注释头尾部分内容,就可以获取到Module

// var Monalisa = (function() {
//     var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
//     if (typeof __filename !== 'undefined')
//         _scriptDir = _scriptDir || __filename;
//     return (function(Monalisa) {
//         Monalisa = Monalisa || {};
        Monalisa = {};

        var Module = typeof Monalisa !== "undefined" ? Monalisa : {};
        var readyPromiseResolve, readyPromiseReject;
        Module["ready"] = new Promise(function(resolve, reject) {
            readyPromiseResolve = resolve;
            readyPromiseReject = reject
        }
        );
        var moduleOverrides = {};
        var key;
        for (key in Module) {
            if (Module.hasOwnProperty(key)) {
                moduleOverrides[key] = Module[key]
            }
        }
		/*
		中间省略几千行		
		*/
        Module["run"] = run;
        if (Module["preInit"]) {
            if (typeof Module["preInit"] == "function")
                Module["preInit"] = [Module["preInit"]];
            while (Module["preInit"].length > 0) {
                Module["preInit"].pop()()
            }
        }
        noExitRuntime = true;
        run();

        // return Monalisa.ready
    // }
    // );
// }
// )();
// if (typeof exports === 'object' && typeof module === 'object')
//     module.exports = Monalisa;
// else if (typeof define === 'function' && define['amd'])
//     define([], function() {
//         return Monalisa;
//     });
// else if (typeof exports === 'object')
//     exports["Monalisa"] = Monalisa;
console.log(Module);

接下来就是尝试调用_monalisa_set_license方法来获取密钥了,安装js的方法,首先获取一个ctx,前面有说过,然后是调用_monalisa_set_license方法

console.log(Module);
function decrypt() {
    var ctx = Module["cwrap"]("monalisa_context_alloc", "number", [])();
    var License = "AA4ACgMAAAAAAAAAAAQCDwACATADEAAnAgAgeyWysVa0GpbmCNvd+S1tsL6yp/j2tbA14sqW1ppgepYCAAAAAxEANwEAMDCtrqLHyZQ7p8RX3ih4NIqLWR1zCfu3mMFlxC2kiPgHmxZY7I/KYq4pMkH3rZQsqgEAAgD/EgAkAQAAIGSSUL7C0qWJp/LIkKoS12QYws1e0z/CewNJaaqktC3z";
    Module["cwrap"]("monalisa_set_license", "number", ["number", "string", "number", "string"])(ctx, License, License.length, "0");

    console.log(new Buffer.from(Module.HEAPU8.slice(ctx+4, ctx+4+16)).toString('hex'))
}
decrypt();

但是出现报错了

                    throw ex
                    ^

TypeError: Cannot read property 'D' of undefined

说js中的D函数还没有定义,实际这是一个异步加载的wasm,当我们运行到解密函数的时候,实际上wasm还没有加载完。对于异步加载的wasm,有两个重要的参数runtimeInitialized和runtimeExited,一个代表wasm的加载时机,完成加载则会变成true;runtimeExited是wasm的卸载时机,完成卸载则会变成true。

既然后异步加载的,那么就可以设置一个定时器来监控runtimeInitialized的值,当期变为true时,再执行解密函数

console.log(Module);
function decrypt() {
    var ctx = Module["cwrap"]("monalisa_context_alloc", "number", [])();
    var License = "AA4ACgMAAAAAAAAAAAQCDwACATADEAAnAgAgeyWysVa0GpbmCNvd+S1tsL6yp/j2tbA14sqW1ppgepYCAAAAAxEANwEAMDCtrqLHyZQ7p8RX3ih4NIqLWR1zCfu3mMFlxC2kiPgHmxZY7I/KYq4pMkH3rZQsqgEAAgD/EgAkAQAAIGSSUL7C0qWJp/LIkKoS12QYws1e0z/CewNJaaqktC3z";
    Module["cwrap"]("monalisa_set_license", "number", ["number", "string", "number", "string"])(ctx, License, License.length, "0");

    console.log(new Buffer.from(Module.HEAPU8.slice(ctx+4, ctx+4+16)).toString('hex'))
}

var timer = setInterval(c, 1);
function c() {
    if (runtimeInitialized){
        clearInterval(timer);
        decrypt()
    }
}

这时就可以正确获取到密钥,这时只要将License修改为process.argv[2],就可以在命令行调用了

function decrypt() {
    var ctx = Module["cwrap"]("monalisa_context_alloc", "number", [])();
    var License = process.argv[2];
    Module["cwrap"]("monalisa_set_license", "number", ["number", "string", "number", "string"])(ctx, License, License.length, "0");

    console.log(new Buffer.from(Module.HEAPU8.slice(ctx+4, ctx+4+16)).toString('hex'))
}

var timer = setInterval(c, 1);
function c() {
    if (runtimeInitialized){
        clearInterval(timer);
        decrypt()
    }
}
def decrypt_zimu():
    License = "AA4ACgMAAAAAAAAAAAQCDwACATADEAAnAgAgeyWysVa0GpbmCNvd+S1tsL6yp/j2tbA14sqW1ppgepYCAAAAAxEANwEAMDCtrqLHyZQ7p8RX3ih4NIqLWR1zCfu3mMFlxC2kiPgHmxZY7I/KYq4pMkH3rZQsqgEAAgD/EgAkAQAAIGSSUL7C0qWJp/LIkKoS12QYws1e0z/CewNJaaqktC3z";
    nodejs = os.popen('node libmonalisa-v3.0.6-browser '+License)
    key = nodejs.read().replace('\n', '')
    nodejs.close()
    enc_text = 'by5JecM7CKHaHHUd0C2wupB2A/X+CE2JRSbc8LK9p/U='
    crypto = AES.new(key=bytes.fromhex(key), mode=AES.MODE_CBC, iv=bytes([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]))
    print(unpad(crypto.decrypt(base64.b64decode(enc_text.encode())), AES.block_size).decode())
    # 怎麼樣啊 醫生

完美,正确解密出结果,同一个字幕文件,所使用的所有key都是一样的,也就是说调用一次获取密钥,就可以解密出一整个字幕文件了

完结,散花

参考文献

1.XXX视频cKey9.1的生成分析和实现:https://www.52pojie.cn/thread-948353-1-1.html
2.一种Wasm逆向静态分析方法:https://www.52pojie.cn/thread-962068-1-1.html
3.wasm一键转c:https://www.52pojie.cn/thread-1438499-1-1.html
4.高级加密标准AES-FIPS197:https://wenku.baidu.com/view/2ce7a11b10a6f524ccbf8514.html
5.常见加密算法:http://www.codinganswer.com/?yohytk=skmhl1

上一篇:EVM、Wasm虚拟机原理和设计思路


下一篇:【Rust日报】 2019-05-11:wasm-flate 使用WASM对客户端文件进行超快压缩的