标题: 寻找变形Python解释器中opcode映射关系的思路 创建: 2023-05-08 21:04 更新: 2023-05-09 10:39 链接: https://scz.617.cn/python/202305082104.txt 多年前看过一篇,针对重度混淆过的Python解释器进行逆向工程 Reversing Obfuscated Python Applications Breaking the dropbox client on windows - ExtremeCoders [2014-07-03] http://www.slideshare.net/extremecoders/reversing-obfuscated-python-applications-dropbox-38138420 其目标是Dropbox,版本是2.x。现在基本都是3.x了,但很多思路是相通的,没看过 的,值得一阅。 XYM在工作中遭遇现实世界的变形Python 3.9解释器,其保护力度远不如Dropbox,但 对之逆向工程有现实意义。其some.pyc的"magic number"非标准值,序列化的 PyObject被混淆过。XYM设法反混淆后,发现opcode映射关系置换过,很多CTF题有类 似场景,我从未关心过,但这次不一样,是现实世界场景。 单就「opcode置换」,简单说一下应对思路。先写一个opcode_full.py,顾名思义, 对该文件动用dis模块,可生成「所有种类」opcode。示例 -------------------------------------------------------------------------- import dis def func ( n ) : for i in range( n )[::-1] : yield -i dis.dis( func ) -------------------------------------------------------------------------- $ python3.9 opcode_full.py 4 0 LOAD_GLOBAL 0 (range) 2 LOAD_FAST 0 (n) 4 CALL_FUNCTION 1 6 LOAD_CONST 0 (None) 8 LOAD_CONST 0 (None) 10 LOAD_CONST 1 (-1) 12 BUILD_SLICE 3 14 BINARY_SUBSCR 16 GET_ITER >> 18 FOR_ITER 12 (to 32) 20 STORE_FAST 1 (i) 5 22 LOAD_FAST 1 (i) 24 UNARY_NEGATIVE 26 YIELD_VALUE 28 POP_TOP 30 JUMP_ABSOLUTE 18 >> 32 LOAD_CONST 0 (None) 34 RETURN_VALUE 并不要求func()可执行,可用dis反汇编即可。直接调dis.dis()太粗暴,可换成 dis.get_instructions()之类的,精准输出instr.opname、instr.opcode等等。确保 opcode_full.py涵盖尽可能多的opcode。 网友UID(1889059107)提到一个python2的现成实现 https://github.com/dkw72n/notes/tree/master/scripting/python/py27_all_ops 有些片段或可借用。再就是直接问GPT,比如 「什么样的python3代码会生成 UNARY_POSITIVE 这样的字节码」 有些opcode不易生成,比如 LIST_APPEND SET_ADD DICT_UPDATE SETUP_ANNOTATIONS ROT_FOUR LOAD_CLASSDEREF MAP_ADD DELETE_DEREF NOP PRINT_EXPR 可写程序在Python安装目录下遍历所有some.pyc,解析并从中搜索指定opcode,进而 查看相应some.py,借鉴源码实现,补充至opcode_full.py。 Python 3.9及更低版本无法正常生成NOP,PRINT_EXPR只在"interactive mode"生成 并使用,换句话说,正常some.pyc中不可能出现这两个opcode。 参看 How to get the Python interpreter to emit a NOP instruction - [2017-06-05] https://stackoverflow.com/questions/44379192/how-to-get-the-python-interpreter-to-emit-a-nop-instruction This appears to be impossible. NOP opcodes are only generated by the peephole optimizer, but the last step of peephole optimization removes all NOPs and retargets jumps for the new instruction indices. 参 cpython-3.9\Python\peephole.c PyCode_Optimize()中有个注释,"Remove NOPs and fixup jump targets",查看其 附近代码。 网友UID(2178100891)提到,Python 3.10及更高版本,下列代码会生成NOP -------------------------------------------------------------------------- while True : pass -------------------------------------------------------------------------- 可在此测试 https://python.godbolt.org/ 假设opcode_full.py已能生成指定版本解释器尽可能多种类的opcode,除了NOP、 PRINT_EXPR。接下来,用标准解释器、变形解释器分别反汇编opcode_full.py,各有 一份输出。变形解释器可能会出幺蛾子,根据抛出的异常临时Patch相应文件,确保 能采集到opcode_full.py所有instr.opcode即可,其instr.opname无意义。对比两份 输出,即可找出尽可能多种类的opcode映射关系,此过程可用各种Linux文本处理工 具,不必手工比对。显然,opcode_full.py是Python版本强相关的,标准解释器、变 形解释器必须是同一Python版本。 假设通过opcode_full.py找出除NOP、PRINT_EXPR之外所有种类opcode映射关系,对 于绝大多数逆向工程,足矣,缺少这两个opcode,没有实际影响。有了新的opcode映 射关系,可临时修改标准解释器的opcode.py使dis模块可用,输出正确的反汇编结果。 还可修改python_*.map并重新编译,使pycdas、pycdc可用。 Decompyle++ A Python Byte-code Disassembler/Decompiler https://github.com/zrax/pycdc python_*.map形如 -------------------------------------------------------------------------- 72 POP_TOP 60 ROT_TWO 81 ROT_THREE 42 DUP_TOP -------------------------------------------------------------------------- python_*.map第1列并不要求单向递增,没有排序一说。瞎填NOP、PRINT_EXPR的值, 也无实际影响。 有洁癖或其他特殊需求时,可通过逆向工程找出NOP、PRINT_EXPR的映射。在Source Insight中打开 cpython-3.9\Include\opcode.h 在该文件中定位PRINT_EXPR,右键"Jump To Caller",选中ceval,实际跳至 _PyEval_EvalFrameDefault()的"case TARGET(PRINT_EXPR)",附近代码出现特征串 "lost sys.displayhook"。 用IDA打开变形Python解释器,Strings窗口搜"lost sys.displayhook",交叉引用定 位"case 7",表示PRINT_EXPR现在映射至7。 NOP没有特征串借力,得用其他奇技淫巧,留作逆向工程练习吧。这个无须用变形解 释器练手,就用标准解释器好了,思路可以平移。得假设跳转表9号元素不是NOP,莫 犯低级错误。 若「opcode置换」已影响到HAVE_ARGUMENT,应对措施更复杂些,但思路普适。 这事儿属于一次投入、反复受益。无论是打CTF,还是现实世界逆向工程,值得长期 维护不同版本opcode_full.py,不但快速确定opcode映射关系,还可测试Pyton反汇 编器、反编译器、010 Editor模板等等。