小程序逆向:混淆代码中16进制字符转换为字符串

504 阅读6分钟

文章涉及 repr() 和 literal_eval() 的用法

今天客户给了一个小程序源码,不过很可惜,是混淆后的,压根无法肉眼分析。。。。

image.png

看得我头皮发麻,但是硬着头皮也要进行审计,那么就先想着,先把这些 16 进制格式的字符串给转换为字符串吧。然后再进行 js 的格式化。

然后就不出意外的出了意外。

在一开始我发现,在 python 中,如果是 \x6a 格式的字符串,那么是可以直接输出为字符串的。

a = "\x6a"
print(a)

输出结果是 j

但是如果你从文件里读取字符,然后是没有办法进行这样的操作的。

a = r"\x6a"
print(a)

输出结果是 \x6a

就多了一个 r , 结果完全不同。

这里就涉及到一个问题,当 python 读取到一个字符串的时候,底层是会理解为 bytes 来进行处理的。

普通字符串带 r 的字符串
代码中"\x6a"r"\x6a"
bytes 表示6a5c 78 36 61

可以看到,带 r 和不带 r 的差别非常大,带 r 的意思是 4 个字符,不带 r 的意思是 1 个字符。

如果我们想要将 r"\x6a 转换成 "\x6a" 那么就要非常复杂了,你要截取后两位,然后转十进制,然后转 chr。

那么如果是中文 unicode 编码呢, 比如 \x61\x74\x74\x72,一个字符可不止占一个字节,还要涉及到编码转换。而且一个 js 中,还有其他那么多干扰字符呢。 光听起来低血压就要被治好了。

还好让我找到了一个简便的方法:

from ast import literal_eval

s = r"一些语句; attr = ['\x61\x74\x74\x72','\x0a', '\x61\x74\x74\x72']; 一些语句"

s = repr(s)

s = s.replace(r'\\', '\\')
# 这一句是我的特殊需求,我不要换行,因为我后面要格式化代码
s = s.replace(r'\x0a', r'\n')

s = literal_eval(s)

print(s)

repr() 的意思是将字符串格式化成机器可理解的字符:将字符串用单引号包裹,然后将其中影响理解的字符(主要就是单斜杠 )给转移一下。

literal_eval() 就是 repr 的逆方法。

那么正常情况下:literal_eval(repr(r"\x0a"))literal_eval(repr("\x0a")) 会发生什么呢?

literal_eval(repr(r"\x0a"))literal_eval(repr("\x0a"))
repr()"'\\x0a'""'\x0a'"
literal_eval()'\x0a'\n

repr() 输出的原本是机器理解的结果,但是为了大家的脑细胞考虑我给改写为人正常理解的。如果我这里不改写的话:也就是 "'\\x0a'" <=人的视角====机器视角=> "'\\\\x0a'""'\x0a'" <=人的视角====机器视角=> "'\\x0a'"

repr() 函数的功能是转义转义符号 \(也就是多加 \ 呗) ,然后两头添加了单引号而已。

正常情况下, 上述过程的示意图如下:

image.png

可以看到中间的两个字符串只相差一个斜杠而已,如果我们通过字符变换,将左边的字符串变成右边的字符串不久好了吗

image.png

而且这个字符串的操作也非常简单,就是替换一下就好。这样就简单实现了 4 个字符 "\" + "x" + "0" + "a" 到一个字符 换行 的变换。过程如下:

s = r'\x0a'
s = repr(s)
s = s.replace("\\\\", "\\")
s = literal_eval(s)
print(s)

当然了,如果你改造原始字符串也是可以的,在原始字符外边加单引号或者双引号都行。

from ast import literal_eval

s = r'\x0a'
s = '"' + s + '"'
# s = ' "\\x0a" '
s = literal_eval(s)
print(s)

# 或者

s = r'\x0a'
s = "'" + s + "'"
# s = " '\\x0a' "
s = literal_eval(s)
print(s)

# 或者

s = r"\x0a"
s = "'" + s + "'"
# s = " '\\x0a' "
s = literal_eval(s)
print(s)

# 或者
s = r"\x0a"
s = '"' + s + '"'
# s = ' "\\x0a" '
s = literal_eval(s)
print(s)

真有趣啊,没想到字符串外边加单双引号还有这么多结果。

序号相加结果
1单 + 双单双
2双 + 单双单
3单 + 单双单
4双 + 双单双

搞定了,就是这么简单,而且不受干扰字符的限制,就算你在 \x0a 前面或者后面添加别的字符,或者一段字符串中有多个这样的字符都不影响代码(如果有特殊的情况还是会报错的,我没有仔细研究到底是啥字符导致的。估计是触发了 python 库代码的 bug 吧留给有缘人研究了。)

下面就给他出题目的解决代码吧,可以输入一个文件夹的路径,然后遍历文件夹及其子文件夹中所有的 js 文件,然后读取文件内容,将其中的 16 进制格式的文本(其实是 unicode_escape 格式)转化为字符串,然后将 js 代码格式化一下。

import codecs
import os
from ast import literal_eval

import jsbeautifier

"""
小程序混淆过的源码里有很多的十六进制的数据,肉眼完全没法看,那么只能转换一下看看了
"""


def transform(target_file_path):
    """
    将代码中的 16 进制表示的字符串转换为字符串

    Parameters:
    target_file_path (string): 要进行转换的文件的路径

    Returns:
    boole: 转换是否成功,true 成功,false 失败

    Raises:
    Unknown: 我也不知道。

    Example:
    >> transform("/abc/aa.js")
    5
    """
    target_file = codecs.open(target_file_path, mode='r', encoding="UTF-8")
    content = target_file.read()
    target_file.close()

    content = repr(content).replace(r'\\', '\\')
    content = content.replace(r'\x0a', r'\\n')
    try:
        content = literal_eval(content)
    except SyntaxError:
        print("[E] 发生错误:" + target_file_path)
        return False

    # 如果先进行代码格式化的话,
    beautified_code = jsbeautifier.beautify(content)

    target_file = open(target_file_path, "w")
    target_file.write(beautified_code)
    target_file.close()

    return True


def beautify(target_file_path):
    target_file = codecs.open(target_file_path, mode='r', encoding="UTF-8")
    content = target_file.read()
    target_file.close()

    beautified_code = jsbeautifier.beautify(content)

    target_file = open(target_file_path, "w")
    target_file.write(beautified_code)
    target_file.close()
    pass


if __name__ == "__main__":
    # 设置要遍历的文件夹路径
    # folder_path = '/Users/sanqiushu/Code_Space/PyCharm_CE_Space/APPandMiniAPP/将小程序混淆代码中的十六进制转化为字符串/test'
    folder_path = '/Users/sanqiushu/Downloads/dist-pro'
    errFile = []
    # 使用os.walk()遍历文件夹
    for root, dirs, files in os.walk(folder_path):
        print("当前文件夹:" + root)
        # 遍历当前文件夹中的所有文件
        for file in files:
            file_path = os.path.join(root, file)
            if file_path.endswith(".js"):
                # 对文件内的 16 进制进行转换
                print("开始处理:" + file_path)
                if not transform(file_path):
                    errFile.append(file_path)
            else:
                print(file_path + ": 非 js 文件,跳过")
    for i in errFile:
        beautify(i)

完全不需要自己去正则匹配 16 进制的格式,然后自己转化什么的,方便了很多。