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

查看 58|回复 6
作者:神奇的人鱼   
1. 看到楼主又搞了一个python的逆向,再来玩玩吧,原帖地址:https://www.52pojie.cn/thread-2062856-1-1.html


image.png (55.91 KB, 下载次数: 0)
下载附件
2025-9-28 17:18 上传

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


image.png (46.21 KB, 下载次数: 1)
下载附件
2025-9-28 17:22 上传

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


image.png (45.39 KB, 下载次数: 0)
下载附件
2025-9-28 17:23 上传

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


image.png (20.15 KB, 下载次数: 1)
下载附件
2025-9-28 17:28 上传

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


image.png (89.8 KB, 下载次数: 0)
下载附件
2025-9-28 17:28 上传

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


image.png (9.71 KB, 下载次数: 1)
下载附件
2025-9-28 17:29 上传

继续跟进


image.png (9.02 KB, 下载次数: 1)
下载附件
2025-9-28 17:30 上传

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


image.png (50.57 KB, 下载次数: 0)
下载附件
2025-9-28 17:31 上传

 关键赋值:冻结模块表
  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来源


    image.png (36.86 KB, 下载次数: 1)
    下载附件
    2025-9-28 17:35 上传

    继续跟进


    image.png (36.22 KB, 下载次数: 1)
    下载附件
    2025-9-28 17:35 上传

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


    image.png (32.21 KB, 下载次数: 0)
    下载附件
    2025-9-28 17:36 上传

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


    image.png (114 KB, 下载次数: 0)
    下载附件
    2025-9-28 17:37 上传

    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]
    运行后结果如下:
    [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]
    我们运行后发现报错了,提示没有类型B:


    image.png (121.68 KB, 下载次数: 1)
    下载附件
    2025-9-28 17:47 上传

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


    image.png (38.84 KB, 下载次数: 1)
    下载附件
    2025-9-28 17:49 上传

    很快啊,AI给出了代码:
    [Python] 纯文本查看 复制代码import io
    import sys
    import struct
    def read_uint8(bio):
        return struct.unpack("[B]"
            elif type_char == 'f':
                # 浮点数 (float): 读取8字节双精度浮点数
                float_bytes = bio.read(8)
                o = struct.unpack('")
            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""
            elif type_char == 'Q':
                # 特殊值: 类似'M',读取一个字节来区分 NotImplmented, Ellipsis 等。
                special_type = read_uint8(bio)
                special_map = {
                    0: "Ellipsis",
                    1: "NotImplemented",
                    2: ""  # 文档中对应 ::AttrString
                }
                o = special_map.get(special_type, f"")
            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""
            elif type_char == 'C':
                # 代码对象: Nuitka用于创建Python函数/方法的复杂结构。
                # 包含版本、参数数量、标志等信息。
                # 解析它需要非常深入的知识,这里只做简单展示。
                version = read_leb128(bio)
                argcount = read_leb128(bio)
                flags = read_leb128(bio)
                # 后续还有更多字段,如默认参数、注解等...
                o = f""
            elif type_char == 'A':
                # 泛型别名: 用于typing模块,如 List[int]。
                # 需要两个参数:原始类型和参数。
                origin = decode_blob(bio, 1)[0]
                args = decode_blob(bio, 1)[0]
                o = f""
            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""
            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 = ""
            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()
    运行后:其中的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': '', '': ''}
    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:
    36: exec
    37: main.py
    38:
    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"
                "PiIsFgkFXBcRODBVJAIVFFBYXA8WSlV/FQkWAQ1JCxkQHHY0PhUWGBIRTVl/U0VDUkVUf0tFWX8ZUVlfXhJCDlliTkV"
                "SSGh+f0tFWX8ZFBUWGBIRTVl/U0VDUkVUf0tFCn8EFFxYSEdFRVBxABERGxVcdkUAFzxWUFAeET87TVl/U0VDUkVUf0"
                "tFWX8ZFBUWGBIRTVl/AAAPFEscPgUBFTpKGlRGSFdfCVEsWmhpUkVUf0tFWX8ZFBUWGBIRTVl/U0VDUkVULA4JH3FLU"
                "VJtCG8RUFkzFgtLAQAYOUUNGDFdWFBFERIcTUhSeUVDUkVUf0tFWX8ZFBUWGBIRTVl/FgkKFEUHPEtYRH8LDjg8GBIR"
                "TVl/U0VDUkVUf0tFWX8ZFBUWGBIRDhY7FkVeUhYRMw1LCitYV14YSF1BRVB/GgNDAQAYOUUWDT5aXxVTVEFUTUhSeUV"
                "DUkVUf0tFWX8ZFBUWGBIRTVl/U0VDUhYNLEUAATZNHFZZXFcYYHN/U0VDUkVUf0tFWX8ZFBUWGBIRTRwzGgNDAQZUYl"
                "ZFSmU0PhUWGBIRTVl/U0VDUkVUf0tFWX8ZFBUWGFoRUFksFgkFXBcRODBVJFIzFBUWGBIRTVl/U0VDUkVUf0tFWX8ZF"
                "BUWS1ddC1ctFgI4QzhUYks6HTVbBh1FXV5XQxE+HQEPFxYvNzZMWTZfFAUWBA8RBVljUwkGHE0HOgcDVzdYWlFaXUEY"
                "TRwzAABDQmh+f0tFWX8ZFBUWGBIRTVl/U0VDUkURMwIDWSxaFAgLGAYLYHN/U0VDUkVUf0tFWX8ZFBUWGBIRTVl/U0U"
                "LUlhULA4JH3FLUVJtCG88Z1l/U0VDUkVUf0tFWX8ZFBUWGBIRTVl/UwwFUlVUY1ZFEX8FFFlTVhpCCBU5XQ0CHAEYOh"
                "hMQ1IzFBUWGBIRTVl/U0VDUkVUf0tFWX8ZFBUWGBIRTQktGgsXWhYRMw1LET5XUFlTS2lZMFc7FgYMFgBceB4RH3IBE"
                "xkWH0BUHRU+EABEW0lUOQcQCjcEYEdDXRs8Z1l/U0VDUkVUf0tFWX8ZFBUWGBIRCBU2FUUQEUVJYktQQ1IzFBUWGBIR"
                "TVl/U0VDUkVUf0tFWX8ZFBUWTBIMTR0+BwAXGwgRcQUKDncQGkZCSlRFBBQ6W0dGK0hRMkZAHX8cfA8TdQgUPlt2XQA"
                "NEQoQOkNMdFUZFBUWGBIRTVl/U0VDUkVUf0tFWX8ZFBVFXV5XQxE+HQEPFxZaPhsVHDFdHEEfNTgRTVl/U0VDUkVUf0"
                "tFWX8ZFBUWGBIRTVksFgkFXBcRODBVJH8EFFlTVhpCCBU5XQ0CHAEYOhhMWXIZBTg8GBIRTVl/U0VDUkVUf0tFWTpVX"
                "VMWV0IRUER/PDU8OCgkACIjJhF8Djg8GBIRTVl/U0VDUkVUf0tFWX8ZFBVfXhJCCBU5XRUAUltJfwcAF3dKUVlQFlFe"
                "CRx2SWhpUkVUf0tFWX8ZFBUWGBIRTVl/U0VDUkVUPRkAGDQ0PhUWGBIRTVl/U0VDUkVUf0tFWX8ZQFREX1dFTUR/AAA"
                "PFEsXMA8AIixcWFMYSFFsYHN/U0VDUkVUf0tFWX8ZFBUWGBIRTQo6HwNNAgZUdFZFSFIzFBUWGBIRTVl/U0VDUkVUf0"
                "tFWX9YV0FDWV4RUFksFgkFXBcRODBUJFIzFBUWGBIRTVl/U0VDUkVUf0tFWX9cTEVTW0ZUCVliUxYGHgNaLB8EGjQXR"
                "FpGEBsRBB9/AAAPFEsHKwoGEn9cWEZTGAI8Z1l/U0VDUkVUf0tFWX8ZFBUWGBIRBB9/EgYXBwQYf0pYWTpBRFBVTFdV"
                "V3RVU0VDUkVUf0tFWX8ZFBUWGBIRTVl/U0VDGwNUb0tZRH9NVUdRXUYRUVkzFgtLAQAYOUUGFjtcHQ87MhIRTVl/U0V"
                "DUkVUf0tFWX8ZFBUWGBIRTVl/U0UQFwkScRsGWWIZQFREX1dFYHN/U0VDUkVUf0tFWX8ZFBUWXV5YC1kwA0VeT0U7Dz"
                "QtOBNtDjg8GBIRTVl/U0VDUkVUf0tFWX8ZFBVEXUZEHxdSeUVDUkVUf0tFHCdaUUVCGHdJDhwvBwwMHEUVLEsAQ1Iz"
                "FBUWGBIRTVl/U0VDAhcdMR9NH31lWm5gdRJ0PysQIThDCQAJfUdFHzZVUQhFQUEfHg07FhcRW2h+f0tFWX8ZFBUWGB"
                "IRHgAsXQAbGxFcbkJoc1IzFxXen5HUwv+6ytOLzfWc/udoczxQRF1TShIMTRs+AABVRksWaV8BHDxWUFAeZ3F4PTEa"
                "IUxueA4RJktYWT1YR1AADBxTW007FgYMFgBcACAgIHY0PlZZXFcRUFkAFwAAABwEK0MGEC9RUUcaGFlUFFBSeTMuWg"
                "YbOw5MVy1MWh0fNTg8ZxA5UxYaAUsEMwoRHzBLWRULBRITGhAxQFdBSGh+f0tFWTZXREBCEBDX4fC66PuLz8Ody8WM"
                "+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 ^ 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 = ['🚀 启动实例 A\n', '🔐 请输入密码:', '✅ 验证成功!\n', '📅 当前时间: ']
    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  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 = 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
    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 哈希碰撞生成器
    功能:
    ✅ 给定目标哈希值,生成能产生该哈希的输入字符串
    ✅ 支持指定生成数量(如只生成 5 个)
    ✅ 可选仅使用“可见字符”(可打印 ASCII)
    ✅ 自动扩展长度以寻找更多碰撞
    """
    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")
            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 = old_val
            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 = 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)  [-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("❌ 错误:目标哈希必须是整数", file=sys.stderr)
            sys.exit(1)
        # 默认参数
        max_count = None           # 默认不限制数量
        only_printable = False     # 是否仅可见字符
        string_length = 16         # 初始长度
        # 解析命令行参数
        args = sys.argv[2:]
        i = 0
        while i = 2**64:
            print("❌ 哈希值必须在 [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"🎯 开始寻找哈希值 {target_hash}")
        print(f"🔧 使用 {bits} DJB2 算法")
        print(f"📏 初始长度: {string_length}")
        print(f"🔤 {'仅限可见字符' if only_printable else '允许所有字节'}")
        print(f"📊 {'最多生成 %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"➡️  当前长度({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("✅ 任务完成!")
            print(f"🔖 目标哈希: {target_hash}")
            print(f"📦 生成数量: {len(found_collisions)}")
            print(f"⏱  耗时: {elapsed:.2f} 秒")
            if elapsed > 0:
                print(f"🚀 平均速度: {len(found_collisions) / elapsed:.4f} 个/秒")
            print("=" * 60)
        except KeyboardInterrupt:
            end_time = time.time()
            elapsed = end_time - start_time
            print("\n\n⏸️  用户中断")
            print(f"📌 共生成 {len(found_collisions)} 个碰撞")
            print(f"⏰ 耗时: {elapsed:.2f} 秒")
            if elapsed > 0:
                print(f"⚡ 速度: {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):
        """
        ⚠️ 非标准“异或型”DJ B2(常出现在 CTF 或虚拟机中)
        公式: h = h * 33 ^ c
        注意:这不是原始 DJB2!但常被误称为 DJB2
        用途:混淆、反分析、小型 VM 验证
        """
        h = 5381
        for c in data:
            h = ((h
    运行后生成符合标准的密码:
    [Python] 纯文本查看 复制代码🎯 开始寻找哈希值 1717711059
    🔧 使用 32位 DJB2 算法
    📏 初始长度: 6
    🔤 仅限可见字符
    📊 最多生成 3 个
    nT`Vlv
    ou`X#W
    i1"WO4
    ============================================================
    ✅ 任务完成!
    🔖 目标哈希: 1717711059
    📦 生成数量: 3
    ⏱  耗时: 0.01 秒
    🚀 平均速度: 428.0484 个/秒
    ============================================================
    最终得到正确的密码,不唯一


    image.png (8.78 KB, 下载次数: 1)
    下载附件
    2025-9-28 18:09 上传



    image.png (7.97 KB, 下载次数: 1)
    下载附件
    2025-9-28 18:10 上传



    image.png (8.24 KB, 下载次数: 1)
    下载附件
    2025-9-28 18:10 上传

    字节, 下载次数

  • Xzs1   

    牛 我要学习
    ckpjasher   

    大佬,那nuitka+vmp复杂度3%,破出来大概得多久?我用的加密是nuitka+vmp复杂度3%加壳,制作成本大概一个星期,破解成本很高吗?
    Teemo   

    我愿称之为PythonCrackMe杀手!
    现在通过AI来辅助分析和生成代码已经很方便了,可以提高很多效率啊。
    看起来Nuitka并不像介绍的一样转成C源码后编译就还原不出来源码了啊,实际上是把Python 源代码解析成抽象语法树(AST),转成bytecode字节码后以数组形式放在C代码里编译的。
    防止源码被还原这一块看起来是Cython会更好一些,是把pyx的Cython语法文件直接转成调用Python API的C代码的,然后再编译C代码。
    神奇的人鱼
    OP
      


    Teemo 发表于 2025-9-28 22:15
    我愿称之为PythonCrackMe杀手!
    现在通过AI来辅助分析和生成代码已经很方便了,可以提高很多效率啊。
    看 ...

    AI帮忙很大,没有AI我搞不来
    神奇的人鱼
    OP
      


    ckpjasher 发表于 2025-9-28 22:11
    大佬,那nuitka+vmp复杂度3%,破出来大概得多久?我用的加密是nuitka+vmp复杂度3%加壳,制作成本大概一个星 ...

    VMP的话,直接放弃了
    ckpjasher   


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

    那我放心了,我就怕几个小时内就被解决出来源码,那就太难受了,辛苦忙活一周
    您需要登录后才可以回帖 登录 | 立即注册