一次微信小程序的测前准备

一次微信小程序的测前准备

作为一名后勤人员,工作的首要任务就是帮兄弟们做好战前准备工作。这不来了一个微信小程序的活。还好抓包兄弟们那边没有问题。主要是加密破解问题。

1.wxapkg 包的获取与反编译
在这之前先大概介绍一下小程序缓存代码的反编译:
Android 手机最近使用过的微信小程序所对应的 wxapkg 包文件都存储在特定文件夹下,可通过以下命令脱下来查看:
adb pull /data/data/com.tencent.mm/MicroMsg/{User}/appbrand/pkg其中`{User}` 为当前用户的用户名,类似于 `2bc**************b65`

1.1 wxapkg 包与小程序的对应关系

根据访问时间确定需要脱的.wxapkg文件,如果知道小程序关键字也可以全脱下来,使用grep -r来筛选。

1.2 小程序编译工具安装与配置

反编译工具下载链接:

https://github.com/geilige/wxappUnpacker.git

(这个工具原作者已经将仓库清了,这是一个fork的,github也有一个仍在维护反编译工具的作者,但是没有试过,因为目前使用的这个也还可以)

这些 node.js 程序除了自带的 API 外还依赖于以下包: cssbeautify、CSSTree、VM2、Esprima、UglifyES、js-beautify

直接执行npm install;另外如需全局安装这些包可执行以下命令:
npm install esprima -gnpm install css-tree -gnpm install cssbeautify -gnpm install vm2 -gnpm install uglify-es -gnpm install js-beautify -gnpm install escodegen -g

此外,这些 node.js 程序之间也有一定的依赖关系,比如他们都依赖于 wuLib.js.

1.3 如何使用

具体使用方式readme里有详细介绍,一般就直接使用如下命令解压即可:
node wuWxapkg.js D:/node/wxappUnpacker-xxxxxx-41_321.wxapkg(具体的.wxapkg 的文件路径)
例:不知道先后日期,先全脱下来,然后用关键词搜筛选。
一次微信小程序的测前准备
解包操作:
一次微信小程序的测前准备
然后使用vscode等有全局搜索功能的编辑器,搜索诸如{ appid、secretid、secretkey}等敏感词
一次微信小程序的测前准备

2.小程序的加解密及加签算法

前面只是举个栗子,介绍了如何获取到小程序的缓存源码。下面具体分析一下这个小程序的加解密及加签算法。废话不多说先抓一个包再说。抓包截图如下所示:
一次微信小程序的测前准备
数据包总共有三个参数:当然加密的就只是第一个参数了,毕竟那么长嘛!
常规操作,首先打开夜神模拟器(这里使用的是最古老的安卓5系统,微信也是一个兄弟提供的一版较老的32位安装包)。然后登录微信搜索打开该目标小程序,尽量多点一下,让小程序缓存到本地微信数据目录。
具体的操作就不展示了,和上一节介绍的那个栗子一样。这里我们直接开始分析加解密及加签的是如何实现的。
我们首先来看加密的数据参数这是我们可以上手的重要线索:requestData
使用带全局搜索的编辑器直接全文搜索一下requestData:
一次微信小程序的测前准备
搜索到的requestData有多处,但是经过比较发现最终选择分析的代码中不只是有requestData,还带有数据包的其余两个参数。所以应该是我们要找的加解密的上层调用代码。可以从这个口子往下分析其调用实现的代码。

2.1 参数1

数据包参数1的值来自 v,而 v = t(m);因此进入 t(m)函数:一次微信小程序的测前准备
t函数中的randomStr来自下面的导出函数,而该导出函数为该文件定义的e函数:
一次微信小程序的测前准备
现在函数已经找到了,只差参数m:
一次微信小程序的测前准备
直接全局搜索XCXAppId,得到两个值,在实际脚本写的时候用的第二个:
一次微信小程序的测前准备
按理说应该按照代码去找相应的,但是在对代码进行交叉引用寻找时,被带的走弯路了。实际测试中也没有具体的影响因为该参数未参与加解密及加签,而且实际抓包中该参数的值确实是第二个XCXAppId的。所以参数1最后值为前5个随机字符加上XCXAppId再加上5个随机字符后缀。
一次微信小程序的测前准备

2.2 加签参数

接下来看一下加签参数是如何计算的。
签名参数的值来自于P,而P来自于o.default函数计算得出的值。
一次微信小程序的测前准备
点击o进入到r(require("./md5.js"))
一次微信小程序的测前准备
r函数定义如下:
一次微信小程序的测前准备
所以o.default即为md5函数。
r是要加密的json数据,在这里被JSON.stringify(r)转换为字符串再加密。

2.3 数据加密参数2

计算完签名,我们来分析加解密算法即requestData参数:
requestData的值为s.encrypt,是sm算法。
一次微信小程序的测前准备
这里有两个参数,第一个参数是d,d是由n函数加上{}、r(就是前面计算md5的待加密数据)、origin_stamp: p
一次微信小程序的测前准备
具体代码不去分析了,可以从第二个n来进行猜测,这个n是一个拼接json的函数,所以第一个n函数的作用就是将之前计算的md5也就是数据的前面加到数据json后面,然后再进行sm4计算。
n({requestData: s.encrypt(JSON.stringify(d), l)
sm函数的第二个参数是他的密钥,密钥来自:
l = (0, i.getGlobalData)("encryptKey")
微信小程序在JavaScript文件中声明的变量和函数只在该文件中有效;不同的文件中可以声明相同名字的变量和函数,不会互相影响。如果希望在各个页面之间共同使用某些信息,并且可以对共享数据进行修改设置,以便于其他页面根据数据变化进行对应的调整,最好使用全局数据globalData。于是我们去app.js寻找,大致的查找步骤如下图所示:
一次微信小程序的测前准备
网上查询wx.getExtConfigSync函数:
一次微信小程序的测前准备
一次微信小程序的测前准备

2.4 第三个参数

此值为一个固定值“wx”,可能用于表示数据来源。

2.5 Burpsuite插件编写

既然加密和加签算法已经分析出来了,接下来要做的工作就是编写burpsuite插件完成其自动加解密。这里当时最开始是采用了将加密算法使用node编写成exe文件,然后通过传参的方式进行调用。但是在测试过程中发现json数据作为参数始终无法正确传递给要加密的exe。所以最后采用了http请求的方式进行传参,最终服务端的代码如下(xxx代表密钥):
s = require("./index.js").sm4;const http = require('http');let app = http.createServer((req, res) => {    const { headers, method, url } = req;      let body = [];      req.on('error', (err) => {        console.error(err);      }).on('data', (chunk) => {        body.push(chunk);      }).on('end', () => {        body = Buffer.concat(body).toString();        t = JSON.parse(body);        if(t.type=="decrypt"){            // b = s.decrypt(eval(t.data),"xxxxxxxxxxxxxxxxx");            b = s.decrypt(t.data,"xxxxxxxxxxxxxxxxxxx");            res.writeHead(200, {'Content-Type': 'text/plain'});            res.end(b);        }        if(t.type=="encrypt"){            // a = s.encrypt(eval(JSON.stringify(t.data)),"xxxxxxxxxxxxxxxxx");            console.log(t.data)            a = s.encrypt(t.data,"xxxxxxxxxxxxxxxxxxx");            console.log(a);            res.writeHead(200, {'Content-Type': 'text/plain'});            res.end(a);        }      });  });app.listen(3000, '127.0.0.1');
接下来我们来看如何编写burpsuite插件。
首先是一些固定的代码接口的实现,这对于每个插件是固定的,是用于创建插件面板的:
class BurpExtender(IBurpExtender,IMessageEditorTabFactory):    def registerExtenderCallbacks(self, callbacks):        self.stdout = PrintWriter(callbacks.getStdout(), True)        self.stderr = PrintWriter(callbacks.getStderr(), True)        self._callbacks = callbacks        self._helpers = callbacks.getHelpers()        callbacks.setExtensionName(pkgname)        callbacks.registerMessageEditorTabFactory(self)        return    def createNewInstance(self, controller, editable):        return Display_data(self, controller, editable)
接下来就是在Display_data这个类里面来展示要加密和解密的数据包。下列的代码也是固定的:
class Display_data(IMessageEditorTab):    def __init__(self, extender, controller, editable):        self._helpers = extender._helpers        self._txtInput = extender._callbacks.createTextEditor()        self._extender = extender        self.rep_body = ""        self.res_body = ""    def getUiComponent(self):        return self._txtInput.getComponent()    def getTabCaption(self):        return pkgname    def isEnabled(self, content, isRequest):        return True
在Display_data类主要编写的是三个函数:用于往插件面板展示数据的setMessage函数和往raw面板获取数据的getMessage函数。本质来讲只需要这两个就可以了。但是因为我们的加解密是要调用node生成http api来对数据进行加解密。所以我们还需要创建一个数据的请求函数用于获取加解密数据。Api函数如下:
def api_req(self,data,type):        try:            headers = {"Content-type": "application/json",                       "Accept": "text/plain"}            conn = httplib.HTTPConnection(_api)            # print data            params = json.dumps({'type':type,'data': data})            # print params            conn.request("POST", "/", params, headers)            resp = conn.getresponse()            assert resp.status == 200            data = resp.read()            conn.close()            # print(data)            return str(data)        except Exception as e:            return {"status":"error","data":e.__str__()}
这里的请求params里面有个type参数,对应的就是最开始的服务端代码中的”decrypt”和”encrypt”。以此来区分加密还是解密。
接下来我们了解一下setMessage函数的编写:该函数有三个参数:
def setMessage(self, content, isRequest):
isRequest参数可以用来判断要处理的数据是往Request模块的插件面板写入还是往Response模块的插件面板写数据。
    info = self._helpers.analyzeRequest(content)            raw_data = content[info.getBodyOffset():].tostring()
以上代码可以获取到请求的body,然后对数据进行处理之后向解密的服务端发送请求获取到解密后的数据:
 d_body = json.loads(raw_data)            # print d_body            raw = d_body['requestData']            output = self.api_req(raw.encode("utf-8"),"decrypt")
Response模块的也是和上面的操作一致,因为他两个对应的解密接口一致,将output数值赋予_output。最后在setMessage函数结尾将_output数据填充至self._txtInput.setText()函数内:
  self._txtInput.setText(_output)  return
getMessage函数相对有些复杂:
edit_text = self._helpers.bytesToString(self._txtInput.getText())
上面的代码可以获取到插件面板的数据。因为测试的时候是需要对参数进行修改的。修改之后的数据加签就会改变。所以我这边采用的是正则表达式的方式将签名数值进行重新计算后修改回去。首先要做的是将签名参数从加签数据去除。
origin_stamp_re = re.compile(r',"origin_stamp":"(w{32})"',re.DOTALL)origin_stamp_text = re.sub(origin_stamp_re,'',edit_text)
去除之后重新计算加签数值并将其重新加到数据中去:
hl = hashlib.md5()        hl.update(origin_stamp_text.encode(encoding='utf-8'))        text = re.sub(origin_stamp_re,r',"origin_stamp":"%s"' % hl.hexdigest(),edit_text)
然后将处理好的数据通过api接口进行加密请求:   
output = self.api_req(text,"encrypt")        self.res_body['requestData'] = output.strip()
最后重新封装成返回:
request_bytes = self._helpers.stringToBytes(body)info = self._helpers.buildHttpMessage(self._headers, request_bytes)# print(info)return info
最终结果如下面所示:
解密结果:
一次微信小程序的测前准备

加密及加签:

一次微信小程序的测前准备

原文始发于微信公众号(雁行安全团队):一次微信小程序的测前准备

版权声明:admin 发表于 2022年12月28日 上午11:34。
转载请注明:一次微信小程序的测前准备 | CTF导航

相关文章

暂无评论

暂无评论...