吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 9928|回复: 78
上一主题 下一主题
收起左侧

[原创] 破解 [CrackMe] 我又来了,这次用py的nuitka,我打包工具的另一个分支 思路

  [复制链接]
跳转到指定楼层
楼主
神奇的人鱼 发表于 2025-9-28 18:11 回帖奖励
1. 看到楼主又搞了一个python的逆向,再来玩玩吧,原帖地址:https://www.52pojie.cn/thread-2062856-1-1.html

2. 可以看到这次使用的是nuitka那这个就不像之前那么简单了
添加了upx壳直接 upx -d 脱壳

脱壳后,可以看到是nuikta直接打包单文件

3. 提取相关文件,可以通过github上的一个库提取内置的二进制文件,可以通过监控程序查看释放路径
地址:https://github.com/extremecoders-re/nuitka-extractor

其中main.dll是关键代码所在

4. 打开ida开始分析main.dll
导入表有一个run_code,显然是主要内容

继续跟进

这个地方就是关键,可以把这个代码扔给AI





🔥 关键赋值:冻结模块表

  PyImport_FrozenModules = PyImport_FrozenModules_0;
  sub_180001F20(v6);
  sub_1800D3090(v6);
  • PyImport_FrozenModules 是一个指向 struct _frozen* 数组的指针。
  • PyImport_FrozenModules_0 很可能是你自己所有被编译进来的 .py 文件的字节码数据(以 C 数组形式嵌入)!

🎯 这是最接近源码的地方!

🔍 建议你现在立即查看:

.ro0000018011A300 PyImport_FrozenModules_0 dq offset frozen_name_main
.ro0000018011A308                 dq offset code_main
.ro0000018011A310                 dd  length_main

如果能找到类似结构,你就拿到了原始模块的编译后代码对象(虽然是 marshal 序列化的字节码,但可提取)!



知道了这个地方获取冻结模块表,那就需要找到PyImport_FrozenModules_0来源

继续跟进

这里获取了资源,nuitka会将python的字节码写入在资源段中

5. 提取资源
使用Resource Hacker提取id为3的资源,可以看到里面的一些字节码,还有一些库的注释,这就是我们需要的

6. 解析字节码
但是我们不知道如何解析,上网搜索后找到如下帖子:
https://goatmilkk.notion.site/Nuitka-a3ac9ee7f3f240f3baa345c17f2b8aa3
https://blog.svenskithesource.be/posts/flare10-07-(nuitka)/
https://services.google.com/fh/files/misc/7-flake-flareon10.pdf
最后一篇是google官方教你如何逆向一个nuitka的CTF挑战,非常有帮助
根据文档编写解析代码
[Python] 纯文本查看 复制代码
import io
import struct

def read_uint32(bio):
    return struct.unpack("<I", bio.read(4))[0]

def read_uint16(bio):
    return struct.unpack("<H", bio.read(2))[0]

def read_utf8(bio):
    bs = b""

    while True:
        bs += bio.read(1)
        if b"\x00" in bs:
            break
    return bs[:-1].decode("utf-8")

def main():
    with open("main.bin", "rb") as f_in:
        bs = f_in.read()

    bio = io.BytesIO(bs)
    hash_ = read_uint32(bio)
    size = read_uint32(bio)
    print(f"hash: {hex(hash_)}")
    print(f"size: {hex(size)}")

    while bio.tell() < size:
        blob_name = read_utf8(bio)
        blob_size = read_uint32(bio)
        blob_count = read_uint16(bio)
        print(f"name: {blob_name}, size: {hex(blob_size)}, count: {hex(blob_count)}")
        bio.seek(bio.tell() + (blob_size - 2))

if __name__ == "__main__":
    main()

运行后结果如下:
[Python] 纯文本查看 复制代码
hash: 0x705322c1
size: 0x6075a9
name: .bytecode, size: 0x5f750a, count: 0x156
name: , size: 0x361, count: 0x69
name: PIL._version, size: 0x8d, count: 0xc
name: PIL, size: 0x75a, count: 0x23
name: __main__, size: 0x1490, count: 0x28
name: pystray._appindicator, size: 0x5c0, count: 0x60
name: pystray._base, size: 0x3590, count: 0x11c
name: pystray._darwin, size: 0xed9, count: 0xcc
name: pystray._dummy, size: 0xf7, count: 0xd
name: pystray._gtk, size: 0x4ed, count: 0x4a
name: pystray._util, size: 0x3c3, count: 0x29
name: pystray._util.gtk, size: 0xb7c, count: 0x97
name: pystray._util.notify_dbus, size: 0x4b6, count: 0x4b
name: pystray._util.win32, size: 0xd22, count: 0x103
name: pystray._win32, size: 0x17e6, count: 0xf8
name: pystray._xorg, size: 0x196a, count: 0x114
name: pystray, size: 0x37a, count: 0x3c
name: six, size: 0x37f0, count: 0x223
name: tkinter-preLoad, size: 0xbb, count: 0x11
name: watchdog, size: 0xd9, count: 0x10

可以看到一个关键__main__
我们继续编写函数解析这个main,文档下面也有解析的地方我们抄过来
[Python] 纯文本查看 复制代码
import io
import sys
import struct


def read_uint8(bio):
    return struct.unpack("<B", bio.read(1))[0]

def read_uint16(bio):
    return struct.unpack("<H", bio.read(2))[0]

def read_uint32(bio):
    return struct.unpack("<I", bio.read(4))[0]

def read_utf8_size_1(bio):
    return bio.read(1).decode("utf-8")

def read_utf8(bio):
    bs = b""
    while True:
        c = bio.read(1)
        if c == b"\x00" or not c:  # 读取到空字节或文件末尾时停止
            break
        bs += c
    return bs.decode("utf-8")

def read_bytearray(bio):
    bs = b""
    while True:
        c = bio.read(1)
        if c == b"\x00" or not c:
            break
        bs += c
    return bs

def decode_blob(bio, count):
    container = []
    for i in range(count):
        type_ = chr(read_uint8(bio))
        if type_ in ('a', 'u'):
            o = read_utf8(bio)
        elif type_ == 'l':
            o = read_uint32(bio)
        elif type_ == 'w':
            o = read_utf8_size_1(bio)
        elif type_ == 'T':
            sub_count = read_uint32(bio)
            o = tuple(decode_blob(bio, sub_count))
        elif type_ == 'c':
            o = read_bytearray(bio)
        elif type_ == 'b':
            size = read_uint32(bio)
            o = bio.read(size)
        else:
            raise ValueError(f"unhandled type {type_}")
        container.append(o)
    return container


def main():
    with open("main.bin", "rb") as f_in:
        bs = f_in.read()
    bio = io.BytesIO(bs)

    hash_ = read_uint32(bio)
    size = read_uint32(bio)
    # print(f"Header Hash: {hex(hash_)}")
    # print(f"Total Size: {hex(size)}")

    while bio.tell() < size:
        blob_name = read_utf8(bio)
        blob_size = read_uint32(bio)
        blob_count = read_uint16(bio)
        # print(f"Found Blob - Name: '{blob_name}', Size: {hex(blob_size)}, Count: {hex(blob_count)}")

        # 检查是否是我们要找的 '__main__' 数据块
        if blob_name == "__main__":
            print("Decoding '__main__' blob...")
            # 解码 '__main__' 数据块中的所有对象
            decoded = decode_blob(bio, blob_count)
            # 打印出每个对象的索引和值
            for idx, obj in enumerate(decoded):
                print(f"{idx}: {obj}")
            break  # 如果只需要 __main__,找到后就可以退出

        else:
            # 跳过当前数据块的剩余部分
            bio.seek(bio.tell() + (blob_size - 2))


if __name__ == "__main__":
    main()

我们运行后发现报错了,提示没有类型B:

那大概率是nuitka版本变更导致解析方式也变了,我们只能返回ida找到新的解析办法,也是成功找到的解析函数,直接把仍给AI,让AI仿照文章中的代码给出一个

很快啊,AI给出了代码:
[Python] 纯文本查看 复制代码
import io
import sys
import struct


def read_uint8(bio):
    return struct.unpack("<B", bio.read(1))[0]


def read_uint16(bio):
    return struct.unpack("<H", bio.read(2))[0]


def read_uint32(bio):
    return struct.unpack("<I", bio.read(4))[0]


# --- 新增:实现LEB128解码 ---
def read_leb128(data_stream):
    """
    从字节流中读取一个无符号LEB128编码的整数。
    LEB128是一种可变长度的整数编码方式。
    """
    result = 0
    shift = 0
    while True:
        byte_val = data_stream.read(1)
        if not byte_val:
            raise EOFError("Unexpected end of stream while reading LEB128")
        byte_val = ord(byte_val)  # 转换为整数

        # 取低7位,左移相应位数后加到结果上
        result |= (byte_val & 0x7F) << shift

        # 如果最高位是0,表示这是最后一个字节
        if not (byte_val & 0x80):
            break

        shift += 7  # 每个字节提供7位有效数据

    return result


# --- 结束新增 ---

def decode_blob(bio, count):
    container = []
    for i in range(count):
        type_byte = read_uint8(bio)
        type_char = chr(type_byte)

        if type_char == 'n':
            # Py_None: 直接使用None
            o = None
        elif type_char == 't':
            # Py_True: 直接使用True
            o = True
        elif type_char == 'F':
            # Py_False: 直接使用False
            o = False
        elif type_char in ('a', 'u'):
            # 字符串/Unicode: 读取以空字节结尾的UTF-8字符串
            bs = b""
            while True:
                c = bio.read(1)
                if c == b"\x00" or not c:
                    break
                bs += c
            o = bs.decode("utf-8")
        elif type_char == 'w':
            # 单字符字符串: 读取一个字节并解码
            o = bio.read(1).decode("utf-8")
        elif type_char == 'l' or type_char == 'q':
            # 有符号整数: 使用LEB128读取,如果是'q'则取负值
            value = read_leb128(bio)
            if type_char == 'q':
                value = -value
            o = value
        elif type_char == 'T':
            # 元组: 用LEB128读取子元素数量,递归解码
            sub_count = read_leb128(bio)
            o = tuple(decode_blob(bio, sub_count))
        elif type_char == 'L':
            # 列表: 用LEB128读取元素数量,递归解码
            list_count = read_leb128(bio)
            o = [decode_blob(bio, 1)[0] for _ in range(list_count)]
        elif type_char == 'D':
            # 字典: 用LEB128读取键值对数量,交替读取key和value
            dict_count = read_leb128(bio)
            o = {}
            for _ in range(dict_count):
                key = decode_blob(bio, 1)[0]
                value = decode_blob(bio, 1)[0]
                o[key] = value
        elif type_char == 'S' or type_char == 'P':
            # 集合/冻结集合: 用LEB128读取元素数量,递归解码
            set_count = read_leb128(bio)
            elements = [decode_blob(bio, 1)[0] for _ in range(set_count)]
            if type_char == 'S':
                o = set(elements)
            else:  # 'P'
                o = frozenset(elements)
        elif type_char == 'B':
            # 字节数组: 用LEB128读取长度,再读取指定长度的字节
            length = read_leb128(bio)
            o = bio.read(length)
        elif type_char == 'c':
            # 字节字符串 (interned): 读取以空字节结尾的字节序列
            bs = b""
            while True:
                c = bio.read(1)
                if c == b"\x00" or not c:
                    break
                bs += c
            o = bs
        elif type_char == 'b':
            # 字节字符串: 用LEB128读取长度,再读取指定长度的字节
            length = read_leb128(bio)
            o = bio.read(length)
        elif type_char == 'd':
            # 浮点数 (double): 读取一个字节索引,用于查找预定义浮点数
            # **注意**:这是一个简化处理。实际中'd'指向一个全局数组中的索引。
            # 为了通用性,我们将其视为需要特殊处理的类型,这里先返回索引。
            index = read_uint8(bio)
            # 在真实分析中,你需要知道这个全局数组的内容。
            # 此处我们返回一个占位符或索引本身。
            o = f"<PREDEFINED_FLOAT_INDEX_{index}>"
        elif type_char == 'f':
            # 浮点数 (float): 读取8字节双精度浮点数
            float_bytes = bio.read(8)
            o = struct.unpack('<d', float_bytes)[0]
        elif type_char == 'j':
            # 复数 (complex): 读取两个8字节双精度浮点数 (real, imag)
            real_bytes = bio.read(8)
            imag_bytes = bio.read(8)
            real = struct.unpack('<d', real_bytes)[0]
            imag = struct.unpack('<d', imag_bytes)[0]
            o = complex(real, imag)
        elif type_char == 'v':
            # 长度可变的UTF-8字符串: 用LEB128读取长度,再读取指定长度的UTF-8字节并解码
            length = read_leb128(bio)
            string_bytes = bio.read(length)
            o = string_bytes.decode('utf-8')
        elif type_char == 's':
            # 长度可变的UTF-8字符串 (interned): 与'v'类似,但会进行"驻留"(interning)
            # 对于我们的目的,解码方式相同。
            length = read_leb128(bio)
            string_bytes = bio.read(length)
            o = string_bytes.decode('utf-8')
        elif type_char == 'M':
            # 匿名对象: 读取一个字节来区分不同的特殊类型 (如 None, Ellipsis, NotImplemented 等)
            anon_type = read_uint8(bio)
            # 根据文档中的switch语句,映射这些匿名类型
            anon_map = {
                0: "Py_None",
                1: "PyEllipsis_Type",
                2: "Py_NotImplementedStruct",
                3: "PyFunction_Type",
                4: "PyGen_Type",
                5: "PyCFunction_Type",
                6: "PyCode_Type",
                7: "PyModule_Type"
            }
            o = anon_map.get(anon_type, f"<ANON_TYPE_{anon_type}>")
        elif type_char == 'O':
            # 动态属性获取: 这是一个复杂的操作,通常用于获取内置模块的属性。
            # 它会一直读取直到遇到空字节,然后调用 PyObject_GetAttrString。
            # 我们无法在静态解析中模拟其结果,只能记录其行为。
            attr_name = ""
            while True:
                c = bio.read(1)
                if c == b"\x00" or not c:
                    break
                attr_name += c.decode("utf-8")
            o = f"<DYNAMIC_ATTR_GET:{attr_name}>"
        elif type_char == 'Q':
            # 特殊值: 类似'M',读取一个字节来区分 NotImplmented, Ellipsis 等。
            special_type = read_uint8(bio)
            special_map = {
                0: "Ellipsis",
                1: "NotImplemented",
                2: "<SELF_REFERENCE>"  # 文档中对应 ::AttrString
            }
            o = special_map.get(special_type, f"<SPECIAL_VALUE_{special_type}>")
        elif type_char == 'X':
            # 字节偏移: 用LEB128读取一个长度,然后将数据指针向前移动该长度。
            # 这不是一个独立的对象,而是一种跳过数据的方式。
            # 在Nuitka的上下文中,它可能用于对齐或填充。
            skip_length = read_leb128(bio)
            # 移动文件指针
            bio.seek(bio.tell() + skip_length)
            # 这种类型不产生一个可以放入列表的Python对象。
            # 我们可以跳过它,或者用一个标记表示。
            continue  # 跳过此条目,不添加到container中
        elif type_char == 'Z':
            # 预定义浮点数: 读取一个字节索引,类似于'd'。
            # 也需要特殊处理。
            index = read_uint8(bio)
            o = f"<PREDEFINED_DOUBLE_INDEX_{index}>"
        elif type_char == 'C':
            # 代码对象: Nuitka用于创建Python函数/方法的复杂结构。
            # 包含版本、参数数量、标志等信息。
            # 解析它需要非常深入的知识,这里只做简单展示。
            version = read_leb128(bio)
            argcount = read_leb128(bio)
            flags = read_leb128(bio)
            # 后续还有更多字段,如默认参数、注解等...
            o = f"<CODE_OBJECT_VERSION_{version}_ARGCOUNT_{argcount}_FLAGS_{flags}>"
        elif type_char == 'A':
            # 泛型别名: 用于typing模块,如 List[int]。
            # 需要两个参数:原始类型和参数。
            origin = decode_blob(bio, 1)[0]
            args = decode_blob(bio, 1)[0]
            o = f"<GENERIC_ALIAS_ORIGIN_{origin}_ARGS_{args}>"
        elif type_char == ';':
            # Lambda表达式: 一种特殊的代码对象。
            # 解析方式与'C'类似但更简单。
            code_obj = decode_blob(bio, 1)[0]
            defaults = decode_blob(bio, 1)[0]
            closure = decode_blob(bio, 1)[0]
            o = f"<LAMBDA_CODE_{code_obj}_DEFAULTS_{defaults}_CLOSURE_{closure}>"
        elif type_char == ':':
            # 切片对象: 创建一个slice(start, stop, step)。
            start = decode_blob(bio, 1)[0]
            stop = decode_blob(bio, 1)[0]
            step = decode_blob(bio, 1)[0]
            o = slice(start, stop, step)
        elif type_char == 'p':
            # 堆栈引用: 引用前面已经解码过的对象。
            # 这是一个相对引用(通常是前一个对象)。
            # 在我们的线性解码中,这很难准确模拟,因为它依赖于解码顺序。
            o = "<STACK_REFERENCE_PREV>"
        else:
            raise ValueError(f"unhandled type {type_char} (0x{type_byte:02X})")

        container.append(o)
    return container


def main():
    with open("main.bin", "rb") as f_in:
        bs = f_in.read()
    bio = io.BytesIO(bs)

    hash_ = read_uint32(bio)
    size = read_uint32(bio)

    while bio.tell() < size:
        blob_name = ""
        while True:
            c = bio.read(1)
            if c == b"\x00" or not c:
                break
            blob_name += c.decode("utf-8")

        blob_size = read_uint32(bio)
        blob_count = read_uint16(bio)

        if blob_name == "__main__":
            print(
                f"Decoding '__main__' blob (Name: '{blob_name}', Size: {hex(blob_size)}, Count: {hex(blob_count)})...")
            decoded = decode_blob(bio, blob_count)
            for idx, obj in enumerate(decoded):
                print(f"{idx}: {obj}")
            break
        else:
            bio.seek(bio.tell() + (blob_size - 2))


if __name__ == "__main__":
    main()

运行后:其中的base64代码省略
[Python] 纯文本查看 复制代码
Decoding '__main__' blob (Name: '__main__', Size: 0x1490, Count: 0x28)...
0: __mro_entries__
1: bases
2: __iter__
3: __getitem__
4: %s argument after * must be an iterable, not %s
5: __name__
6: keys
7: %s argument after ** must be a mapping, not %s
8: %s got multiple values for keyword argument '%s'
9: called
10: star_arg_dict
11: star_arg_list
12: kw
13: b''
14: decrypted
15: append
16: key_len
17: XOR 解密函数
18: __doc__
19: __file__
20: __cached__
21: __annotations__
22: base64
23: {'data': 'key', 'return': '<DYNAMIC_ATTR_GET:bytes>', '<STACK_REFERENCE_PREV>': '<STACK_REFERENCE_PREV>'}
24: xor_decrypt
25: my_secret_key_9456821
26: ONEKEY
27: utf-8
28: SECRET_KEY
29: YHM2HhUMABFULBIWdFVQWUVZSkYR.........M+d/cs48YFhwTRHRV
30: ENCRYPTED_B64
31: b64decode
32: encrypted_data
33: decrypted_script
34: {'__name__': '__main__'}
35: <string>
36: exec
37: main.py
38: <module>
39: ('data', 'key', 'key_len', 'decrypted', 'i', 'byte')

通过这个,我们可以还原原始的python代码了:
[Python] 纯文本查看 复制代码
import base64

# 第一步:Base64解码
b64_data = ("YHM2HhUMABFULBIWdFVQWUVZSkYRDxgsFlNXf28SLQQIWTtYQFBCUV9UTRAyAwoRBkUQPh8ADTZUUTg8NThuLjAPOyA"
            "xUlhUeD4OPxJjBllTWXVnWyE1Nh8uWTwmEQIBNQ0LWFFXf2QeNT0aBCg3MwwhGjkqIzIAUlQEYgUWYHMAOCA6UlhUeD"
            "0gPw9jBgxQWQBnWCElOhIuGDAceGZvJgxtZnx4f2ERUFkEVJX86OVUuvvKnNWR0Zuo3Iy6TTgDHUJPUkKEwP/1WbeWg"
            "92Iq9e0yJzw9YLD84rIxUxJWXjbqLAW0Zi9hdbele3zl+/rsNfkJTEeGBURyK2i6Fm6zvaG++iSyN2M7usDFBJrNTg8"
            "ZzYPLDU2IS1UYktVAW8IOT95aG19IjgbLDY3IEVJf1sdSWk0PnpmZ3FwITV/TkVTClVHUmEqKQBzeWVpcXRuIzx/TkV"
            "TClVAUmEqKQBxdXliGA8RXQFvRmhpf28QOg1FJjtcV0dPSEYZCRgrEklDGUxOUmFFWX8ZRlBCTUBfTRsmBwAQWgdUAU"
            "sOIjYZERVaXVwZBlACUwMMAEUdc0sHWTZXFFBYTV9UHxgrFk0HExEVdkJoc1IzUFBQGG1VBxttWxZKSGh+f0tFWTcZC"
            "RUDCwoAYHN/U0VDFAoGfwhFEDEZRw87MhIRTVl/U0VDGkVJf0NNEX8FCBUDERIaTRF2UztDEWh+f0tFWS1cQEBEVhJZ"
            "TV9/Qx0lNCMyGS0jP1IzOT9VVFNCHlkJPl9ueEVUf0sBHDkZa2pfVltFMiZ3AAAPFElUPAQBHHYDOT8WGBIRTVl/UxY"
            "GHgNaPAQBHH8EFFZZXFc8Z1l/U0VDUkVULA4JH3FJVxULGAI8Z1l/U0VDUkVULA4JH3FKQFRVUxIMTSICfm9DUkVUf0"
            "tFWSxcWFMYSldWTUR/KFVPUlUpUmFFWX8ZFBUWGEFUAR9xGwQNFgkRLEtYWQRkOT87MhIRTVk7FgNDABAadxgAFTkQD"
            "jg8GBIRTVl/U0UXABxOUmFFWX8ZFBUWGBIRTVkoGwwPF0UHOgcDVy9aFAkWVFdfRQo6HwNNEQoQOkJfdFUZFBUWGBIR"
            "TVl/U0VDUkVUMBtFRH9KUVlQFlFeCRwEAAAPFEsEPDZoc38ZFBUWGBIRTVl/U0VDUkUHOgcDVy9aFB4LGAM8Z1l/U0V"
            "DUkVUf0tFWX8ZFBVfXhJeHVliTkUsIjokCjgtQ1IzFBUWGBIRTVl/U0VDUkVUf0tFWX9QUhVFXV5XQwk8U05DRkVKfw"
            "cAF3dKUVlQFlFeCRx2SWhpUkVUf0tFWX8ZFBUWGBIRTVl/U0VDUkVUPRkAGDQ0PhUWGBIRTVl/U0VDUkVUf0tFWX8ZQ"
            "hULGFtfGVc5AQoOLQcNKw4WUSxcWFMYW11VCCIsFgkFXBUXZRgAFTkXRFYdDG8dTV4zGhEXHgBTdmZvWX8ZFBUWGBIR"
            "TVl/U0VDUkVUf0sWHDNfGkZCWVFaQxgvAwANFk0CdmZvWX8ZFBUWGBIRTVl/U0VDUkVUf0sWHDNfGkVVGBkMTU1SeUV"
            "DUkVUf0tFWX8ZFBUWGBJUARA5UwoTUlhJfyQ1JhN2dXFpa2ZjV3RVU0VDUkVUf0tFWX8ZFBUWGBIRTVk2FUUQFwkScR"
            "sGWWEEFFlTVhpCCBU5XQYMFgBdZWZvWX8ZFBUWGBIRTVl/U0VDUkVUf0tFWX8ZVkdTWVk8Z1l/U0VDUkVUf0tFWX8ZF"
            "BUWGBIRHhwzFUsRFwIvbzZFRH9KUVlQFlFeCRwEAAAPFEsEPDZoc38ZFBUWGBIRTVl/U0VDUkVUf0tFCjpVUhtGWxIa"
            "UFlufm9DUkVUf0tFWX8ZFBUWGBIRCBU2FUUMAkVJYksqKQB6dXl6Aj87TVl/U0VDUkVUf0tFWX8ZFBUWGBJYC1ksFgk"
            "FXBUXf1VYWTNcWh1FXV5XQxowFwBKSGh+f0tFWX8ZFBUWGBIRTVl/U0VDUkVUf0tFGy1cVV47MhIRTVl/U0VDUkVUf0"
            "tFWX8ZFBUWS1ERUFksFgkFXAYbOw4+CjpVUhtGW288Z1l/U0VDUkVUf0tFWX8ZFBUWGBIRHhwzFUsTEUVfYktUdFUZF"
            "BUWGBIRTVl/U0VDUkVUf0tFWTZfFEZVGA8MTUllfm9DUkVUf0tFWX8ZFBUWGBIRTVl/U0VDUkUELQILDXdmZ2FkcXx2"
            "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"
            "+d/cs48YFhwTRHRV")

decoded_b64 = base64.b64decode(b64_data)

# 第二步:XOR解密
key = b"my_secret_key_9456821"
decrypted_bytes = bytearray()
for i in range(len(decoded_b64)):
    decrypted_byte = decoded_b64[i] ^ key[i % len(key)]
    decrypted_bytes.append(decrypted_byte)

# 结果是原始的Python源代码
original_code = decrypted_bytes.decode('utf-8')
print(original_code)

运行结果:
[Python] 纯文本查看 复制代码
import sys
import base64
from datetime import datetime

_CIPHER = 'UkFMZ2leaGV6XjEzM+YRNidLR2ldaGV/XDEwMTAiUEROZm9fa2Z7'
_KEY = 'VEFPZ29fa2V5XzIwMjUh'
_STRINGS = ['&#128640; 启动实例 A\n', '&#128272; 请输入密码:', '&#9989; 验证成功!\n', '&#128197; 当前时间: ']

OP_PUSH = 0x01
OP_LOAD_STR = 0x06
OP_CALL = 0x03
OP_JMP_IF_NE = 0x04
OP_HALT = 0x05

def _decrypt(data, k):
    return bytes(b ^ k[i % len(k)] for i, b in enumerate(data))

def _djb2(s):
    h = 5381
    for c in s:
        h = ((h << 5) + h) ^ c
    return h & 0xFFFFFFFF

class VM:
    def __init__(self, code):
        self.code = code
        self.pc = 0
        self.stack = []
        self.reg = [0, 0]
        self.handles = []

    def run(self):
        try:
            while self.pc < len(self.code):
                op = self.code[self.pc]
                self.pc += 1
                if op == OP_PUSH:
                    if self.pc + 4 > len(self.code):
                        break
                    v = int.from_bytes(self.code[self.pc:self.pc+4], 'little')
                    self.stack.append(v)
                    self.pc += 4
                elif op == OP_LOAD_STR:
                    if self.pc >= len(self.code):
                        break
                    self.reg[0] = self.code[self.pc]
                    self.pc += 1
                elif op == OP_CALL:
                    if self.pc >= len(self.code):
                        break
                    sc = self.code[self.pc]
                    self.pc += 1
                    if sc == 0:
                        print(_STRINGS[self.reg[0]], end='', flush=True)
                    elif sc == 1:
                        s = input().strip().encode()
                        self.handles.append(s)
                        self.reg[0] = len(self.handles) - 1
                    elif sc == 2:
                        code = self.stack.pop() if self.stack else 1
                        sys.exit(code)
                    elif sc == 3:
                        h = self.reg[0]
                        self.reg[1] = _djb2(self.handles[h]) if 0 <= h < len(self.handles) else 0
                    elif sc == 4:
                        h = self.reg[0]
                        if 0 <= h < len(self.handles):
                            print(self.handles[h].decode('utf-8', 'replace'), flush=True)
                    elif sc == 5:
                        t = datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode()
                        self.handles.append(t)
                        self.reg[0] = len(self.handles) - 1
                elif op == OP_JMP_IF_NE:
                    if self.pc >= len(self.code):
                        break
                    target = self.code[self.pc]
                    self.pc += 1
                    actual = self.reg[1]
                    expected = self.stack.pop() if self.stack else 0
                    if actual != expected:
                        if 0 <= target < len(self.code):
                            self.pc = target
                elif op == OP_HALT:
                    return
        except Exception as e:
            print(f"\n[VM ERROR] {e}", file=sys.stderr)
            sys.exit(1)

# 解密并运行
cipher = base64.b64decode(_CIPHER)
key = base64.b64decode(_KEY)
code = _decrypt(cipher, key)
VM(code).run()

if sys.platform == "win32":
    input("按回车键退出...")

7. 破解密码
我们已经得到了作者的原始代码,现在观察这个代码,是一个简单的虚拟机,字节码如下
让AI自动去分析,{:301_997:} 太懒了{:301_1007:}
可以直接必须输入一个hash 是 1717711059 才能通过
8. 编写破解hash的代码,github:https://github.com/m9psy/DJB2_collision_generator/tree/master/djb2_collision_generator
魔改一下:
generator.py
[Python] 纯文本查看 复制代码
# -*- coding: utf-8 -*-
"""
增强版 DJB2 哈希碰撞生成器

功能:
&#9989; 给定目标哈希值,生成能产生该哈希的输入字符串
&#9989; 支持指定生成数量(如只生成 5 个)
&#9989; 可选仅使用“可见字符”(可打印 ASCII)
&#9989; 自动扩展长度以寻找更多碰撞
"""

from __future__ import print_function

import warnings
import random
import copy
import time
import sys

# 导入哈希函数
from hash_functions import djb2_32, djb2_64, djb2_xor_32


class GreedyGenerator:
    """
    贪心式哈希碰撞生成器(支持可见字符限制)
    """

    DEFAULT_LENGTH = 16              # 减小默认长度以便更快找到结果
    TOTAL_ROUNDS = 10                # 每轮优化次数
    MUTATION_MOD = 2                 # 突变比例:修改一半字节
    VERBOSE = False                  # 是否输出调试信息

    # 定义可见字符范围(ASCII 32 ~ 126)
    PRINTABLE_RANGE = list(range(32, 127))

    def __init__(self, hash_function, target_value, total_letters=DEFAULT_LENGTH, only_printable=False):
        """
        初始化生成器
        :param hash_function: 哈希函数(如 djb2_32)
        :param target_value: 目标哈希值
        :param total_letters: 字符串长度
        :param only_printable: 是否仅使用可见字符
        """
        if total_letters <= 0:
            raise ValueError("字符串长度必须大于 0")

        self.TOTAL_LETTERS = total_letters
        self.BYTES_TO_MUTATE = max(1, self.TOTAL_LETTERS // self.MUTATION_MOD)
        self.string_to_guess = bytearray(b'\x00' * self.TOTAL_LETTERS)
        self.checker = hash_function
        self.target = target_value
        self.only_printable = only_printable  # 是否限制为可见字符
        self.allowed_bytes = self.PRINTABLE_RANGE if only_printable else list(range(256))

    def set_target(self, new_target):
        self.target = new_target

    def set_collision_size(self, new_size):
        """动态调整字符串长度"""
        if new_size <= 0:
            raise ValueError("长度必须 > 0")
        old_val = self.string_to_guess[:min(self.TOTAL_LETTERS, new_size)]
        self.TOTAL_LETTERS = new_size
        self.BYTES_TO_MUTATE = max(1, self.TOTAL_LETTERS // self.MUTATION_MOD)
        new_guess = bytearray(b'\x00') * self.TOTAL_LETTERS
        for i in range(len(old_val)):
            new_guess[i] = old_val[i]
        self.string_to_guess = new_guess

    def __iter__(self):
        return self

    def __next__(self):
        return self.next()

    def next(self):
        """生成下一个可能的碰撞(返回 bytes)"""
        self.string_to_guess = self.mutate_full(self.string_to_guess)

        while True:
            prev_state = None

            for round_idx in range(self.TOTAL_ROUNDS):
                improved = False

                for pos in range(self.TOTAL_LETTERS):
                    candidates = self._get_hash_for_position(pos)
                    match = self._check_exact_hit(pos, candidates)
                    if match is not None:
                        return match

                    # 在允许的字节范围内选择最优值
                    best_byte = min(
                        self.allowed_bytes,
                        key=lambda b: abs(candidates.get(b, float('inf')) - self.target)
                    )

                    if self.string_to_guess[pos] != best_byte:
                        self.string_to_guess[pos] = best_byte
                        improved = True

                current_hash = self.checker(self.string_to_guess)
                if self.VERBOSE:
                    print(f"第 {round_idx} 轮 | 哈希={current_hash} | 误差={abs(current_hash - self.target)}")

                if not improved or self.string_to_guess == prev_state:
                    break
                prev_state = copy.copy(self.string_to_guess)

            self.string_to_guess = self.mutate_partial(self.string_to_guess)

    def _get_hash_for_position(self, index):
        """枚举当前位置所有允许字节对应的哈希值"""
        original = self.string_to_guess[index]
        results = {}

        for b in self.allowed_bytes:
            self.string_to_guess[index] = b
            results[b] = self.checker(self.string_to_guess)

        self.string_to_guess[index] = original
        return results

    def _check_exact_hit(self, index, candidates):
        """检查是否有字节能命中目标哈希"""
        for byte_val, h in candidates.items():
            if h == self.target and byte_val in self.allowed_bytes:
                self.string_to_guess[index] = byte_val
                return bytes(copy.copy(self.string_to_guess))
        return None

    def mutate_partial(self, data):
        """部分突变:只在允许字符集中随机替换"""
        for _ in range(self.BYTES_TO_MUTATE):
            pos = random.randint(0, self.TOTAL_LETTERS - 1)
            new_val = random.choice(self.allowed_bytes)
            data[pos] = new_val
        return data

    def mutate_full(self, data):
        """完全随机初始化(仅用允许字符)"""
        return bytearray(random.choices(self.allowed_bytes, k=self.TOTAL_LETTERS))


if __name__ == "__main__":
    """
    主入口:支持参数控制
    用法: python generator.py <目标哈希> [选项...]
    示例:
        python generator.py 1717864659
        python generator.py 123456789 -n 5 -p -l 20
    """

    if len(sys.argv) < 2:
        print("&#10060; 用法: python generator.py <目标哈希> [-n 数量] [-p] [-l 长度]", file=sys.stderr)
        print("选项:")
        print("  -n N      : 只生成 N 个结果(默认无限)")
        print("  -p        : 仅使用可见字符(可打印 ASCII)")
        print("  -l LENGTH : 设置初始字符串长度(默认 16)")
        sys.exit(1)

    # 解析目标哈希
    try:
        target_hash = int(sys.argv[1])
    except ValueError:
        print("&#10060; 错误:目标哈希必须是整数", file=sys.stderr)
        sys.exit(1)

    # 默认参数
    max_count = None           # 默认不限制数量
    only_printable = False     # 是否仅可见字符
    string_length = 16         # 初始长度

    # 解析命令行参数
    args = sys.argv[2:]
    i = 0
    while i < len(args):
        arg = args[i]
        if arg == '-n' and i + 1 < len(args):
            try:
                max_count = int(args[i+1])
                if max_count <= 0:
                    raise ValueError
                i += 1
            except Exception:
                print("&#10060; -n 后需接正整数", file=sys.stderr)
                sys.exit(1)
        elif arg == '-p':
            only_printable = True
        elif arg == '-l' and i + 1 < len(args):
            try:
                string_length = int(args[i+1])
                if string_length <= 0:
                    raise ValueError
                i += 1
            except Exception:
                print("&#10060; -l 后需接正整数", file=sys.stderr)
                sys.exit(1)
        else:
            print(f"&#10060; 未知参数: {arg}", file=sys.stderr)
            sys.exit(1)
        i += 1

    # 校验哈希范围并选择函数
    if target_hash < 0 or target_hash >= 2**64:
        print("&#10060; 哈希值必须在 [0, 2^64) 范围内", file=sys.stderr)
        sys.exit(1)

    # hash_func = djb2_64 if target_hash >= 2**32 else djb2_32
    hash_func = djb2_xor_32
    bits = "64位" if target_hash >= 2**32 else "32位"

    # 输出配置信息
    print(f"&#127919; 开始寻找哈希值 {target_hash}")
    print(f"&#128295; 使用 {bits} DJB2 算法")
    print(f"&#128207; 初始长度: {string_length}")
    print(f"&#128292; {'仅限可见字符' if only_printable else '允许所有字节'}")
    print(f"&#128202; {'最多生成 %d 个' % max_count if max_count else '无限生成'}")

    start_time = time.time()
    generator = GreedyGenerator(hash_func, target_hash, string_length, only_printable=only_printable)

    found_collisions = set()
    consecutive_misses = 0

    try:
        while True:
            try:
                candidate = next(generator)
                if candidate not in found_collisions:
                    try:
                        # 尝试以 UTF-8 解码显示(非必须)
                        # decoded = candidate.decode('utf-8', errors='backslashreplace')
                        print(f"{candidate.decode('utf-8', errors='backslashreplace')}")
                    except:
                        print(candidate)
                    found_collisions.add(candidate)
                    consecutive_misses = 0

                    # 如果达到指定数量,退出
                    if max_count and len(found_collisions) >= max_count:
                        break
                else:
                    consecutive_misses += 1

            except StopIteration:
                print(f"&#10145;&#65039;  当前长度({generator.TOTAL_LETTERS})无新解,扩展至 {generator.TOTAL_LETTERS + 1}...")
                generator.set_collision_size(generator.TOTAL_LETTERS + 1)
                consecutive_misses = 0

            # 提前结束判断
            if max_count and len(found_collisions) >= max_count:
                break

        # 主动结束输出统计
        end_time = time.time()
        elapsed = end_time - start_time
        print("\n" + "=" * 60)
        print("&#9989; 任务完成!")
        print(f"&#128278; 目标哈希: {target_hash}")
        print(f"&#128230; 生成数量: {len(found_collisions)}")
        print(f"&#9201;  耗时: {elapsed:.2f} 秒")
        if elapsed > 0:
            print(f"&#128640; 平均速度: {len(found_collisions) / elapsed:.4f} 个/秒")
        print("=" * 60)

    except KeyboardInterrupt:
        end_time = time.time()
        elapsed = end_time - start_time
        print("\n\n&#9208;&#65039;  用户中断")
        print(f"&#128204; 共生成 {len(found_collisions)} 个碰撞")
        print(f"&#9200; 耗时: {elapsed:.2f} 秒")
        if elapsed > 0:
            print(f"&#9889; 速度: {len(found_collisions) / elapsed:.4f} 个/秒")

hash_functions.py
[Python] 纯文本查看 复制代码
# -*- coding: utf-8 -*-
from functools import partial

MOD_32 = 2 ** 32
MOD_64 = 2 ** 64


def djb2(data, modulo=MOD_32):
    h = 5381
    for c in data:
        h = (h * 33 + c) % modulo
    return h

def djb2_xor_32(data, modulo=MOD_32):
    """
    &#9888;&#65039; 非标准“异或型”DJ B2(常出现在 CTF 或虚拟机中)
    公式: h = h * 33 ^ c
    注意:这不是原始 DJB2!但常被误称为 DJB2
    用途:混淆、反分析、小型 VM 验证
    """
    h = 5381
    for c in data:
        h = ((h << 5) + h) ^ c  # h * 33 ^ c
        h &= 0xFFFFFFFF  # 强制 32 位截断(即使 modulo 是 64 位)
    return h


djb2_32 = partial(djb2, modulo=MOD_32)
djb2_64 = partial(djb2, modulo=MOD_64)

运行后生成符合标准的密码:
[Python] 纯文本查看 复制代码
&#127919; 开始寻找哈希值 1717711059
&#128295; 使用 32位 DJB2 算法
&#128207; 初始长度: 6
&#128292; 仅限可见字符
&#128202; 最多生成 3 个
nT`Vlv
ou`X#W
i1"WO4

============================================================
&#9989; 任务完成!
&#128278; 目标哈希: 1717711059
&#128230; 生成数量: 3
&#9201;  耗时: 0.01 秒
&#128640; 平均速度: 428.0484 个/秒
============================================================

最终得到正确的密码,不唯一



免费评分

参与人数 33吾爱币 +36 热心值 +29 收起 理由
YouYou9649 + 1 我很赞同!
climb238 + 1 鼓励转贴优秀软件安全工具和文档!
maj001 + 2 + 1 我很赞同!
airzen + 1 + 1 我很赞同!
lingyun011 + 1 + 1 热心回复!
Lishi8 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
wuhuhu + 1 + 1 我很赞同!
lucien777 + 1 鼓励转贴优秀软件安全工具和文档!
ehu4ever + 1 + 1 谢谢@Thanks!
smallchop + 1 + 1 谢谢@Thanks!
FZZZP + 1 + 1 谢谢@Thanks!
tianwaiyoulong + 1 + 1 谢谢@Thanks!
fengbin8606 + 1 + 1 我很赞同!
yaojingls + 1 + 1 我很赞同!
allspark + 1 + 1 用心讨论,共获提升!
fengbolee + 2 + 1 我很赞同!
M79241 + 1 + 1 热心回复!
Yao2903 + 1 谢谢@Thanks!
ioyr5995 + 1 + 1 热心回复!
heju + 1 + 1 谢谢@Thanks!
nevinhappy + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
jaffa + 1 谢谢@Thanks!
hrh123 + 3 + 1 我很赞同!
xiaofeng4929 + 1 谢谢@Thanks!
khuntoria + 1 + 1 热心回复!
ZZ730605 + 1 + 1 我很赞同!
zuiai125520 + 2 + 1 热心回复!
15126819695 + 1 + 1 绝了,看到的第一篇逆向nuitka的 点赞
xuanle + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
隆菄 + 1 + 1 用心讨论,共获提升!
mjhwzwg6 + 1 + 1 用心讨论,共获提升!
bfloat16 + 1 + 1 我很赞同!
helian147 + 1 + 1 热心回复!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
nevinhappy 发表于 2025-9-30 18:52
这个源码是故意有个base64,再加载的吧,我发现我解出来的并没有base64部分,没办法还原的。
推荐
 楼主| 神奇的人鱼 发表于 2025-9-29 09:49 |楼主
沙发
Xzs1 发表于 2025-9-28 21:55
3#
ckpjasher 发表于 2025-9-28 22:11
大佬,那nuitka+vmp复杂度3%,破出来大概得多久?我用的加密是nuitka+vmp复杂度3%加壳,制作成本大概一个星期,破解成本很高吗?
4#
Teemo 发表于 2025-9-28 22:15
我愿称之为PythonCrackMe杀手!
现在通过AI来辅助分析和生成代码已经很方便了,可以提高很多效率啊。
看起来Nuitka并不像介绍的一样转成C源码后编译就还原不出来源码了啊,实际上是把Python 源代码解析成抽象语法树(AST),转成bytecode字节码后以数组形式放在C代码里编译的。
防止源码被还原这一块看起来是Cython会更好一些,是把pyx的Cython语法文件直接转成调用Python API的C代码的,然后再编译C代码。
5#
 楼主| 神奇的人鱼 发表于 2025-9-28 22:17 |楼主
Teemo 发表于 2025-9-28 22:15
我愿称之为PythonCrackMe杀手!
现在通过AI来辅助分析和生成代码已经很方便了,可以提高很多效率啊。
看 ...

AI帮忙很大,没有AI我搞不来
6#
 楼主| 神奇的人鱼 发表于 2025-9-28 22:19 |楼主
ckpjasher 发表于 2025-9-28 22:11
大佬,那nuitka+vmp复杂度3%,破出来大概得多久?我用的加密是nuitka+vmp复杂度3%加壳,制作成本大概一个星 ...

VMP的话,直接放弃了
7#
ckpjasher 发表于 2025-9-28 22:21
神奇的人鱼 发表于 2025-9-28 22:19
VMP的话,直接放弃了

那我放心了,我就怕几个小时内就被解决出来源码,那就太难受了,辛苦忙活一周
8#
Sandyang 发表于 2025-9-29 00:02
通过监控程序查看释放路径,什么监控程序?文件监控还是写入监控?
9#
khuntoria 发表于 2025-9-29 04:04
想问一下楼主,如果用的是pymraor加密的话,该怎么解密呢
10#
隆菄 发表于 2025-9-29 06:08
感谢分享思路清晰的分析和经验交流
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-2-1 17:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表