加密之仿佛加了又仿佛没加系列

admin 2023年3月17日11:20:41评论109 views字数 11040阅读36分48秒阅读模式

“阿珍爱上了阿强,在一个有星星的夜晚”

“所以这跟你写文章有啥关系”

“噢,没有关系,我只是想唱一句。。。。哎哎哎别打脸,我说我说。。。。当你抓个包动不动就是密文乎脸,而你只能脸上笑嘻嘻,心里mmp的时候,你能怎么办,我也不想的呀,我也想做个好人呀,啊sir”

开个玩笑,没别的意思,就是遇到太多了,记录一下美好的学习瞬间。这边以两个案例详细走一遍分析、利用的过程,提一些不同的利用过程中可能出现坑。

01
加密之仿佛加了又仿佛没加系列
案例一
加密之仿佛加了又仿佛没加系列

  • 分析&调试  


挂着burp访问网站,可以看到在加载时已经开始传输加密数据了

             加密之仿佛加了又仿佛没加系列

  • 加密方法  

直接打开浏览器的开发者工具,在sources里全局搜索传输的参数ReqEncyptData,可以看到只有两条

加密之仿佛加了又仿佛没加系列

点击跳转过去,可以看到区分了web端和客户端,实际上这里只有一个赋值

加密之仿佛加了又仿佛没加系列

在packageEncrypt这一行打个断点,然后随意输入账户密码登录,确认一下位置是否正确

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

可以看到确实通过这里向packageEncrypt方法传递了明文和一个publicKey,继续跟进,进入packageEncrypt方法后,基本就看到了整个的加密过程和密文的拼接方式。

加密之仿佛加了又仿佛没加系列

继续往下走的话,可以发现tempSM4Key并不为空,所以没有再次随机生成,猜测可能是在第一次加密时就确定了tempSM4Key

加密之仿佛加了又仿佛没加系列

如果要直接调用js的话,这里就没有必要继续了,所有的key值和算法都明确了。接下来是decrypt部分,同样的方式,不再赘述了。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

02
加密之仿佛加了又仿佛没加系列
案例二
加密之仿佛加了又仿佛没加系列
  • 获取小程序包  

准备一个root了安卓机或者模拟器,使用文件管理类的app或者adb找到目录/data/data/com.tencent.mm/MicroMsg/[微信id]/appbrand/pkg,里面大概长这样(体验版小程序的包是debug开头的文件)。

加密之仿佛加了又仿佛没加系列

清空目录下的文件之后,再重新在微信中访问一下小程序,这里就会重新出现对应的小程序包了,有些情况下小程序会在用户登录之后加载两次或多次资源,导致小程序存在分包,所以建议登录之后,点点功能,再导包出来。

加密之仿佛加了又仿佛没加系列

(可以参考这个博主的文章http://xuedingmiao.com/blog/xcx_unpack.html

  • 解包&加载源码  

将包通过工具导出之后,使用mp-unpack之类的工具,将包一一反编译导出,获得混淆过后的源码。

加密之仿佛加了又仿佛没加系列

如果在这一步报错,可以查一下解决办法,解决不了的,就G。(这个工具部分版本,在windows下使用bingo.bat时,如果存在分包时需要加-s参数指定主包的文件目录,但是工具在处理-s的时候存在bug,需要将wuWxapkg.js里面代码orderElement.indexOf('-s')修改成orderElement.indexOf('s'))

成功获得源码后,就可以按照前面的方法去找加解密的function,唯一的区别时,如果想要插入console.log之类的或者断点之类的调试,需要使用微信开发者工具去加载源码。

加密之仿佛加了又仿佛没加系列

此处建议在“详情-本地设置”中,将“将JS编译成ES5”的勾选去掉,将“不校验合法域名、web-view、TLS版本以及HTTPS证书”勾选上。

加密之仿佛加了又仿佛没加系列

如果还报错,请自行根据报错信息查找解决办法。

  • 分析&调试  

跟上面一样的过程,根据请求数据中的关键字,比如说这里面的requestData,cryptoType,is_crypto_open,通过搜索找到给这些参数赋值的代码

加密之仿佛加了又仿佛没加系列

  • 加密方法  

这里全局查询后,可以看到cryptoType参数只有一个地方存在,相应的代码块中也能看到s.encrypt的方法,所以大概率请求参数的赋值就在这个方法中了,尝试插入console.log输出encrypt方法传入的参数确认(也可以和web端一样打断点调试,这里换一种方式)。

加密之仿佛加了又仿佛没加系列

重新加载之后,可以看到确实在console中输出了对应的信息,所以可以确定主要的加密方法就在这个位置。

加密之仿佛加了又仿佛没加系列

然后找到s的声明,可以确认程序使用了sm4  。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

打开sm4.js后,找到调用的encrypt方法,r是传入的明文,n应该是传入的key或者key相关的参数。

加密之仿佛加了又仿佛没加系列

return了h()方法,跟上去看一下,看到cbc,pkcs#5之类的,基本可以确认是加解密方法了,如果要调用js处理加解密的话,到这里只要输出一下n或者调用位置的l参数值就行了,通过多次调用、重新加载等,也可以确认这个值是固定的。

加密之仿佛加了又仿佛没加系列

注:但其实了解sm4的同学应该能看出来这个n并不是最终的key,它长度不太对(sm4密钥应该是16bytes,转成hex编码应该是32个字符),这个点目前不需要考虑,后续跟前面的小坑一起说。

  • 其他不明确的参数  

之后,再反过去看获取的明文信息,根据参数名,目前有两个参数的意义和来源方式不明origin_stamp和yhb_stamp。

先看origin_stamp,在刚刚加密调用的上方,传入了参数p,向上继续找到p的声明和赋值。

加密之仿佛加了又仿佛没加系列

可以看到o调用了一个方法,处理了传入的r。

先来找o,轻易的发现是调用了md5

加密之仿佛加了又仿佛没加系列

再看r,是传参传进来的,而且这个o处理的时候还使用了JSON.stringify,并且在下方加密前也将r赋值给了d,基本可以确认是传入的明文,console.log输出一下确认。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

这里就可以看出来跟传入加密的明文基本相同,可以确定r就是传入的明文,而origin_stamp是对明文计算的md5值。

接下来看yhb_stamp,根据上面来看,yhb_stamp是随明文r一起传入的,那全局搜索一下,可以看到这个位置给赋值了一个 f.default方法的结果

加密之仿佛加了又仿佛没加系列

跳转或者找到这个f的声明

加密之仿佛加了又仿佛没加系列

继续跟进stamp.js文件,有点反人类,大概意思应该是,随机生成了8位字符然后和时间戳之类的算了一个MD5,基本没差,参数yhb_stamp的长度也对的上,可以说是一个随机数,应该是用作校验唯一值的,但是跟服务端应该没有直接的数据交互,可以自己随意生成

加密之仿佛加了又仿佛没加系列

最终可以确认加密为sm4(明文(数据+yhb_stamp+origin_stamp(md5(数据+yhb_stamp))))

  • 解密方法  

找到加密的方法了,解密方法就很简单了,可以按照找加密的方式一步步来,这里就直接在libsmCryptoFilter.js中直接搜decrypt了。

加密之仿佛加了又仿佛没加系列

然后在这个位置直接console.log输出一下确认,很明显就是返回的密文和刚刚找到sm4密钥。

加密之仿佛加了又仿佛没加系列

  • 小坑  

开发写(抄)加解密代码的时候,可能会对一些算法进行修改(或者抄漏了,自己修改了一下),这样就造成其结果和原算法得出的结果不同,比如这个例子中,如果我们在sm4的密钥变换方法中,输出一下中间过程的值,可以发现其中存在一些问题,个位大于A的值将被置为0,

加密之仿佛加了又仿佛没加系列

具体原因是在处理传入的密钥时,相当于进行了一次强制类型转换,大概是想将字符串转成对应的十进制数

加密之仿佛加了又仿佛没加系列

但是由于其中存在A-F,则这些就被识别成0了;网上查一下类似的代码,可以看到这段key变化确实如猜想一样,这个位置应该传入的是十进制数的列表,相比缺少了一段stringToByte方法,导致和key变换后和正常的key不同

加密之仿佛加了又仿佛没加系列

如果继续分析,可以看到传入方法l()前,使用了y()方法对原key进行了一次处理

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

简单看一下,就是对原key进行截取或填充,使其长度为16,然后获得每一位的Unicode编码并转成hex,猜测一下,如果这个地方不进行hex编码转换,也许就达到了上面的stringToByte方法一样的效果了

  • 复现实现加解密  

目前根据复杂度、项目时间要求等等,可以至少有三种方式,简单说一下提供个思路。

  • python完整复现  

  • 实现  

这个以个人网银为例(我尝试了整合调用js,但是这个站在处理数据的时候加载调用了不下8个js,前后代码上万行,用nodejs调用的话,可能需要改很多代码,时间成本不太划算)先通过登录抓一个参考的数据,然后使用浏览器的console调用解密的方法解出对应的明文。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

也可以通过burp的proxy中“Match and Replace”功能或者fiddler在对应的js中插入console.log输出需要的明文、sm4key、sm2key。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

或者通过替换,强制固定sm4的密钥。接下来就按部就班的使用python调用第三方库(国密可以使用gmssl)进行加解密

加密之仿佛加了又仿佛没加系列

  • 小坑  

  • 第一个  

我们传参的时候如果使用json的格式,在python里就是字典结构,那在传入加密的时候,一般都需要使用json.dumps转成str,此时需要添加两个参数ensure_ascii和separators

json.dumps(data, ensure_ascii=False, separators=(',', ':'))

原因,python的json.dumps(),会在“,”“:”后面加一个空格,而绝大部分的前端使用JSON.stringify()将json格式的数据转换成字符串,这个方法默认不会像python那样添加空格,所以,经常会出现因为这个空格导致服务端解密后认为请求数据不合法的情况。

加密之仿佛加了又仿佛没加系列

  • 第二个  

用python去复现代码的时候,可能会跟开发想法产生偏差,或者由于对方调用其他人代码的时候,有意无意的添加或漏掉某些代码,导致结果与预期的不一致

例如这次个人网银的例子,我们在上边分析发现在加密后将结果进行了base64编码,它在这里使用了一个hex2b64()的方法

加密之仿佛加了又仿佛没加系列

如果我们直接将加密输出的hex字符串直接base64,可能跟它原本的结果不一样,跟一下这个hex2b64方法,可以发现,这个方法是先将hex字符串按照hex编码转成bytes之后在进行正常的base64,所以如果写python加密的话,就需要在生成hex字符串之后,进行base64时需要bytes.fromhex一下(当然直接用gmssl.sm4输出的bytes格式结果也可)

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

在例如上面的小程序,上面分析的时候,我们已经发现找的的那个sm4的key长度不对,后续代码中进行了某种变化,而且在用python进行sm4加密时,无论怎么调用,同一个明文都无法输出和小程序一样的密文(后续网上查到了类似的实现,发现小程序在处理数据的时候少了一步对key的变换处理),所以为了减少分析的时间,这里可以尝试直接使用小程序的js代码,结合python的execjs库进行加解密。

  • 第三个  

如果目标站点使用了rsa加解密,前端和服务端可能会只用一套公私钥,此时服务端返回的数据则会将私钥当公钥用进行加密,而前端则使用公钥当私钥进行解密。

而python的三方库中,一般会使用Crypto或者rsa进行rsa的加解密,能做到公钥解密的,目前已知只能是rsa。Crypto的rsa模块如果导入公钥当私钥将报错。

rsa库的实现公钥解密的代码大致如下:

# 公钥解密,对应的是私钥加密            def decrypt_by_rsa_with_pubk(ciphertext, public_key):                ciphertext = base64.b64decode(ciphertext)                if "-----BEGIN RSA PUBLIC KEY-----" in public_key:                    public_key = public_key.replace("n", "").replace("-----BEGIN RSA PUBLIC KEY-----", "").replace(                        "-----END RSA PUBLIC KEY-----", "")                public_key = base64.b64decode(public_key)                try:                    rsa_public_key = PublicKey.load_pkcs1_openssl_der(public_key)                    cipher_text_bytes = transform.bytes2int(ciphertext)                    decrypted_text = core.decrypt_int(cipher_text_bytes, rsa_public_key.e, rsa_public_key.n)                    final_text = transform.int2bytes(decrypted_text)                    final_qr_code = final_text[final_text.index(0) + 1:]                    return final_qr_code.decode()                except Exception as ex:                    print(ex)                    return None            

能这么用的原因跟rsa的公私钥生成方式有关,感兴趣可以去看这个up的视频《【RSA加密算法】| RSA加密过程详解》

  • 使用js复现  

  • 实现  

这个以小程序为例(web端也可以这么干,取决于python是否可以轻松实现加解密)。为了方便测试js代码能否被正常调用,建议先装一个nodejs和npm。

首先把分析到的已知js文件复制出来,然后根据加载这两个文件的地方,可知就是正常的require,且在md5和sm4的js中也未发现其他的require和import

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

然后在复制出来的同目录下,新建一个调用的js,用于调试和后续python调用 ,这个可以根据小程序中的调用来写

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

js文件内容:

var key = "6SEo3VxrdwF7GBOKsptuTR8nmQygIXZDfLlC1qh5WUbiMk4HYJeNcj2Pzv09aA";            var crypt = require("./sm4")            var md5_hash = require("./md5")                      function encrypt(postData){                       // return crypt.encrypt(JSON.stringify(postData), key);              return crypt.encrypt(postData, key);            }                      function decrypt(respData){                        return crypt.decrypt(respData, key);            }                       function md5(postData){                        return md5_hash(JSON.stringify(postData));              // return md5_hash(postData);            }            

      随便在微信开发者工具中输出一个请求,测试js文件的加解密

加密之仿佛加了又仿佛没加系列

使用python+execjs调用这个js文件中的加解密方法

import execjs            import os            import json                      os.environ["EXECJS_RUNTIME"] = "Node"                            # 编译加载js字符串            def js_from_file(file_name):                with open(file_name, 'r', encoding='utf-8') as file:                    result = file.read()                return result                                context = execjs.compile(js_from_file('./ybd.js'))                             def encrypt(data):                # data's type is json                en = context.call("encrypt", data)                # print(en)                return en                                 def decrypt(en):                data = context.call("decrypt", en)                # print(data)                return data                        def md5(data):                return context.call("md5", data)

测试一下

加密之仿佛加了又仿佛没加系列

当然这个地方调用js,也可以用burp的插件,主要看测试的需求。

  • 小坑  

虽然这个例子中没有遇到,但大部分前端加密中,会存在几个跟浏览器相关全局对象,window、document、navigator之类的,而nodejs中并不存在这类全局对象,此时如果直接使用nodejs调用,则会产生xxx is not defined或者xxx is not a constructor的报错

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

这个问题有两种解决办法,

第一,nodejs的module,jsdom,来模拟各种浏览器的对象,比如window等,可以参考https://www.cnblogs.com/huchong/p/11044238.html

使用npm安装jsdom,然后在js文件顶部添加jsdom的window和document对象

const jsdom = require("jsdom");const { JSDOM } = jsdom;const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);window = dom.window;document = window.document;XMLHttpRequest = window.XMLHttpRequest;

安装使用jsdom的时候,有一个地方需要注意,使用npm全局安装这个jsdom的话,nodejs可以用,但是python不行,可以通过两个方案解决:1、在项目目录使用npm装一个(建议这个,也方便直接甩给别人用);2、调用js执行的时候,使用cwd指定jsdom的安装目录

execjs.compile(js,cwd=r'C:Usersw001AppDataRoamingnpmnode_modules')

第二,既然是is not defined,那就在文件的js代码最外层,将所有not defined的参数都指向this(参考https://ask.csdn.net/questions/7490634http://www.ezd.cc/zs/18416.html

加密之仿佛加了又仿佛没加系列

或者也可以将这些浏览器的全局对象(window、document、navigator)改成nodejs的全局对象global(不保证所有情况都适用)

加密之仿佛加了又仿佛没加系列

  • 类hook式转发  

我们知道app或者exe程序,可以通过hook的方式对其中的代码或者参数进行更改、调用,将正常的请求流程截断,修改之后再放回去原流程中发出去,这种方式的思路类似,通过burp或者fiddler等工具,在明文传入加密函数前(明文从解密函数输出时),采用ajax之类的方式将明文转发至本地或者远程的中转服务器上,这个过程中,可以通burp拦包进行修改明文。

这种方式限制比较多,如果目标是小程序或者需要调用微信接口的h5页面,基本上不能使用;但好处也很明显,只要找到明文传入的地方(返回的话就是明文输出的)就可以进行更改了,不需要深入分析。不是很推荐这个方式,这里就简单描述一下思路。

加密之仿佛加了又仿佛没加系列

弊端很明显,第一,只能一发一改,不能重放;第二,如果目标是https,ajax也必须发到https(或者localhost),且证书必须是有效 的,不然需要浏览器将其添加信任,虽说在pc端可以通过“高级-接受风险”来使用,但微信移动端则没办法这么用。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

  • 延伸  

  • 透明代理  

在上面我们实现了单次的加解密,现在就要考虑一下怎么能在测试中自动进行加解密,甚至让其它工具也可以使用加解密进行自动化测试

这里提供一个思路,可以使用python的代理库mitmproxy,一个http代理库,可以通过自己写的python代码,实现自动修改请求、返回中的所有数据,达到透明代理的效果,类似xray可以放在burp之后,进行自动改包,甚至可以延伸成被动扫描。

使用python 的脚本大概如下

加密之仿佛加了又仿佛没加系列

request()中,用于处理请求的数据

def request(self, flow):                # 处理请求数据                # 目前已知POST请求中存在加解密,为减小代理对网络的影响,尽量最小化修改                if flow.request.method == 'POST':                    datas = flow.request.get_content()                    # 判断关键词在请求中,意味着需要解密                    if b"ReqEncyptData" in datas:                        req_data = json.loads(datas)["ReqEncyptData"]                        # var reqdata = SM2Data + "|" + SM4Data + "|" + SM3Data;                        # 安装拼接格式,这里以|分割取第二块数据                        en_data = req_data.split("|")[1]                        print(en_data)                        de_data = getsm4decode(en_data, self.sm4_key)                        print('########################requests path##################################')                        print(flow.request.path)                        print('###########################requests data###############################')                        print(de_data)                            # 判断关键词不在请求中,意味着需要加密                    if b"ReqEncyptData" not in datas:                        req_data = getreqdata(datas, self.sm4_key, self.sm2_key).encode()                        print(req_data)                        Content_Length = bytes(str(len(req_data)), 'utf-8')                        headers = flow.request.headers                        headers['Content-Length'] = Content_Length                              flow.request.raw_content = req_data

response()用于处理返回包中的数据

def response(self, flow):                if flow.request.method == 'POST':                    datas = flow.response.get_content()                    # 判断关键词在请求中,意味着需要解密                    if b"RespEncyptData" in datas:                        en_datas = json.loads(datas)                        de_datas = getsm4decode(en_datas["RespEncyptData"].split("|")[0], self.sm4_key)                        print('########################response path##################################')                        print(flow.request.path)                        print('##########################response data################################')                        print(de_datas)                        print('##########################################################')                        en_datas["datas"] = json.loads(de_datas)                        res = json.dumps(en_datas, ensure_ascii=False, separators=(",", ":")).encode()                        # print(res)                        Content_Length = bytes(str(len(res)), 'utf-8')                        headers = flow.response.headers                        headers['Content-Length'] = Content_Length                        headers['Content-Type'] = "application/json"                        flow.response.raw_content = res
  • 随机key处理  

部分系统为了某些考虑,可能会使用随机的密钥进行对称加密,可能一次随机也可能此次随机,然后再通过非对称加密传输密钥。

如果前端存在非对称加密算法的私钥,可以直接解,不需要其他操作,但是如果没有,我们可以考虑两种办法。

一、 强制使用固定的密钥  

通过burp、fiddler等工具,替换密钥获取的部分,以上面的个人网银为例,可以将csiiPackageEnc.min.js中的var tempSM4Key = "";替换成固定密钥var tempSM4Key = "21527699423324889253637794508928";

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

二 、直接发出明文请求和密钥  

通过burp、fiddler等工具,替换请求参数复制的部分,以上面的个人网银为例,可以将vx2-config.js中config.data={"ReqEncyptData":encryptData}替换成config.data={"ReqEncyptData":vx.toJson(config.data),"SM4Key": tempSM4Key},这样也可以顺便输出明文,但是这种做法,我们需要结合代理将明文自动加密回去。

加密之仿佛加了又仿佛没加系列

加密之仿佛加了又仿佛没加系列

其它  

目前还有一种组合无法解决,即在小程序中使用类似rsa(aes_random_key)+aes(text),虽然可以分析找到对应加密算法,但受限于小程序反编译的完整性和我个人能力的问题,暂时(也可能是永久)无法解决这个问题。有想法的大哥可以一起讨论一下。

参考  

微信小程序的反编译(http://xuedingmiao.com/blog/xcx_unpack.html)

通过Fiddler用本地js文件替换源网页的js文件(http://www.inseo.cn/seo/161.html)

【RSA加密算法】| RSA加密过程详解(https://www.bilibili.com/video/BV1YQ4y1a7n1/?vd_source=1eae91115c4e098adc946b1ff46dc156)

python使用execjs执行含有document、window等对象的js代码,使用jsdom解决(https://www.cnblogs.com/huchong/p/11044238.html)

js逆向遇到JSEncrypt is not defined 和 JSEncrypt is not a constructor问题(https://ask.csdn.net/questions/7490634)

Node中是否有window对象(http://www.ezd.cc/zs/18416.html)

快速定位前端加密方法(https://gv7.me/articles/2018/fast-locate-the-front-end-encryption-method/)

监制:船长、铁子   策划:AST   文案:枫子   美工:青柠

原文始发于微信公众号(千寻安服):加密之仿佛加了又仿佛没加系列

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月17日11:20:41
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   加密之仿佛加了又仿佛没加系列http://cn-sec.com/archives/1611258.html

发表评论

匿名网友 填写信息