Python逆向及相关知识

今天第二次看见python字节码的逆向题,然后发现了一个介绍Python逆向的文章,所以把文章里的内容简单整理记录一下。

文章参考:https://www.cnblogs.com/blili/p/11799398.html  奇安信攻防社区-浅谈ctf中的python逆向https://www.cnblogs.com/blili/p/11799398.html

PYC逆向及混淆手法 - 在逆向的边缘反复横跳

Python运行原理:

一.什么是Python

Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。

二.解释性语言和编译型语言的区别

我们编程都是用的高级语言,计算机不能直接理解高级语言,只能理解和运行机器语言,所以必须要把高级语言翻译成机器语言,计算机才能运行高级语言所编写的程序。翻译的方式有两种,一个是编译,一个是解释。

用编译型语言写的程序执行之前,需要一个专门的编译过程,通过编译系统(不仅仅只是通过编译器,编译器只是编译系统的一部分)把高级语言翻译成机器语言,把源高级程序编译成为机器语言文件。

解释型语言没有严格编译汇编过程,由解释器将代码块按需要变运行边翻译给机器执行。因此解释型语言一度存在运行效率底,重复解释的问题。但是通过对解释器的优化!可以提高解释型语言的运行效率。Python就属于这一种编程语言。

三.Python运行原理概述

Python没有严格意义上的编译和汇编过程。一般可以认为编写好的python源文件,由python解释器翻译成以.pyc为结尾的字节码文件。pyc文件是二进制文件,可以由python虚拟机直接运行。

注:有的朋友可能会问,为什么我运行python,有时候生成pyc文件,有时候没有呢?Python在执行import语句时,将会到已设定的path中寻找对应的模块。并且把对应的模块编译成相应的PyCodeObject(python中的一个类)中间结果,然后创建pyc文件,并将中间结果写入该文件。然后,Python会import这个pyc文件,实际上也就是将pyc文件中的PyCodeObject重新复制到内存中。而被直接运行的python代码一般不会生成pyc文件。

加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。

四.综述

Python源码->python解释器->.pyc文件->python虚拟机运行

Python的pyc文件结构

Python代码的编译结果就是PyCodeObject对象。PyCodeObject对象可以由虚拟机加载后直接运行,而pyc文件就是PyCodeObject对象在硬盘上的保存形式。因此我们先分析PyCodeObject对象的结构,随后再涉及pyc文件的二进制结构。

一.PyCodeObject对象结构

 二.pyc文件生成:

python中使用marshal.dump的方法将PyCodeObject对象转化为对应的二进制文件结构。每个字段在二进制文件中的结构如下图:

pyc文件结构主要包括两部分:pyc文件头部表示和PyCodeObject对象部分。上面对PyCodeObject对象的二进制部分已经有了了解,pyc文件头部比较简单,在python2中只占用4个字节包含两个字段magic和mtime,完整的pyc文件结构见下图: 

python字节码反编译

经过编译的python文件可以提高程序的运行速度,一定程度上也对源代码起到了保护作用。然而如果我们只有编译过的python字节码文件,就给我们审查源码造成了一定的困难,这就引出了python字节码反编译的需求。

根据python的编译原理我们知道PyCodeObjectData是python源文件作为一个实例化的类,通过python内置库函数marshal.dumps生成的二进制数据段,因此通过marshal.loads(PyCodeObjectData) ,我们可以得到PyCodeObjectData反序列化的对象。

使用python内置模块dis可以对PyCodeObject进行反编译,从而获取到python二进制字节码代码段的“汇编形式”。这样可以便于对字节码进行阅读。

Python字节码解读

字节码结构如下
源码行号 | 跳转注释符 | 指令在函数中的偏移 | 指令符号(助记符) | 指令参数 | 实际参数值

上图表示

  • 该字节码指令在源码中对应59行
  • 此处是跳转的目的地址
  • 82该字节指令的字节码偏移
  • 操作指令对应的助记符为LOAD_GLOBAL
  • 操作参数为6
  • 操作参数对应的实际值为disassemble

常见字节码解读

1.常量

加载常量只有一行LOAD_CONST,对应源码第1行,字节码偏移地址0字节,常量数组中索引0,实际常量值‘123 ’

2.局部变量

加载局部变量a:LOAD_CONST加载常量1,调用STORE_NAME(参数a),并将变量a存储为1
同理加载局部变量b

3.全局变量

加载全局变量a,与加载局部变量不同的是通过STORE_GLOBAL在存储变量

4.数据类型list

先将所有的list元素加载,调用BUILD_LIST方法生成list于内存中,通过STORE_NAME将堆栈中的list存储于局部变量a中

5.数据类型dict

BUILD_MAP声明字典元素数量,通过两次LOAD_CONST后,调用STORE_MAP生成键值对存于堆栈,最终通过STORE_NAME将堆栈中长度为2的两个键值对最为字典数据类型存储在a中

6.数学运算

字节码中显示先对局部变量a、b赋值,通过LOAD_NAME加载局部变量,调用加法BINARY_ADD,生成结果存储与堆栈中,使用STORE_NAME将堆栈中的计算结果存储与局部变量c
加减乘除的运算字节码相似,不不再赘述,读者可以自行分析,如下图:

上图中为对a、b做加减乘除的字节码,因为没有存储计算结果,所以每次运算完没有使用STORE_NAME方法存储,解释器默认调用POP_TOP方法将计算结果从堆栈顶部弹出,以保证堆栈平衡。

7.for循环

上图显示一个FOR循环的过程。SETUP_LOOP表明循环开始,参数说明此循环知道字节码偏移28字节的指令结束(也就是28字节开始不是循环)。调用range方法生成generator存于堆栈。FOR_ITER调用堆栈,声明generator作用到字节码偏移位置27字节。从第16字节起到27为generator迭代作用域。其中为一个print函数。

8.if判断

以一个简单的IF判断为例,先加载需要比较的常量,调用COMPARE_OP指令对堆栈中两个常量进行比较,将结果存入堆栈。调用POP_JUMP_IF_FALSE指令,判断栈顶值来决定程序运行顺序实现判断功能。 

补充:

Python 字节码与字节码混淆 - 灰信网(软件开发博客聚合)

PythonCodeObject补充

在 Python 解释器目录下 ./lib/python3.7/importlib/_bootstrap-external.py 中有明确的版本号记录

对于 pyc 文件整体的 C 结构体,可以在 ./include/python2.7/code.h 或不同版本类似的文件中找到

对于字节码opcode对应的二进制数可在./Python38/include/opcode.h查看

pyc反编译工具

(1)uncompyle6   使用:uncompyle6 ./xx.pyc

(2)pycdc             使用:./pycdc ./xxx.pyc

(3)pydumpck   使用:pydumpck *.exe/*.pyc支持exe和pyc的反编译,支持上面两种方式反编译,具有多线程支持

(4)py4coc           使用:是一个python脚本,依赖pycdc,可直接把pyc或exe反编译成python文件。实现了pydumpck的基础功能。

(5)pyinstxtractor  使用:pyinstxtractor是支持通过pyinstaller打包成的elf或exe的解包,支持的pyinstaller的版本较全,解包效果较好

相关api 或 结构成员

(1)dis.dis函数能反编译出所有的pyc的字节码

(2)dis.opmap是dis模块中的一个字典类型成员,他记录了字节码和16进制的对应关系

(3)dis.Bytecode:它可以吧字节码更细致的打印出来,不过直接使用的话,只能打印 第一个函数的信息(一般是main函数)

bytecode = dis.Bytecode(Pyobj)
for instr in bytecode:
    print(instr)
    print(instr.opcode)         # 输出所有的opcode名字
    print(instr.offset)         # 输出所有的自解码偏移

(4)Pyobj.co_code

默认打印第一个函数(main)函数中的字节码的16进制信息

由于 python3.6 版本之后,一个python字节码大小为两字节,我们可以使用一下方式来获取字节码数量

len(Pyobj.co_code) // 2

此处以一道题为例,使用 010 editor 打开

  • 最前面的 4 个字节为 Magic Number ,其中前两个直接为解释器的版本号

    • 此处前两个字节为 62211,也就是 Python 2.7.0a0 版本的字节码解释器
    • 注意这里是小端序,就是高位在后面,所以是 0xF303
  • Magic Number 之后的四个字节为时间戳,这里是 0x5EC652B0,之后就是 Python 代码对象

  • 代码对象首先一个字节表示此处的对象类型,这里值为 TYPE_CODE,值为 0x63,

  • 此后四个字节表示参数的个数,也就是 co_argcount 的值

  • 往后四个字节是局部变量的个数 co_nlocals

  • 往后四个字节是栈空间大小 co_stacksize

  • 往后四个字节是 co_flags

  • 之后就是 co_code 了,也就是编译好的字节码的部分

    • co_code 部分首先的一个字节也是表示此处的对象类型,这里是 TYPE_STRING,为 0x73
    • 接下来四个字节表示此 co_code 对象的长度,此后就是代码对象,这里的代码长度为 0xA7
    • 也就是后方 163 个字节的长度都是代码对象
  • 此 co_code 对象的字节码内容结束后,接着是 co_consts 内容,也就是用到的常量的内容
    • 最开始是 TYPE_TUPLE,表示这是个元组类型
    • 此后四个字节是元素个数,这里是 0x23,之后每一个字节与对应的值一组,一共 0x23 组
      • 每组中第一个字节表示元素类型,比如 0x69 指 TYPE_INT,此后为对应的值
  • 后方也对应结构体中的相应内容

字节码混淆

Anti-uncompyle6

对于正常的 pyc 文件,使用 uncompyle6 插件可以正常的进行字节码逆向,得到原来的代码

如果需要使 uncompyle6 失效的话,只要在 co_code 头部加上 0x71 0x03 0x00 ,然后把记录 co_code 长度的数据加 3

  • 这段字节码指 JUMP_ABSOLUTE 3 ,也就是向后跳 3 个字节后继续执行,实际上没有改变代码逻辑
  • 但是 uncompyle6 插件的还原逻辑就没办法识别此字节码原先的意思,导致解析异常

Anti-dis

  • 上文的改法会导致 uncompyle6 插件异常,但是这个方法的实质只是增加了一句字节码
  • Python 可以借助自带的 dis 库和 marshal 库解析 pyc 二进制文件中的信息,举个例子
    def fun1():
        enc = "Ua`|{f.4V}$l4h4Vx{s.4|``dg.;;vx{s:v}$l:wz;4h4Dxqugq4}zp}wu`q4`|q4g{afwq4ur`qf4m{af4pqf}bu`}{z"
        flag = ""
        for i in enc:
            flag += chr(ord(i) ^ 0x14)
        print flag
    fun1()
    

    编译成 pyc 文件后,尝试加入 JUMP_ABSOLUTE 3 到代码头部

  • 橙色的字节为编辑过的

 使用 uncompyle6 发生 Parse error 异常,但是还是可以正常运行。尝试使用 marshal 模块搭配 dis 模块进行字节码解析,程序输出了完整的字节码,根据字节码还是可以顺利的还原出源代码信息

如果我们不想让 dis 顺利的导出字节码,也可以用一些指令来使得 dis 模块产生异常

  • 比如来个指令重叠,中间插一个读取数据的字节码0x71 0x04 0x00 0x64 0x71 0x08 0x00 0x00
  • 这里的 0x64 为解释器的 LOAD_CONST 指令,如果正常的话这里应该是 LOAD_CONST 0x0871
  • 那么 dis 模块就看不懂了,实际上通过前面的 0x71 0x04 0x00 会跳过此字节码,实际上是不执行的
  • 后方的0x71 0x08 0x00 是根据前面第一个 0x71 开始跳转的,所以跟的是 0x08

尝试 dis 字节码,直接抛出 IndexError 了,同时 uncompyle6 也 IndexError 了

pyc花指令

整理自PYC逆向及混淆手法 - 在逆向的边缘反复横跳

常见的python花指令形式有两种:单重叠指令和多重叠指令。

以下以python3.8为例,指令长度为2字节

单重叠指令:

#例1 Python单重叠指令
 0 JUMP_ABSOLUTE        [71 04]     5 
 2 PRINT_ITEM           [47 --]
 4 LOAD_CONST           [64 10]     16
 6 STOP_CODE            [00 --]
#例1 实际执行
 0 JUMP_ABSOLUTE        [71 04]     5 
 4 LOAD_CONST           [64 10]     16

单重叠指令多是分支的跳转,导致一些反编译工具如pycdc、uncompyle6出错。

多重叠指令:

#例2 Python多重叠指令
 0 EXTENDED_ARG         [91 64] 
 2 EXTENDED_ARG         [91 53]
 4 JUMP_ABSOLUTE        [71 01]
#例2 实际执行
 0 EXTENDED_ARG         [91 64] 
 2 EXTENDED_ARG         [91 53]
 4 JUMP_ABSOLUTE        [71 02]
 1 LOAD_CONST           [64 91]
 3 RETURN_VALUE         [53 --]

多重叠指令是将指令的数据部分当作下一条指令的opcode部分执行,在跳转基础上进一步混淆控制流的技术手段

常见的花指令形式:

83         654 JUMP_FORWARD             0 (to 656)
        >>  656 JUMP_FORWARD             2 (to 660)
            658 NOP
        >>  660 JUMP_FORWARD             4 (to 666)

 84         662 NOP
            664 NOP
        >>  666 JUMP_FORWARD             4 (to 672)
            668 NOP

 85         670 NOP
        >>  672 JUMP_FORWARD             4 (to 678)
            674 NOP
            676 NOP
            
            
 65       412 LOAD_CONST               1 ('You Are Debug')
            414 LOAD_CONST               1 ('You Are Debug')
            416 LOAD_CONST               2 ('0b')
            418 JUMP_FORWARD             0 (to 420)

pyc解花指令

pyc去除花指令后,很大可能是不能被现有工具反编译成源码的,因为现有反编译工具对pyc要求比较严格,不能有nop以及其他junk指令

解决方法

(1)将原来花指令的地方填充其他无关指令占位

(2)修改pycdc源码,将反编译相关的限制放开

例题:花指令的单重叠指令

题目链接:题目

下载完是个.pyc文件,使用uncompyle6反编译一下

发现编译不了,并且.pyc是3.8版本的,那就在3.8版本环境下看一下字节码

 发现前面JUMP_ABSOLUTE就是单重叠,去花,在010去掉这三个指令的二进制,然后修改co_code的长度(减6)

看一下

 又因为经查看./Python38/include/opcode.h知道JUMP_ABSOLUTE对应0x71,即把下图71 04 71 06 71 02删除,然后把前面的EE 07减6即可

 修改后在用uncompyle6就可以反编译成功。

_map = [
 [
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1], [1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1], [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1], [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1], [1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1], [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1], [1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

def maze():
    x = 1
    y = 1
    step = input()
    for i in range(len(step)):
        if step[i] == 'w':
            x -= 1
        else:
            if step[i] == 's':
                x += 1
            else:
                if step[i] == 'a':
                    y -= 1
                else:
                    if step[i] == 'd':
                        y += 1
                    else:
                        return False
        if _map[x][y] == 1:
            return False
        if x == 29 and y == 29:
            return True


def main():
    print('Welcome To VNCTF2022!!!')
    print('Hello Mr. X, this time your mission is to get out of this maze this time.(FIND THAT 7!)')
    print('you are still doing the mission alone, this tape will self-destruct in five seconds.')
    if maze():
        print('Congratulation! flag: VNCTF{md5(your input)}')
    else:
        print("Sorry, we won't acknowledge the existence of your squad.")


if __name__ == '__main__':
    main()

然后走迷宫就可以解flag

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值