From 9e3709a30fb566efd2ae8cbb1e3b401fcb80368c Mon Sep 17 00:00:00 2001 From: Lil-Ran Date: Tue, 15 Apr 2025 10:35:34 +0800 Subject: [PATCH] initial commit --- .gitignore | 8 + README.md | 196 ++++++++++++++++++ 工具/工具清单.md | 16 ++ 源码/source/clean.ps1 | 8 + 源码/source/gen-attachment.ps1 | 63 ++++++ 源码/source/obfuscator/pyhumor_runtime.c | 133 ++++++++++++ 源码/source/obfuscator/setup.py | 20 ++ 源码/source/task-src/get-marshaled.py | 7 + 源码/source/task-src/get-obfuscated.py | 30 +++ 源码/source/task-src/get-task-pyc.py | 3 + 源码/source/task-src/plain.py | 35 ++++ 解题/exp.py | 11 + ...Pyhumor_f9c556898dc4227298c77b72441b0196.zip | Bin 0 -> 21342 bytes 13 files changed, 530 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 工具/工具清单.md create mode 100644 源码/source/clean.ps1 create mode 100644 源码/source/gen-attachment.ps1 create mode 100644 源码/source/obfuscator/pyhumor_runtime.c create mode 100644 源码/source/obfuscator/setup.py create mode 100644 源码/source/task-src/get-marshaled.py create mode 100644 源码/source/task-src/get-obfuscated.py create mode 100644 源码/source/task-src/get-task-pyc.py create mode 100644 源码/source/task-src/plain.py create mode 100644 解题/exp.py create mode 100644 附件/Pyhumor_f9c556898dc4227298c77b72441b0196.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..214b0f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/*.zip +*.pdf + +# CPython 3.10 的 include 文件夹 +源码/source/obfuscator/include/ + +# libtomcrypt 的 src 文件夹 +源码/source/obfuscator/libtomcrypt/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6cdc0b --- /dev/null +++ b/README.md @@ -0,0 +1,196 @@ +# 永信至诚春秋Game CTF赛题设计说明 + +### [题目信息]: + +| 出题人 | 出题时间 | 题目名字 | 题目类型 | 难度等级 | 题目分值 | +|:------ |:-------- |:------- |:------- |:---- |:---- | +| LilRan | 20241226 | Pyhumor | Reverse | 6 | 500 | + +### [题目描述]: + +``` +对一款常见加密混淆器的简化和模仿。真是很幽默啊。 +``` + +### [题目考点]: + +``` +考察 SMC、动态调试获取数据的知识点。本题用自己实现的一个简化版 Pyarmor 来体现其基本原理,选手可以了解到 CPython pyd 扩展模块和 CPython PyCodeObject 结构。 +``` + +### [是否原创]: + +``` +是 +``` + +### [Flag]: + +`flag{9bc74ce3-a56d-467f-eb52-d5f3d8923c6f}` + +### [题目环境]: + +``` +只分发附件 +``` + +### [特别注意]: + +``` +(此项不需要向选手说明)出题和做题所有用到 Python 的步骤,版本都需要是 Python 3.10,高一点或低一点都不行。这是 CPython ABI 的要求 +``` + +### [题目制作过程]: + +1. 使用 C 语言编写混淆器运行时,源码位于 `obfuscator/pyhumor_runtime.c`,此文件与 Flag 内容无关 +2. 编译此运行时:以 `obfuscator/` 为工作目录,使用 Python 3.10 运行 `python setup.py build`(可能会遇到编译器找不到 Windows Kits 路径的问题,需要结合实际环境解决) +3. 使用 Python 编写 Flag 检查程序,源码位于 `task-src/plain.py`,其中包含 Flag 校验逻辑(如果修改此文件,与 Flag 无关的部分需保持代码不变) +4. 生成混淆脚本,即题目附件:以 `task-src/` 为工作目录,使用 Python 3.10 运行 `python get-obfuscated.py`,在此目录下生成的 `pyhumor_runtime.cp310-win_amd64.pyd` 和 `pyhumor.py` 即题目附件 +5. (可选)使用 `.pyc` 而不是 `.py` 作为题目附件:以 `task-src/` 为工作目录,使用 Python 3.10 运行 `python get-task-pyc.py` + +### [题目writeup]: + +题目给了 pyc 和 pyd,这两种文件都是与 Python 版本相关的,本题是 3.10。起手用 pycdc 反编译 pyc 得到: + +```python +# Source Generated with Decompyle++ +# File: pyhumor.pyc (Python 3.10) + +from pyhumor_runtime import __pyhumor__ +__pyhumor__(__name__, __file__, b'PYHUMOR\x00\x00\x03\n...') # 省略 +``` + +看这段代码与常见加密混淆工具 Pyarmor 很相似,都是通过 CPython 扩展模块,从一段字节串中解密原程序运行。 + +用 IDA 反编译 pyhumor_runtime.cp310-win_amd64.pyd,从字符串 `pyhumor_runtime` 或 `__pyhumor__` 的交叉引用,定位到 sub_180002A40 为主要逻辑。 + +要实现将字节串转换为符合 CPython 解释器 ABI 的结构并与之交互,无非这两种可能: + +- 在 C 层面遍历数据并构造 PyObject,相当于另外实现一套 CPython +- 先将字节串解密为 PyCodeObject 或源码字符串,然后由 Python 反射运行 + +从实现的难度来看显然后者可能性更大,从 sub_180002A40 后几行的 `PyImport_ExecCodeModuleObject` 也能知道。 + +sub_180002A40 依次进行了以下操作: + +- 解析函数传参,`PyArg_ParseTuple_SizeT` 的后四个参数从左到右依次是 `(PyObject* __name__, PyObject* __file__, char* input_code, Py_ssize_t input_code_len)` +- 检查字节串指明的 Python 版本号、长度 +- malloc 一段内存来存放解密后的字节串 +- 通过很长的算法来解密(其实是 libtomcrypt 的 AES_ECB) +- 将字节串 unmarshal 成 PyObject +- 往 globals 里注册 `__humor_enter__` 函数为 sub_180002950 +- Eval 上面的 PyObject +- 从 globals 里删除 `__humor_enter__` + +注意到字节串会被 unmarshal 成可执行的 PyObject,而实际上 pyc 文件格式就是 marshal 然后前面加个 Magic Number 得到的。所以可以在 `PyMarshal_ReadObjectFromString` 处打断点,调试获得解密后的字节串。(此函数两个参数分别为字节数组和长度) + +*感兴趣的可在 Python 安装目录下 include 文件夹中找到 PyCodeObject 结构体的定义,并将其导入到 IDA,观察内存中的实例* + +为了调试 pyd,我们可以把调用者改成下面这样,然后趁着 sleep 的时候将调试器附加到进程: + +```python +import os +import time +from pyhumor_runtime import __pyhumor__ + +print(os.getpid()) +time.sleep(20) +__pyhumor__(__name__, __file__, b'PYHUMOR\x00\x00\x03\n...') # 省略 +``` + +拿到解密后 unmarshal 前的 PyCodeObject,我们既可以将它传递给 dis.dis,也可以补上与附件 pyc 相同的文件头,然后用 pycdas 反汇编。dis 得到: + +```python +>>> import dis +>>> import marshal +>>> +>>> code = marshal.loads(bytes.fromhex('''E300000000000000000000000000000000070000004000000073140100006500830001007A7873A805686DEFF01C36058A753A614B72CEF927609136F0E0AEC31293A93659474E8E26093235E694EA9C2D0F783A23D92C7ED02D4A25C07E852FE2D6390BD6D21DAA0238AF09DCCF120AC1CDF261C3F55E9DFDCCB61517EF8F008F0BC9188365B085DBD5B055F4B3CA57EC415C89DFC55461B3AA68A26316B234B84F949A27B40E1216FFCBF29530971A3C919EBF9EF2105FDFDEEB0E28A596123FACF53C928C4CDDDE31617009569E8A99F6546D34BB24DA3A0EDA096A4015CBA7B44929184C9DCD2E798EDA2E5D750BE8F4DB3642819C7D84877E365CDCC6C50A355E4862B08F766429C8C50DA130E83FD40FF4948930716A435CF95BABCCD18587C1A0B3746404830101005900640553002916E9000000002901DA067368613235362901DA067265647563657A11456E74657220796F757220666C61673A20E9010000004EE9030000005A03626967690100803B696ED1590969F0FFFFFF5A306463663536343736343537383830626635623339623239353431366632363762376136333633323462616561653166646302000000000000000000000002000000020000004300000073080000007C007C014100530029014EA9002902DA0178DA017972060000007206000000FA083C66726F7A656E3EDA083C6C616D6264613E0E00000073020000000800720A000000E9020000005A0557726F6E67E955000000E907000000E9080000006C160000000028B3313666676CED18E619E018F833E01B30783362E330F85831303478CC60D8428959DD307311434B9B015A0552696768747A2E53746F70207265766572736520656E67696E656572696E67206D652C20656E6A6F7920796F757220646179203A2929145A0F5F5F68756D6F725F656E7465725F5F5A07686173686C69627202000000DA0966756E63746F6F6C737203000000DA05696E707574DA04666C6167DA03696E74DA0A66726F6D5F6279746573DA06656E636F6465DA09686578646967657374DA08656E647377697468DA036D6170DA036F72645A0767726F75705F31DA03616C6CDA057072696E74DA0465786974DA03616363DA01697206000000720600000072060000007209000000DA083C6D6F64756C653E010000007330000000060202020C010C01080220031C01160104FD080608010801040208010C010801160108020E0108020E01060208010E010B0B0B0B0B0B0B0B0B0B0B''')) +>>> dis.dis(code) + 3 0 LOAD_NAME 0 (__humor_enter__) + 2 CALL_FUNCTION 0 + 4 POP_TOP + + 5 6 SETUP_FINALLY 120 (to 248) + + 6 8 POP_JUMP_IF_TRUE 168 (to 336) + 10 DUP_TOP_TWO +Traceback (most recent call last): + File "", line 1, in + File "...\lib\dis.py", line 79, in dis + _disassemble_recursive(x, file=file, depth=depth) + File "...\lib\dis.py", line 376, in _disassemble_recursive + disassemble(co, file=file) + File "...\lib\dis.py", line 372, in disassemble + _disassemble_bytes(co.co_code, lasti, co.co_varnames, co.co_names, + File "...\lib\dis.py", line 404, in _disassemble_bytes + for instr in _get_instructions_bytes(code, varnames, names, + File "...\lib\dis.py", line 340, in _get_instructions_bytes + argval, argrepr = _get_name_info(arg, names) + File "...\lib\dis.py", line 304, in _get_name_info + argval = name_list[name_index] +IndexError: tuple index out of range +>>> +``` + +可以发现,最前面对 `__humor_enter__` 的调用已经反汇编出来了,且字节串中可以看到 `Stop reverse engineering me` 等常量(pycdas 可以列出全部常量),但是后面的字节码仍然是错乱的。然而程序又可以正常运行,所以需要继续分析 `__humor_enter__` 即 sub_180002950。 + +sub_180002950 是一个 SMC 解密函数。通过 `PyEval_GetFrame()` 获得了当前 Python 函数调用栈帧(调用 C 函数不会增加一个栈帧),从中获得了正在运行的 PyCodeObject 指针,从中获得了指令字节码(co_code)的字节数组,并再次解密。这样的话,在 sub_180002950 返回后,下一条指令恰好已被解密,使得 CPython 能够运行下去。 + +*注:Python 中的 bytes 是不可变的,但我们是在用 C 语言操纵内存;自 Python 3.11 起此方法失效,因为获得的可能是字节码的一个副本。* + +所以可以用同样的方法,在再次解密前记录下 co_code 内存地址和密文,在 `return Py_NoneStruct;` 前导出完全解密的 co_code 数组,并用它替换掉之前导出的字节串相应位置,补上 pyc 文件头然后用 pycdc 反编译: + +```python +# Source Generated with Decompyle++ +# File: final.pyc (Python 3.10) + +__humor_enter__() + +try: + from hashlib import sha256 + from functools import reduce + flag = input('Enter your flag: ') + group_1 = [ + int.from_bytes(flag[1::3].encode(), 'big') % 998244353 == 156881262, + sha256(flag[-16:].encode()).hexdigest().endswith('dcf56476457880bf5b39b295416f267b7a636324baeae1fd'), + reduce((lambda x, y: x ^ y), map(ord, flag)) == 2] + if not all(group_1): + print('Wrong') + exit(1) + acc = 0 + for i in flag: + i = ord(i) ^ 85 + acc |= i + acc <<= 7 if i & 1 else 8 + if acc == 0xCDCB4322E6C376CC4C2D8C199E0D1818D8F861C788CFC181BE067F06380CF318EDD8CF98D98D9A800L: + print('Right') +finally: + return None + print('Wrong') + exit(1) + return None + print('Stop reverse engineering me, enjoy your day :)') + exit(1) + return None +``` + +可以看到主体部分已经还原了。group_1 的三个不可逆校验是为了防止通过构造具有魔术方法的类来追踪打印计算过程“一把梭”。 + +由于前面分析过程较繁琐,算法上就不设置得太复杂了,只通过后面的或运算和左移运算就可以反推出 flag: + +```python +target = 0xCDCB4322E6C376CC4C2D8C199E0D1818D8F861C788CFC181BE067F06380CF318EDD8CF98D98D9A800 +flag = [] +while target != 0: + target >>= (7 if target & 0x80 else 8) + if target != 0: + flag.append((target & 0x7F) ^ 85) +flag = bytes(flag[::-1]).decode() +print(flag) + +# flag{9bc74ce3-a56d-467f-eb52-d5f3d8923c6f} +``` + +### 注意事项 + ++ 无 \ No newline at end of file diff --git a/工具/工具清单.md b/工具/工具清单.md new file mode 100644 index 0000000..055fd6c --- /dev/null +++ b/工具/工具清单.md @@ -0,0 +1,16 @@ +# 使用工具: + +## 常见工具: + + 1. IDA Pro + 2. Python == 3.10 (注意必须是 3.10,高一点或者低一点都不可以) + 3. pycdc + 4. 010 Editor + +## 较少见工具: + + 1. 无 + +## 自创工具: + + 1. 无 \ No newline at end of file diff --git a/源码/source/clean.ps1 b/源码/source/clean.ps1 new file mode 100644 index 0000000..5f1db7d --- /dev/null +++ b/源码/source/clean.ps1 @@ -0,0 +1,8 @@ +# ȷĿ¼ΪűĿ¼ +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $scriptDir + +Remove-Item "obfuscator/build" -Recurse -Force +Remove-Item "task-src/pyhumor.py" -Force +Remove-Item "task-src/pyhumor.pyc" -Force +Remove-Item "task-src/pyhumor_runtime.cp310-win_amd64.pyd" -Force diff --git a/源码/source/gen-attachment.ps1 b/源码/source/gen-attachment.ps1 new file mode 100644 index 0000000..1be6c5a --- /dev/null +++ b/源码/source/gen-attachment.ps1 @@ -0,0 +1,63 @@ +# ȷĿ¼ΪűĿ¼ +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $scriptDir + +# ȷ Python 汾Ϊ 3.10 +$pythonVersion = & python --version 2>&1 +if ($pythonVersion -notlike "Python 3.10*") { + Write-Host "`n[!] Python 汾 3.10ǰ汾Ϊ $pythonVersion" -ForegroundColor Red + exit 1 +} + +# ʱ +Set-Location "obfuscator" +Write-Host "`n[+] ڱʱ...`n" -ForegroundColor Cyan +$env:INCLUDE="$env:INCLUDE;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared" +$env:LIB="$env:LIB;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64" +& python setup.py build +if ($LASTEXITCODE -ne 0) { + Write-Host "`n[!] ʱʧܣ" -ForegroundColor Red + exit 1 +} + +# ȷ pycryptodome +$pycryptodome = & pip list 2>&1 | Select-String "pycryptodome" +if (-not $pycryptodome) { + Write-Host "`n[!] pycryptodome δװڰװ..." -ForegroundColor Yellow + & pip install pycryptodome + if ($LASTEXITCODE -ne 0) { + Write-Host "`n[!] װ pycryptodome ʧܣ" -ForegroundColor Red + exit 1 + } +} + +# ɻű +Set-Location "../task-src" +Write-Host "`n[+] ɻű..." -ForegroundColor Cyan +& python get-obfuscated.py +if ($LASTEXITCODE -ne 0) { + Write-Host "`n[!] ɻűʧܣ" -ForegroundColor Red + exit 1 +} + +# pyc +& python get-task-pyc.py +if ($LASTEXITCODE -ne 0) { + Write-Host "`n[!] pyc ʧܣ" -ForegroundColor Red + exit 1 +} + +# +Write-Host "`n[+] ڴ..." -ForegroundColor Cyan +$zipFile = "../attachment.zip" +if (Test-Path $zipFile) { + Remove-Item $zipFile +} +Compress-Archive -Path pyhumor.pyc,pyhumor_runtime.cp310-win_amd64.pyd -DestinationPath $zipFile +if ($LASTEXITCODE -ne 0) { + Write-Host "`n[!] ʧܣ" -ForegroundColor Red + exit 1 +} + +Write-Host "`n[+] ɳɹ clean.ps1 ʱļ" -ForegroundColor Green +Set-Location $scriptDir diff --git a/源码/source/obfuscator/pyhumor_runtime.c b/源码/source/obfuscator/pyhumor_runtime.c new file mode 100644 index 0000000..eb8ef32 --- /dev/null +++ b/源码/source/obfuscator/pyhumor_runtime.c @@ -0,0 +1,133 @@ +#define PY_SSIZE_T_CLEAN +#define Py_CPYTHON_FRAMEOBJECT_H +#include "Python.h" +#include "marshal.h" +// #include "internal/pycore_frame.h" +#include "cpython/frameobject.h" + +#define ARGTYPE 1 +#include "libtomcrypt/headers/tomcrypt.h" + +static const int MAJOR_VERSION = PY_MAJOR_VERSION; +static const int MINOR_VERSION = PY_MINOR_VERSION; + +const unsigned char *aes_key = "WAIT_TO_BE_FILLED"; + +// PyObject *CodeObject; + +PyObject* PyHumorRuntime_HumorEnter(PyObject* self, PyObject* args) { + PyFrameObject *frame = PyEval_GetFrame(); + PyCodeObject *f_code = frame->f_code; + // PyCodeObject *f_code = PyFrame_GetCode(PyEval_GetFrame()); + PyObject *codearray = f_code->co_code; + + Py_ssize_t size; + unsigned char *data; + PyBytes_AsStringAndSize(codearray, &data, &size); + if (data == NULL) { + return NULL; + } + if (size < 8 || size > 8192) { + return NULL; + } + int use_size = (size - 8) - (size - 8) % 16; + + symmetric_key skey; + int err; + if ((err = aes_setup(aes_key + 1, 16, 0, &skey)) != CRYPT_OK) { + return NULL; + } + for (int i = 0; i < use_size; i += 16) { + if ((err = aes_ecb_decrypt(data + i + 8, data + i + 8, &skey)) != CRYPT_OK) { + return NULL; + } + } + aes_done(&skey); + + return Py_None; +} + +PyObject* PyHumorRuntime_RunCode(PyObject* self, PyObject* args) { + PyObject* name; + PyObject* file; + char* bytecode; + Py_ssize_t bytecodeSize; + if (!PyArg_ParseTuple(args, "OOy#", &name, &file, &bytecode, &bytecodeSize)) { + return NULL; + } + + // Parse the code meta + if (bytecodeSize < 16) { + return NULL; + } + if (bytecode[9] != MAJOR_VERSION || bytecode[10] != MINOR_VERSION) { + return NULL; + } + if (*(int*)(bytecode + 12) != bytecodeSize - 16) { + return NULL; + } + + // Decrypt the bytecode + symmetric_key skey; + unsigned char *plaintext = malloc(bytecodeSize - 16); + int err; + if ((err = aes_setup(aes_key, 16, 0, &skey)) != CRYPT_OK) { + return NULL; + } + for (int i = 0; i * 16 < bytecodeSize - 16; i++) { + if ((err = aes_ecb_decrypt(bytecode + 16 + i * 16, plaintext + i * 16, &skey)) != CRYPT_OK) { + return NULL; + } + } + aes_done(&skey); + + // Load the code object + PyObject *codeObject = PyMarshal_ReadObjectFromString(plaintext, bytecodeSize - 16); + free(plaintext); + if (codeObject == NULL) { + return NULL; + } + + // Add __humor_enter__ function to the globals + PyMethodDef humorEnterMethodDef = + {"__humor_enter__", PyHumorRuntime_HumorEnter, METH_VARARGS, NULL}; + + PyObject* humorEnterFunc = PyCFunction_New(&humorEnterMethodDef, NULL); + if (humorEnterFunc == NULL) { + return NULL; + } + + PyObject* globals = PyEval_GetGlobals(); + if (globals == NULL || PyDict_SetItemString(globals, "__humor_enter__", humorEnterFunc) < 0) { + Py_DECREF(humorEnterFunc); + return NULL; + } + + Py_DECREF(humorEnterFunc); // Decrease reference count since it's now in the globals + + // Run the code + PyObject* result = PyImport_ExecCodeModuleObject(name, codeObject, file, NULL); + Py_DECREF(codeObject); + + // Remove __humor_enter__ function from the globals + PyDict_DelItemString(globals, "__humor_enter__"); + + return result; +} + +static PyMethodDef PyHumorRuntime_Methods[] = { + {"__pyhumor__", PyHumorRuntime_RunCode, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef PyHumorRuntime_Module = { + PyModuleDef_HEAD_INIT, + "pyhumor_runtime", + NULL, + -1, + PyHumorRuntime_Methods +}; + +PyMODINIT_FUNC PyInit_pyhumor_runtime(void) { + return PyModule_Create(&PyHumorRuntime_Module); +} diff --git a/源码/source/obfuscator/setup.py b/源码/source/obfuscator/setup.py new file mode 100644 index 0000000..e4d9871 --- /dev/null +++ b/源码/source/obfuscator/setup.py @@ -0,0 +1,20 @@ +from distutils.core import setup, Extension + +module1 = Extension('pyhumor_runtime', + sources=[ + 'pyhumor_runtime.c', + 'libtomcrypt/ciphers/aes/aes.c', + 'libtomcrypt/ciphers/aes/aes_desc.c', + 'libtomcrypt/ciphers/aes/aes_tab.c', + # 'libtomcrypt/misc/crypt/crypt_argchk.c', + 'libtomcrypt/misc/zeromem.c', + 'libtomcrypt/misc/compare_testvector.c', + ], + include_dirs=['libtomcrypt/headers'], + define_macros=[('ARGTYPE', '1')], + ) + +setup (name = 'pyhumor_runtime', + version = '1.0', + description = '', + ext_modules = [module1]) diff --git a/源码/source/task-src/get-marshaled.py b/源码/source/task-src/get-marshaled.py new file mode 100644 index 0000000..6e969f4 --- /dev/null +++ b/源码/source/task-src/get-marshaled.py @@ -0,0 +1,7 @@ +import marshal +import dis + +code = compile(open('plain.py').read(), "", "exec") +dis.dis(code) +print(code.co_code, len(code.co_code)) +marshal.dump(code, open("plain.marshal", "wb")) diff --git a/源码/source/task-src/get-obfuscated.py b/源码/source/task-src/get-obfuscated.py new file mode 100644 index 0000000..b01dd90 --- /dev/null +++ b/源码/source/task-src/get-obfuscated.py @@ -0,0 +1,30 @@ +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +import marshal +import os + +aes_key = os.urandom(17) +with open('../obfuscator/build/lib.win-amd64-cpython-310/pyhumor_runtime.cp310-win_amd64.pyd', 'rb') as fin: + filled = fin.read().replace(b'WAIT_TO_BE_FILLED', aes_key) +with open('pyhumor_runtime.cp310-win_amd64.pyd', 'wb') as fout: + fout.write(filled) + +code = compile(open('plain.py').read(), "", "exec") +raw = marshal.dumps(code) + +code_len = len(code.co_code) +encrypt_len = (code_len - 8) - (code_len - 8) % 16 +cipher = AES.new(aes_key[1:], AES.MODE_ECB) +encrypted = cipher.encrypt(raw[38:38+encrypt_len]) +assert len(encrypted) == encrypt_len +raw = raw[:38] + encrypted + raw[38+encrypt_len:] +raw = pad(raw, 16) + +cipher = AES.new(aes_key[:16], AES.MODE_ECB) +encrypted = cipher.encrypt(raw) +structured = b'PYHUMOR\x00\x00\x03\x0A\x00' + len(encrypted).to_bytes(4, 'little') + encrypted + +open("pyhumor.py", "w").write(f'''from pyhumor_runtime import __pyhumor__ + +__pyhumor__(__name__, __file__, {repr(structured)}) +''') diff --git a/源码/source/task-src/get-task-pyc.py b/源码/source/task-src/get-task-pyc.py new file mode 100644 index 0000000..29dbef8 --- /dev/null +++ b/源码/source/task-src/get-task-pyc.py @@ -0,0 +1,3 @@ +import py_compile + +py_compile.compile('pyhumor.py', 'pyhumor.pyc') diff --git a/源码/source/task-src/plain.py b/源码/source/task-src/plain.py new file mode 100644 index 0000000..50e4218 --- /dev/null +++ b/源码/source/task-src/plain.py @@ -0,0 +1,35 @@ +# flag{9bc74ce3-a56d-467f-eb52-d5f3d8923c6f} + +__humor_enter__() + +try: + from hashlib import sha256 + from functools import reduce + + flag = input('Enter your flag: ') + + group_1 = [ + int.from_bytes(flag[1::3].encode(), 'big') % 998244353 == 156881262, + sha256(flag[-16:].encode()).hexdigest().endswith('dcf56476457880bf5b39b295416f267b7a636324baeae1fd'), + reduce(lambda x, y: x ^ y, map(ord, flag)) == 2, + ] + + if not all(group_1): + print('Wrong') + exit(1) + + acc = 0 + for i in flag: + i = ord(i) ^ 0x55 + acc |= i + acc <<= (7 if i & 1 else 8) + + if acc == 27473331342481820165679397757145329260017933200691317902624657196062576436414763023083043884214272: + print('Right') + else: + print('Wrong') + exit(1) + +except: + print('Stop reverse engineering me, enjoy your day :)') + exit(1) diff --git a/解题/exp.py b/解题/exp.py new file mode 100644 index 0000000..561b19a --- /dev/null +++ b/解题/exp.py @@ -0,0 +1,11 @@ +# 手动获取 target 的过程见 Writeup +target = 0xCDCB4322E6C376CC4C2D8C199E0D1818D8F861C788CFC181BE067F06380CF318EDD8CF98D98D9A800 +flag = [] +while target != 0: + target >>= (7 if target & 0x80 else 8) + if target != 0: + flag.append((target & 0x7F) ^ 85) +flag = bytes(flag[::-1]).decode() +print(flag) + +# flag{9bc74ce3-a56d-467f-eb52-d5f3d8923c6f} \ No newline at end of file diff --git a/附件/Pyhumor_f9c556898dc4227298c77b72441b0196.zip b/附件/Pyhumor_f9c556898dc4227298c77b72441b0196.zip new file mode 100644 index 0000000000000000000000000000000000000000..202fbdd20bed90be00092e32b36ffe2a09195aac GIT binary patch literal 21342 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 literal 0 HcmV?d00001