首页
社区
课程
招聘
最新版wxunpacker
发表于: 2023-3-10 17:12 25100

最新版wxunpacker

2023-3-10 17:12
25100

wxunpacker能查到的最新版本,是Tech-Chao在2年前的版本。现在对于不少小程序,都会报解密错误,或者是微信开发者工具无法打开。

结合了各位前人的经验,我用python写了一个新的版本。程序难度,主要在于wxml的解析。我没有分析_mz/_2等一系列解压缩算法,而是利用execjs的功能,直接对相关文件做了调用,并渲染成了对应的wxml文件。

1、先把mac os的SIP做一个disable or enable SIP,否则lldb不能attach到wechat进程上。
2、打开wechat,但是不登录。
3、lldb -p wechat的pid。
4、br set -n sqlite3_key,断点设置好后,c继续运行。
5、微信登录后,会break到断点上,输入memory read --size 1 --format x --count 32 $rsi
6、前16位即是你本机的wechat小程序加密的密钥,而完整的32位则是本机微信聊天记录sqlite db的密钥。


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2023-3-13 09:50 被今天是星期五编辑 ,原因:
收藏
免费 12
支持
分享
最新回复 (28)
雪    币: 113
活跃值: (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
文件调用方法:
usage: python3 wx.py [-h] [-w W] [-i I] [-o O] [-b B] [-m M]

optional arguments:
  -h, --help  show this help message and exit
  -w W        Wechat mini program ID.
  -i I        A <folder name> which contains multiple wxapkg files, or a single wxapkg <file name>.
  -o O        Output folder.
  -b B        True/False means whether to beautify the JS code, True will result in a poor
              performance.
  -m M        A 16-bytes local MAC package key. Looks like '00 11 22 33 44 55 66 77 88 99 AA BB CC
              DD EE FF'


最后于 2023-3-13 10:38 被今天是星期五编辑 ,原因:
2023-3-10 17:13
1
雪    币: 113
活跃值: (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3

在仔细研究了wxss的解析方法之后,修正了原来的wxss的错误解析方式(字符串查找、替代等),增加了对于op=0/1/2的处理,对于已生成wxss的引用处理。

最新修改日期:Mar. 24 2023,这应该是最后一个版本了,不想再改了。


from Crypto.Cipher import AES
import hashlib
import os
import json
import urllib
import jsbeautifier
import execjs
import argparse
import os
import copy

# 微信小程序文件格式
# 文件头
# 1字节 一定是190
# 4字节 一定是0
# 4字节 索引段长度
# 4字节 数据段长度
# 1字节 一定是237
# 4字节 文件总个数
# 索引段
# 4字节 文件名长度
# N字节 文件名
# 4字节 文件在数据段中的位置(相对于header的0偏移,而不是如下数据段的0偏移)
# 4字节 文件长度
# 数据段

# 1、先把mac os的SIP做一个disable or enable SIP,否则lldb不能attach到wechat进程上。
# 2、打开wechat,但是不登录。
# 3、lldb -p wechat的pid。
# 4、br set -n sqlite3_key,断点设置好后,c继续运行。
# 5、微信登录后,会break到断点上,输入memory read --size 1 --format x --count 32 $rsi
# 6、前16位即是你本机的wechat小程序加密的密钥,而完整的32位则是本机微信聊天记录sqlite db的密钥。

OUTPUT_FOLDER = "output"
NEED_BEAUTIFY_JS = False


WXML_CONTENT = ["", 0]
COMMON_STYLESHEETS = {}

def get_string_by_seperators(source, begin_str, end_str, begin_index):
    index = source.find(begin_str, begin_index)
    if index == -1:
        return "", -1

    index2 = source.find(end_str, index + len(begin_str))
    if index2 == -1:
        return "", -1

    return source[index + len(begin_str):index2], index2 + len(end_str)

def decrypt(buf, wxid, local_mac_package_key):
    seek = 0 if len(local_mac_package_key)==16 else 6

    wx_header = buf[seek:seek+1024]
    wx_others = buf[seek+1024:]

    if len(local_mac_package_key)==16:
        cipher = AES.new(local_mac_package_key, AES.MODE_ECB)
        decrypted_wx_header = cipher.decrypt(wx_header)

        return decrypted_wx_header + wx_others
    else:
        aes_key = hashlib.pbkdf2_hmac('sha1', wxid.encode(), b"saltiest", 1000, 32)
        cipher = AES.new(aes_key,AES.MODE_CBC,b"the iv: 16 bytes")

        decrypted_wx_header = cipher.decrypt(wx_header)
        n = decrypted_wx_header[-1]
        if n > 0:
            decrypted_wx_header = decrypted_wx_header[:-n]

        xor_key = ord(str(wxid[-2]))
        decrypted_wx_others = []
        for b in wx_others:
            decrypted_wx_others.append(b ^ xor_key)

        return decrypted_wx_header + bytes(decrypted_wx_others)

def write_file(fname, buf, mode):
    items = fname.split("/")
    path = OUTPUT_FOLDER
    for i in range(0, len(items) - 1):
        path += "/" + items[i]
        md(path)

    f = open(path + "/" + items[-1], mode)
    f.write(buf)
    f.close()

def process_package(buf):
    index = 0
    # <editor-fold desc="处理微信头">
    print("magic number is " + str(ord(buf[index:1])))
    index += 1
    print("always o is {0}".format(int.from_bytes(buf[index:index + 4], "big")))
    index += 4
    index_seg_length = int.from_bytes(buf[index:index + 4], "big")
    print("index segments length is {0}".format(index_seg_length))
    index += 4
    body_seg_length = int.from_bytes(buf[index:index + 4], "big")
    print("body segments length is {0}".format(body_seg_length))
    index += 4
    print("last mask must be 237 ---> {0}".format(int.from_bytes(buf[index:index + 1], "big")))
    index += 1
    file_count = int.from_bytes(buf[index:index + 4], "big")
    print("file count is {0}".format(file_count))
    index += 4
    # </editor-fold">

    index_length = 0
    # <editor-fold desc="处理微信数据段">
    for fcount in range(0, file_count):
        if index_length + 4 >= index_seg_length:  # 如果用while true
            break
        filename_length = int.from_bytes(buf[index:index + 4], "big")
        index += 4
        fname = buf[index:index + filename_length].decode()
        index += filename_length
        offset_of_file_in_segment = int.from_bytes(buf[index:index + 4], "big")
        index += 4
        file_size = int.from_bytes(buf[index:index + 4], "big")
        index += 4

        print("File name length ={0}, file name = {1}, offset in segment = {2}, file size = {3}".format(filename_length,
                                                                                                        fname,
                                                                                                        offset_of_file_in_segment,
                                                                                                        file_size))

        content = buf[offset_of_file_in_segment:offset_of_file_in_segment + file_size]
        if fname.endswith(".json"):
            content = urllib.parse.unquote(json.dumps(json.loads(content.decode()), indent=4))
            write_file(fname, content, "w")
        elif fname.endswith(".js"):
            content = content.decode()
            if "app-service.js" not in fname and NEED_BEAUTIFY_JS is True:
                content = jsbeautifier.beautify(content)
            write_file(fname, content, "w")
        else:
            write_file(fname, content, "wb")
        index_length += 4 * 3 + filename_length
    # </editor-fold>

def md(dir):
    if os.path.exists(dir) is False:
        os.mkdir(dir)

def process_json(fname):
    if os.path.exists(fname) is False:
        return

    f = open(fname, "r")
    all_lines = f.readlines()
    f.close()

    token = ".json'] = {"
    jsons = ''.join(all_lines).split("__wxAppCode__[")

    for j in jsons:
        if token in j:
            index = j.index(token)
            fname = j[1:index] + ".json"
            print("Processing " + fname)
            content, index = get_string_by_seperators(j[index + len(token) - 1:], "{", "};", 0)
            content = json.dumps(json.loads("{" + content + "}"), indent=4)
            write_file(fname, content, "w")

def process_json2(fname):
    with open(fname,"r") as f:
        appjson = json.load(f)
    #appjson = copy.deepcopy(appjson)
    del appjson["page"]

    write_file("/app.json",json.dumps(appjson, indent=4),"w")

def process_js(js_file):
    if os.path.exists(js_file) is False:
        return

    f = open(js_file, "r")
    all_lines = f.readlines()
    f.close()

    items = ''.join(all_lines).split('define("')
    for i in range(1,len(items)):
        line = items[i]
        index = line.index(",")
        fname = line[0:index].replace('"','')

        index = line.index("{")
        line = line[index+1:].strip()

        index = line.rindex("});")
        line = line[0:index]

        if line.endswith("       ") is False and "}" in line:
            index = line.rindex("}")
            line = line[0:index]

        if line.startswith("'use strict';") or line.startswith('"use strict";'):
            line = line[13:]
        elif (line.startswith('(function(){"use strict";') or line.startswith(
                "(function(){'use strict';")) and line.endswith("})();"):
            line = line[25:][:-5]

        print("Processing " + fname)
        if NEED_BEAUTIFY_JS:
            write_file(fname, jsbeautifier.beautify(line), "w")
        else:
            write_file(fname, line, "w")

def get_wxss_content(buf):
    if type(buf).__name__=="str":
        return buf

    content = ""
    for item in buf:
        if type(item).__name__ == "str":
            content += item
        elif type(item).__name__ == "list":
            op = item[0]
            if op==0:
                content += str(item[1])#rpx不处理了,直接加
            elif op==1:
                pass
            elif op==2:
                wxss = COMMON_STYLESHEETS.get(item[1])
                if wxss is None:
                    pass
                else:
                    content += wxss
    return content

def process_wxss(fname):
    if os.path.exists(fname) is False:
        return

    f = open(fname, "r")
    all_lines = ''.join(f.readlines())
    f.close()

    jsons = all_lines.split("__COMMON_STYLESHEETS__['")
    for i in range(1,len(jsons)):
        index = jsons[i].find("[")
        index2 = jsons[i].find("];")
        if index>-1 and index2>-1:
            buf = eval(jsons[i][index:index2+1])
            COMMON_STYLESHEETS[jsons[i][0:index-3]] = get_wxss_content(buf)
    token = ".wxss"
    jsons = all_lines.split("setCssToHead(")

    for j in jsons:
        if token in j:
            index = j.find('.wxss"})', 0)
            if index == -1:
                continue
            buf = eval('['+j[0:index].replace("undefined",'[]').replace("path",'"path"') + '.wxss"}]')
            if len(buf[0])==0:
                continue

            wxss = get_wxss_content(buf[0])
            COMMON_STYLESHEETS[buf[2]["path"]] = wxss

            fname = buf[2]["path"].replace("./", "")
            print("Processing " + fname)
            write_file(fname, wxss, "w")

    return

def process_wxml_nodes(nodes):
    wxml = ""
    if WXML_CONTENT[1]>0:
        wxml = "\t" * WXML_CONTENT[1]

    if type(nodes).__name__ != "dict":
        WXML_CONTENT[0] += str(nodes)
        return

    tag = nodes["tag"].replace("wx-", "")
    wxml += "<" + tag

    if nodes.get("attr") is not None:
        for attr in nodes["attr"].keys():
            wxml += " " + attr + "=\"" + str(nodes["attr"][attr]) + "\""
    wxml += ">"
    wxml += "\n"

    WXML_CONTENT[0] += wxml

    WXML_CONTENT[1] += 1
    for child in nodes["children"]:
        process_wxml_nodes(child)
    WXML_CONTENT[1] -= 1

    WXML_CONTENT[0] += "</" + tag + ">\n"

    return

def process_wxml_remove_useless(wxml_source):
    source = wxml_source

    tmp = get_string_by_seperators(wxml_source,"<script>","</script>",0)[0]
    if len(tmp)>0:
        index = tmp.find("var setCssToHead")
        index2 = tmp.rindex(");")
        source = tmp[0:index]+tmp[index2+2:]

    else:
        first_token = 'if (!noCss)'
        last_token = 'var __subPageFrameEndTime__ = Date.now();'
        index = wxml_source.find(first_token)
        index2 = wxml_source.find(last_token, index)

        if index>-1 and index2>-1:
            source = wxml_source[0:index] + wxml_source[index2:]

    return source

def process_wxml(pageframe):
    if os.path.exists(pageframe) is False:
        return

    source = open(pageframe).read()
    if source=="/* This file is left intentionally blank */":
        return

    flist = "\n"
    patch = 'var window={};var navigator={};navigator.userAgent="iPhone";window.screen={};document={};function define(){};function require(){};function setCssToHead(file, _xcInvalid, info){};'

    items = source.split("else __wxAppCode__[")
    x = []
    index = 0
    for item in items:
        func = get_string_by_seperators(item,"=",";",0)[0]
        if "$" not in func:
            continue
        flist += "fuck_{0}={1};\n".format(index,func.strip())
        index+=1
        x.append(get_string_by_seperators(item,"'","'",0)[0])

    source = process_wxml_remove_useless(source)
    patched_source = patch + source + flist

    js = execjs.compile(patched_source)


    for func_no in range(0, len(x)):
        try:
            nodes = js.call("fuck_{0}".format(func_no),[],[],[],[])
        except Exception as e:
            print(e)
            continue

        fname = x[func_no].replace("./", "")

        print("Processing "+fname)
        if len(nodes["children"])==0:
            continue
        global WXML_CONTENT
        process_wxml_nodes(nodes["children"][0])
        write_file(fname, WXML_CONTENT[0], "w")

        WXML_CONTENT = ["", 0]

def process(flist,func):
    for f in flist:
        func(OUTPUT_FOLDER + f)

        with open(OUTPUT_FOLDER + "/app.json", "r") as fs:
            j = json.load(fs)
            for sub in j["subPackages"]:
                func(OUTPUT_FOLDER + "/" + sub["root"] + f)

def get_package_content(wx_package, wxid,local_mac_package_key):
    fsize = os.path.getsize(wx_package)

    f = open(wx_package, "rb")
    buf = f.read(fsize)
    f.close()

    if int(buf[0]) != 190: # or (buf[0]==b"V" and buf[1]==b"1" and buf[2]==b"M" and buf[3]==b"M" and buf[4]==b"W" and buf[5]==b"X"):
        buf = decrypt(buf, wxid,local_mac_package_key)

    if int(buf[0]) != 190:
        buf = None

    return buf

def init():
    parser = argparse.ArgumentParser()
    parser.add_argument("-w", help="Wechat mini program ID.")
    parser.add_argument("-i", help="A <folder name> which contains multiple wxapkg files, or a single wxapkg <file name>.")
    parser.add_argument("-o", help="Output folder.")
    parser.add_argument("-b",default=False, help="True/False means whether to beautify the JS code, True will result in a poor performance.")
    parser.add_argument("-m",default="", help="A 16-bytes local MAC package key. Looks like '00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF'")

    args = parser.parse_args()

    global OUTPUT_FOLDER
    OUTPUT_FOLDER = args.o
    if args.o is None:
        OUTPUT_FOLDER = "output"

    global NEED_BEAUTIFY_JS
    NEED_BEAUTIFY_JS = args.b


    wxid = args.w
    input = args.i
    local_mac_package_key = bytes.fromhex(args.m)

    wxapkg = []

    if input is None:
        input = "."

    if ".wxapkg" in input and os.path.exists(input):
        wxapkg.append(input)
    elif os.path.exists(input):
        for fp,dirs,fs in os.walk(input):
            for f in fs:
                if ".wxapkg" in f:
                    wxapkg.append(os.path.join(fp,f))

    return wxid,wxapkg,local_mac_package_key

def main():
    wxid, wxapkg,local_mac_package_key = init()
    if wxid is None or len(wxapkg)==0:
        print("Error wxid or input files. Type < python3 wx.py --help > to get more information.")
        return

    md(OUTPUT_FOLDER)

    print("Unpacking package...")
    for apkg in wxapkg:
        buf = get_package_content(apkg,wxid,local_mac_package_key)
        if buf is not None:
            process_package(buf)

    print("================================================================")
    print("")
    print("Unpacking JSON files...")
    process_json(OUTPUT_FOLDER+"/app-service.js")

    if os.path.exists(OUTPUT_FOLDER + "/app.json") is False:
        process_json2(OUTPUT_FOLDER + "/app-config.json")

    print("================================================================")
    print("")
    print("Unpacking JS files...")
    process(["/app-service.js"], process_js)


    print("================================================================")
    print("")
    print("Unpacking WXSS files...")
    process(["/app-wxss.js","/page-frame.js", "/page-frame.html"], process_wxss)

    print("================================================================")
    print("")
    print("Unpacking WXML files...")
    process(["/app-service.js", "/page-frame.js", "/page-frame.html"], process_wxml)

if __name__ == "__main__":
    main()


最后于 2023-3-24 08:49 被今天是星期五编辑 ,原因:
2023-3-10 17:14
2
雪    币: 3836
活跃值: (4142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
谢谢分享
2023-3-10 18:09
0
雪    币: 97
活跃值: (818)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
不会用 
2023-3-11 01:37
0
雪    币: 1503
活跃值: (1784)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6

测试了一下,貌似还是有点问题


2023-3-11 10:07
0
雪    币: 113
活跃值: (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
milko 测试了一下,貌似还是有点问题
出问题的包发给我一下
2023-3-13 07:56
0
雪    币: 221
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8

我一开始有个这样的错误,然后我把from helper import helper改成import helper就没了。

然后又出了这个错误 helper这个包有点问题。。

 

2023-3-13 09:46
1
雪    币: 113
活跃值: (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
Judong0x0 我一开始有个这样的错误,然后我把from helper import helper改成import helper就没了。然后又出了这个错误&nbsp;helper这个包有点问题。。&n ...

那个是我自己的helper辅助类,我把方法从里面拿出来了,代码如下:

def get_string_by_seperators(source, begin_str, end_str, begin_index):
    index = source.find(begin_str, begin_index)
    if index == -1:
        return "", -1
 
    index2 = source.find(end_str, index + len(begin_str))
    if index2 == -1:
        return "", -1
 
    return source[index + len(begin_str):index2], index2 + len(end_str)


2023-3-13 10:40
1
雪    币: 97
活跃值: (818)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
最新的提示如下

Error wxid or input files. Type < python3 wx.py --help > to get more information.
2023-3-13 12:47
0
雪    币: 97
活跃值: (818)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
这个windows不适用?我已经有了小程序包
2023-3-13 12:52
0
雪    币: 113
活跃值: (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
windows下的encoding貌似有些问题,我没有windows环境,你可以debug一下看看。
2023-3-14 08:37
0
雪    币: 107
活跃值: (404)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
感谢分享,学习了
2023-3-14 13:25
0
雪    币: 14806
活跃值: (6043)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
milko 测试了一下,貌似还是有点问题
import sys
sys.setdefaultencoding('utf-8') #set default encoding to utf-8
测试看?
2023-3-15 09:17
1
雪    币: 1216
活跃值: (2821)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
15
非常漂亮
2023-3-15 10:00
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
你好,请问能具体指导一下吗?
2023-3-25 09:44
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
大神,还是不会用啊!
2023-3-31 15:18
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
大神,全网就你这有新版小程序逆向的办法。 能出个详细点的教程吗? 不用具体操作啊。 求大神给小白指条明路
2023-4-1 07:03
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19

大神你好,我现在想查看微信小程序在sha256加密之前参数,请问应该在哪设置断点?   4

2023-4-2 09:59
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20

佬 解析不出app-config呀

2023-4-2 16:04
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21

佬 这是成功了嘛 但是比wxappunpacker解析出来少了很多文件

2023-4-2 16:25
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
楼上,你解析成功之后,能在微信开发工具中运行吗?
2023-4-2 21:14
0
雪    币: 312
活跃值: (123)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
23
mark
2023-4-10 11:06
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
您好这个报错在wxml文件这里,能有空帮忙看下吗
2023-5-17 14:37
0
雪    币: 409
活跃值: (750)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
25
ReferenceError: $gwx_XC_0 is not defined

请问这个需要如何解决?
2023-5-24 10:15
0
游客
登录 | 注册 方可回帖
返回
//