Python字节码

Python字节码

三月 10, 2020

寒假的时候做hgame2020,借week2两道python逆向题,整理一下有关python字节码的一些东西。

pyc文件简介

学过Python的都知道.py结尾的是Python的源码文件,.pyc是源码编译后生成的字节码文件,也可以运行,明明可以直接运行源码文件,这个字节码文件有什么卵用?

一来是为了无源文件发行,可以隐藏源码;还有就是编译后的.pyc文件比源码文件要快,有人说运行的时候会更快,但官方文档是这样写的:

A program doesn’t run any faster when it is read from a .pyc file than when it is read from a .py file; the only thing that’s faster about .pyc files is the speed with which they are loaded.

字节码文件运行并不会比源码文件快,主要是加载(被import)比源码文件快。

代码对象

先介绍一下代码对象,代码对象是 CPython 实现的低级细节。

1
2
3
4
5
>>> def func():
... print('hello')
...
>>> func.__code__
<code object func at 0x0000015797EA3780, file "<stdin>", line 1>

这个东西就是代码对象,表示可执行Python代码或者字节码,在示例中表示函数中的代码部分,不包含对全局变量的引用和函数默认参数等其他东西(示例函数中也没有)。

代码对象中还有一大堆属性:

  • co_name是函数名称。

  • co_argcount是位置参数的数量,包括仅限位置参数和具有默认值的参数。

  • co_kwonlyargcount为仅限位置参数的数量 (包括带有默认值的参数) Python3中有,Python2中没有

  • co_nlocals是函数使用的局部变量数,包括参数。

  • co_varnames是一个包含局部变量名称的元组,以参数名称开头。

  • co_cellvars是一个元组,包含嵌套函数引用的局部变量的名称。

  • co_freevars是一个包含自由变量名称的的元组。

  • co_code是表示字节码指令序列的二进制数据。

  • co_consts是一个包含字节码使用的字面值的元组。如果代码对象表示函数,则co_consts的第一项是函数的文档字符串,如果文档字符串未定义,则是None。

  • co_names是一个包含字节码使用的名称的元组(不包括变量名)。

  • co_filename是编译代码的文件名。

  • co_firstlineno是函数的第一个行号。

  • co_lnotab是一个字符串,用于编码从字节码偏移到行号的映射(更详细的信息参考解释的源代码)。

  • co_stacksize是所需的堆栈大小(包括局部变量)。

  • co_flags 为一个整数,其中编码了解释器所用的多个旗标。以下是可用于 co_flags 的标志位定义:如果函数使用 *arguments 语法来接受任意数量的位置参数,则 0x04 位被设置;如果函数使用 **keywords 语法来接受任意数量的关键字参数,则 0x08 位被设置;如果函数是一个生成器,则 0x20 位被设置。

    代码对象跟分析字节码有啥关系呢?我们可以用一个dis模块把代码对象编程人类可读的字节码。

dis模块 — Python 字节码反汇编器

dis模块通过反汇编支持CPython的字节码分析。

这里只从官方文档以用两个常用的函数介绍,其他具体请看官方文档

  • dis.``dis(x=None, ***, file=None, depth=None)

    反汇编 x 对象。 x 可以表示模块、类、方法、函数、生成器、异步生成器、协程、代码对象、源代码字符串或原始字节码的字节序列(划重点)。对于模块,它会反汇编所有功能。对于一个类,它反汇编所有方法(包括类和静态方法)。对于代码对象或原始字节码序列,它每字节码指令打印一行。它还递归地反汇编嵌套代码对象(推导式代码,生成器表达式和嵌套函数,以及用于构建嵌套类的代码)。在被反汇编之前,首先使用 compile() 内置函数将字符串编译为代码对象。如果未提供任何对象,则此函数会反汇编最后一次回溯。如果提供的话,反汇编将作为文本写入提供的 file 参数,否则写入 sys.stdout 。递归的最大深度受 depth 限制,除非它是 Nonedepth=0 表示没有递归。在 3.4 版更改: 添加 file 形参。在 3.7 版更改: 实现了递归反汇编并添加了 depth 参数。在 3.7 版更改: 现在可以处理协程和异步生成器对象。

  • dis.``distb(tb=None, ***, file=None)

    如果没有传递,则使用最后一个回溯来反汇编回溯的堆栈顶部函数。 指示了导致异常的指令。如果提供的话,反汇编将作为文本写入提供的 file 参数,否则写入 sys.stdout在 3.4 版更改: 添加 file 形参。

    生成人类可读字节码示例如下:

1
2
3
4
5
6
7
8
9
10
11
>>> import dis
>>> def f():
... print('hello')
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('hello')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE

为啥要把源码搞成更难读的字节码?这其实不是我们的主要目的,因为源码一般是不会直接给我们的,dis模块也可以反汇编.pyc文件中原始字节码的字节序列(上面函数介绍中我有划重点)。

marshal模块 — 内部 Python 对象序列化

此模块包含一此能以二进制格式来读写Python值的函数。

Warning:The marshal module is not intended to be secure against erroneous or maliciously constructed data. Never unmarshal data received from an untrusted or unauthenticated source.

也只从官方引用两个常用的函数介绍,其他细节见官方文档:

  • marshal.``dump(value, file[, version])

    向打开的文件写入值。 值必须为受支持的类型。 文件必须为可写的 binary file。如果值具有(或所包含的对象具有)不受支持的类型,则会引发 ValueError — 但是将向文件写入垃圾数据。 对象也将不能正确地通过 load() 重新读取。version 参数指明 dump 应当使用的数据格式(见下文)。

  • marshal.``load(file)

    从打开的文件读取一个值并返回。 如果读不到有效的值(例如由于数据为不同 Python 版本的不兼容 marshal 格式),则会引发 EOFError, ValueErrorTypeError。 文件必须为可读的 binary file。注解 如果通过 dump() marshal 了一个包含不受支持类型的对象,load() 将为不可 marshal 的类型替换 None

简单来说marshal对于我们逆向python最大的作用就是可以从.pyc文件中读取Python对象,然后就可以用dis反汇编成人类可读的字节码来分析。

然后新的问题又来了,我们怎么从.pyc文件中找到代码对象的位置呢?

pyc文件分析

根据不同的Python版本.pyc文件的格式也会有所变化,。

Python2.7

我们先随便写一个py文件

1
2
3
4
5
6
7
8
9
s='ssss'
i=666

def f():
s='ss'
i=66
print('hello')

f()

python -m test.py编译生成.pyc文件

十六进制打开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@kali:~/Challenges# hexdump -C test.pyc
00000000 03 f3 0d 0a d4 c0 22 5e 63 00 00 00 00 00 00 00 |......"^c.......|
00000010 00 01 00 00 00 40 00 00 00 73 20 00 00 00 64 00 |.....@...s ...d.|
00000020 00 5a 00 00 64 01 00 5a 01 00 64 02 00 84 00 00 |.Z..d..Z..d.....|
00000030 5a 02 00 65 02 00 83 00 00 01 64 03 00 53 28 04 |Z..e......d..S(.|
00000040 00 00 00 74 04 00 00 00 73 73 73 73 69 9a 02 00 |...t....ssssi...|
00000050 00 63 00 00 00 00 02 00 00 00 01 00 00 00 43 00 |.c............C.|
00000060 00 00 73 15 00 00 00 64 01 00 7d 00 00 64 02 00 |..s....d..}..d..|
00000070 7d 01 00 64 03 00 47 48 64 00 00 53 28 04 00 00 |}..d..GHd..S(...|
00000080 00 4e 74 02 00 00 00 73 73 69 42 00 00 00 74 05 |.Nt....ssiB...t.|
00000090 00 00 00 68 65 6c 6c 6f 28 00 00 00 00 28 02 00 |...hello(....(..|
000000a0 00 00 74 01 00 00 00 73 74 01 00 00 00 69 28 00 |..t....st....i(.|
000000b0 00 00 00 28 00 00 00 00 73 07 00 00 00 74 65 73 |...(....s....tes|
000000c0 74 2e 70 79 74 01 00 00 00 66 04 00 00 00 73 06 |t.pyt....f....s.|
000000d0 00 00 00 00 01 06 01 06 01 4e 28 03 00 00 00 52 |.........N(....R|
000000e0 03 00 00 00 52 04 00 00 00 52 05 00 00 00 28 00 |....R....R....(.|
000000f0 00 00 00 28 00 00 00 00 28 00 00 00 00 73 07 00 |...(....(....s..|
00000100 00 00 74 65 73 74 2e 70 79 74 08 00 00 00 3c 6d |..test.pyt....<m|
00000110 6f 64 75 6c 65 3e 01 00 00 00 73 06 00 00 00 06 |odule>....s.....|
00000120 01 06 02 09 05 |.....|
00000125

这些密密麻麻的数据分别代表什么意思,先按顺序列一个表,再详细讲解:

字段 长度(默认最低长度) 描述
MagicNumber 4字节 魔数,区别不同版本的Python字节码
时间戳 4字节 最后修改时间
TYPE_CODE 1字节 这里是’c’,表示接下来是一个CodeObject,见Cpython源码
co_argcount 4字节 后面一堆属性上面代码对象都有讲,我就不重复写了
co_nlocals 4字节
co_stacksize 4字节
co_flags 4字节
TYPE_STRING byte 这里是’s’,表示接下来是一个string,也就是co_code
co_code size 4字节 co_code的长度,这里是0x20(小端序)
co_code value 根据上面的size确定字节数 co_code的值
TYPE_TUPLE 1字节 这里是’(‘,表示接下来是一个元组,也就是co_consts
co_consts size 4字节 这里是4,表示co_consts的元组中4个值,接下来就挨个表示这些值
co_consts[0] TYPE 1字节 这里是’t’,表示co_consts[0]是一个int
co_const[0] value 4字节
co_consts[1] TYPE
co_consts[1] size
co_consts[1] value

co_consts结束后是co_names,co_varnames,co_freevars,co_cellvars结构与co_consts相同,需要提一下如果元组中的数据类型是整形就不需要再标明长度,默认是4字节。

上面几个属性结束之后是co_filename在示例中偏移为0xfd处(因为前几个属性长度不同,此字段位置不确定),在这里是's',表示TYPE_STRING,然后是长度为7,然后是文件名test.py;之后是co_name,co_firstlineno,co_lnotab

还有实例中偏移0x51的位置又有一个'c',是因为CodeObject中的co_consts中有另一个CodeObject,也就是源码中的f()函数,之后会递归分析f()函数,然后再返回继续分析上层CodeObject剩下的内容,方法一样。

总结在.pyc文件中对象的储存方式就是 类型表示 长度 值 这样的格式。

Python3.7

还是先整个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@kali:~/Challenges/__pycache__# hexdump -C test.cpython-37.pyc 
00000000 42 0d 0d 0a 00 00 00 00 d4 c0 22 5e 3c 00 00 00 |B........."^<...|
00000010 e3 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 |................|
00000020 00 40 00 00 00 73 1a 00 00 00 64 00 5a 00 64 01 |.@...s....d.Z.d.|
00000030 5a 01 64 02 64 03 84 00 5a 02 65 02 83 00 01 00 |Z.d.d...Z.e.....|
00000040 64 04 53 00 29 05 5a 04 73 73 73 73 69 9a 02 00 |d.S.).Z.ssssi...|
00000050 00 63 00 00 00 00 00 00 00 00 02 00 00 00 02 00 |.c..............|
00000060 00 00 43 00 00 00 73 14 00 00 00 64 01 7d 00 64 |..C...s....d.}.d|
00000070 02 7d 01 74 00 64 03 83 01 01 00 64 00 53 00 29 |.}.t.d.....d.S.)|
00000080 04 4e 5a 02 73 73 e9 42 00 00 00 5a 05 68 65 6c |.NZ.ss.B...Z.hel|
00000090 6c 6f 29 01 da 05 70 72 69 6e 74 29 02 da 01 73 |lo)...print)...s|
000000a0 da 01 69 a9 00 72 05 00 00 00 fa 18 2f 72 6f 6f |..i..r....../roo|
000000b0 74 2f 43 68 61 6c 6c 65 6e 67 65 73 2f 74 65 73 |t/Challenges/tes|
000000c0 74 2e 70 79 da 01 66 04 00 00 00 73 06 00 00 00 |t.py..f....s....|
000000d0 00 01 04 01 04 01 72 07 00 00 00 4e 29 03 72 03 |......r....N).r.|
000000e0 00 00 00 72 04 00 00 00 72 07 00 00 00 72 05 00 |...r....r....r..|
000000f0 00 00 72 05 00 00 00 72 05 00 00 00 72 06 00 00 |..r....r....r...|
00000100 00 da 08 3c 6d 6f 64 75 6c 65 3e 01 00 00 00 73 |...<module>....s|
00000110 06 00 00 00 04 01 04 02 08 05 |..........|
0000011a

Python3.7开始.pyc的文件头有4个32-bit words,第一个word还是魔数,如果第二个是0,那么第三个就是时间戳,第四个是文件大小,如果第二个word的最低为设置为1,那么该.pyc文件就是一个基于哈希的字节码文件,后两个words就是一个64-bit的哈希值。示例中第二个word就为0。具体细节看官方文档

之后就是CodeObject,3.7.pyc中的CodeObject开头是一个0xe3,表示接下来就是CodeObject(2.7中这里是'c'),之后接着是五个32-bit的word,依次是co_argcount,co_kwonlyargcount,co_nlocals,co_stacksize,co_flags,比Python2.7多一个co_kwonlyargcount,示例中在偏移为0x15的位置。

然后's'是类型标识,表示接下来是co_code(代码段),然后是长度,在示例中是0x1a

代码段之后 是co_consts等,格式与2.7版本基本大同小异。

总结一些我发现的两个版本不同的地方(可能并不完全):

  1. Python3.7版本文件头是4个32-bit word,2.7是两个;
  2. Pyhon3.7版本在co_argcount字段后面多了一个co_kwonlyargcount字段,长度为4bytes;
  3. Python3.7中主函数的CodeObject开始的标识是0xe3,内层CodeObject开始标识是'c',2.7全是'c'
  4. Python3.7中元组的类型标识是')',2.7中是'('
  5. Python3.7中字符串的类型标识为'Z',2.7是's';
  6. Python3.7中元组和字符串等的的size字段默认最少是1byte,2.7中是4byte。

还是找到CodeObject的位置用dis和marshal分析,整个样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import dis,marshal
>>> f=open('Pyc.pyc','rb')
>>> f.read(16)
b'B\r\r\n\x00\x00\x00\x00\xdaR%^ \x04\x00\x00'
>>> code=marshal.load(f)
>>> dis.dis(code)
3 0 JUMP_ABSOLUTE 2
>> 2 LOAD_CONST 0 (0)
4 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (os)
8 STORE_NAME 0 (os)
10 LOAD_CONST 0 (0)
.......

至此,我们基本可以把可读字节码给搞出来了,然后我们来看怎么读。

Python字节码说明

还是先整一个样例:

1
2
3
4
5
6
7
8
9
10
11
>>> import dis
>>> def f():
... print('hello world')
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('hello world')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE

反汇编出来的可读字节码有5列:

第一列: 示例中的“2”,在源代码中的行数;

第二列: 语句在co_code中的偏移;

第三列: opcode,操作码,在示例中被换成了对应的opname,方便阅读,opcode与opname的对应可以在python的opcode模块中查看也可以查看官方源码

第四列: 操作数,有的opcode没有操作数;

第五列: 操作数的实际值(自动注释);

在3.6版本中修改:每条指令使用2字节,以前根据指令的不同长度也不同,上面示例是3.7版本的,可以看出每一句的偏移+2。

Python字节码是完全面向栈的,没有寄存器什么的。

用TOS表示栈顶值,TOS1表示栈顶下1个地址的值,TOS2表示栈顶下2个地址的值……

各opname对应的具体操作见dis官方文档

这里解释一下第五列的实际数值和第四列的操作数有什么关系,如果操作数是一个字面值(比如opname为LOAD_CONST),那么之前说了,CodeObject中有一个co_consts是一个储存了所有字面值的元组,那第四列的操作数就是该字面值在元组中的下标(从0开始),局部变量,全局变量等同理,不过反正第五列会自动注释出来的,也不用自己去查;如果操作数没有在这些元组里,比如示例中的CALL_FUNCTION 1,表示以TOS为参数调用TOS1的函数,就没有第五列。

Python字节码混淆

没有经过任何处理的.pyc文件可以直接用在线工具或者uncompyle6反编译出.py源码,也可以用marshal和dis反汇编出可读字节码,对大佬来说可读字节码就和源码一样。我们可以在编译好的.pyc文件上加一些东西来阻止uncompyle6和dis等工具的逆向。(下面的例子只是例子,可以自己灵活处理,也可以搭配食用)

加跳转(可对抗uncompyle6)

在代码段(co_code)开头的位置加一个跳转跳到第二句(原本的第一句),提一下无条件跳转的opcode是0x71,但是要注意版本,因为2.7版本中这条指令是3字节长度,3.7中是2字节长度。

所以2.7版本加三个字节0x71 0x03 0x00,记得改前面的co_code_size字段

1
2
3
4
5
6
1           0 JUMP_ABSOLUTE            3
>> 3 LOAD_CONST 0 ('helloworld')
6 PRINT_ITEM
7 PRINT_NEWLINE
8 LOAD_CONST 1 (None)
11 RETURN_VALUE

3.7版本加两个字节0x71 0x02,记得改前面的co_code_size字段

1
2
3
4
5
6
7
8
9
10
1           0 JUMP_ABSOLUTE            2
>> 2 LOAD_CONST 0 (1099511627775)

2 4 STORE_NAME 0 (a)
6 LOAD_CONST 1 ('0')
8 LOAD_CONST 0 (1099511627775)
10 BINARY_MULTIPLY
12 STORE_NAME 1 (s)
14 LOAD_CONST 2 (None)
16 RETURN_VALUE

很明显这个跳转是无意义的,但加了之后uncompyle6就无法直接反编译出源码,但可以反编译出不完整的可读字节码。

这种方法混淆的字节码还是可以用marshal和dis来分析。

加跳转和错误语句(可对抗dis和uncompyle6)

在字节码中手动加入错误的语句,再加个跳转跳过这条错误语句,因为dis分析的时候不会跳转,而程序运行会跳转,这样就可以阻止dis反汇编出可读字节码。错误语句可以压栈一个很大的操作数,超出元组的下标就会报错。

还是要注意版本。

2.7加0x71 0x06 0x00 0x64 0xff 0xff,记得改前面的co_code_size字段,然后dis分析就会添加的错误语句处报错,无法反汇编后面的代码。

3.7加0x71 0x04 0x64 0xff,改长度。

3.7版本的示例手动还原后是这样的:

1
2
3
4
5
6
7
8
9
10
11
1           0 JUMP_ABSOLUTE            4
2 LOAD_COMST 256
4 >> 4 LOAD_CONST 0 (1099511627775)

6 STORE_NAME 0 (a)
8 LOAD_CONST 1 ('0')
10 LOAD_CONST 0 (1099511627775)
12 BINARY_MULTIPLY
14 STORE_NAME 1 (s)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE

可以看出程序运行时错误指令会被跳过。

重叠指令(可以对抗dis和uncompyle6)

嫖的2.7版本例子:

1
2
3
4
0 JUMP_ABSOLUTE        [71 05 00]     5 
3 PRINT_ITEM [47 -- --]
4 LOAD_CONST [64 64 01] 356
7 STOP_CODE [00 -- --]

第一句绝对跳转到第三局指令的操作数的位置,但是第三个指令的操作数也是一个opcode,所以跳转过来后执行的指令其实是0x64 0x01 0x00 (LOAD_CONST 1)。

Python3.6以后每条指令都是2字节好像就不大好进行这种操作了,我自己测试的时候2.7成功了3.7失败了,可能是我太菜…

私有指令集

在opcode.h文件中修改opcode与opname的匹配,修改后编译生成的pyc文件只有与其拥有相同指令集的目标才能运行,无法用于发行。

Python字节码反混淆

针对上面的加跳转和错误指令的混淆方法,会产生下标越界而报错,可以修改dis模块的源码在去值前先判断是否越界,如果越界则跳过,自己整了个3.7的例子:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if op in hasconst:
argval, argrepr = _get_const_info(arg, constants)
elif op in hasname:
argval, argrepr = _get_name_info(arg, names)
elif op in hasjrel:
argval = offset + 2 + arg
argrepr = "to " + repr(argval)
elif op in haslocal:
argval, argrepr = _get_name_info(arg, varnames)
elif op in hascompare:
argval = cmp_op[arg]
argrepr = argval
elif op in hasfree:
argval, argrepr = _get_name_info(arg, cells)
elif op == FORMAT_VALUE:
argval = ((None, str, repr, ascii)[arg & 0x3], bool(arg & 0x4))
argrepr = ('', 'str', 'repr', 'ascii')[arg & 0x3]
if argval[1]:
if argrepr:
argrepr += ', '
argrepr += 'with format'

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if op in hasconst and arg < len(constants):
argval, argrepr = _get_const_info(arg, constants)
elif op in hasname and arg < len(names):
argval, argrepr = _get_name_info(arg, names)
elif op in hasjrel:
argval = offset + 2 + arg
argrepr = "to " + repr(argval)
elif op in haslocal and arg < len(varnames):
argval, argrepr = _get_name_info(arg, varnames)
elif op in hascompare:
argval = cmp_op[arg]
argrepr = argval
elif op in hasfree and arg < len(cells):
argval, argrepr = _get_name_info(arg, cells)
elif op == FORMAT_VALUE:
argval = ((None, str, repr, ascii)[arg & 0x3], bool(arg & 0x4))
argrepr = ('', 'str', 'repr', 'ascii')[arg & 0x3]
if argval[1]:
if argrepr:
argrepr += ', '
argrepr += 'with format'

2.7版本也可。

之后就可以用dis反汇编出这种混淆的字节码。

题解

babypy

最后言归正传,hgame2020 week2 的两个python题。

第一个打开文件得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
In [1]: from secret import flag, encrypt

In [2]: encrypt(flag)
Out[2]: '7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22'

In [3]: import dis

In [4]: dis.dis(encrypt)
4 0 LOAD_FAST 0 (OOo)
2 LOAD_CONST 0 (None)
4 LOAD_CONST 0 (None)
6 LOAD_CONST 1 (-1)
8 BUILD_SLICE 3
10 BINARY_SUBSCR
12 STORE_FAST 1 (O0O)

5 14 LOAD_GLOBAL 0 (list)
16 LOAD_FAST 1 (O0O)
18 CALL_FUNCTION 1
20 STORE_FAST 2 (O0o)

6 22 SETUP_LOOP 50 (to 74)
24 LOAD_GLOBAL 1 (range)
26 LOAD_CONST 2 (1)
28 LOAD_GLOBAL 2 (len)
30 LOAD_FAST 2 (O0o)
32 CALL_FUNCTION 1
34 CALL_FUNCTION 2
36 GET_ITER
>> 38 FOR_ITER 32 (to 72)
40 STORE_FAST 3 (O0)

7 42 LOAD_FAST 2 (O0o)
44 LOAD_FAST 3 (O0)
46 LOAD_CONST 2 (1)
48 BINARY_SUBTRACT
50 BINARY_SUBSCR
52 LOAD_FAST 2 (O0o)
54 LOAD_FAST 3 (O0)
56 BINARY_SUBSCR
58 BINARY_XOR
60 STORE_FAST 4 (Oo)

8 62 LOAD_FAST 4 (Oo)
64 LOAD_FAST 2 (O0o)
66 LOAD_FAST 3 (O0)
68 STORE_SUBSCR
70 JUMP_ABSOLUTE 38
>> 72 POP_BLOCK

9 >> 74 LOAD_GLOBAL 3 (bytes)
76 LOAD_FAST 2 (O0o)
78 CALL_FUNCTION 1
80 STORE_FAST 5 (O)

10 82 LOAD_FAST 5 (O)
84 LOAD_METHOD 4 (hex)
86 CALL_METHOD 0
88 RETURN_VALUE

In [5]: exit()

输入的flag经过下面字节码的加密后得到'7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22',慢慢分析就行,加密源码如下:

1
2
3
4
5
6
7
8
def encrypt(OOo):
O0O = OOo[::-1]
O0o = list(O0O)
for O0 in range(1, len(O0o)):
Oo = O0o[O0-1] ^ O0o[O0]
O0o[O0] = Oo
O = bytes(O0o)
return O.hex()

解密函数长这样:

1
2
3
4
5
def dec(c):
c = list(c)
for i in range(len(c)-1, 0, -1):
c[i] ^= c[i-1]
return bytes(c[::-1])

babypyc

开头有一个绝对跳转(上面有介绍),所以uncompyle6反编译不了,但是可以dis+marshal走一波得到可读字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
  3           0 JUMP_ABSOLUTE            2
>> 2 LOAD_CONST 0 (0)
4 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (os)
8 STORE_NAME 0 (os)
10 LOAD_CONST 0 (0)
12 LOAD_CONST 1 (None)
14 IMPORT_NAME 1 (sys)

4 16 STORE_NAME 1 (sys)
18 LOAD_CONST 0 (0)
20 LOAD_CONST 2 (('b64encode',))
22 IMPORT_NAME 2 (base64)
24 IMPORT_FROM 3 (b64encode)
26 STORE_NAME 3 (b64encode)

6 28 POP_TOP
30 LOAD_CONST 3 (b'/KDq6pvN/LLq6tzM/KXq59Oh/MTqxtOTxdrqs8OoR3V1X09J')

8 32 STORE_GLOBAL 4 (O0o)
34 LOAD_CONST 4 (<code object getFlag at 0x0000014E615C0D20, file "task.py", line 8>)
36 LOAD_CONST 5 ('getFlag')
38 MAKE_FUNCTION 0

16 40 STORE_NAME 5 (getFlag)
42 LOAD_NAME 5 (getFlag)
44 CALL_FUNCTION 0

18 46 STORE_NAME 6 (flag)
48 LOAD_NAME 6 (flag)
50 LOAD_CONST 1 (None)
52 LOAD_CONST 6 (6)
54 BUILD_SLICE 2
56 BINARY_SUBSCR
58 LOAD_CONST 7 (b'hgame{')
60 COMPARE_OP 3 (!=)
62 POP_JUMP_IF_TRUE 76
64 LOAD_NAME 6 (flag)
66 LOAD_CONST 8 (-1)
68 BINARY_SUBSCR
70 LOAD_CONST 9 (125)
72 COMPARE_OP 3 (!=)

19 74 POP_JUMP_IF_FALSE 94
>> 76 LOAD_NAME 7 (print)
78 LOAD_CONST 10 ('Incorrect format!')
80 CALL_FUNCTION 1

20 82 POP_TOP
84 LOAD_NAME 1 (sys)
86 LOAD_METHOD 8 (exit)
88 LOAD_CONST 11 (1)
90 CALL_METHOD 1

22 92 POP_TOP
>> 94 LOAD_NAME 6 (flag)
96 LOAD_CONST 6 (6)
98 LOAD_CONST 8 (-1)
100 BUILD_SLICE 2
102 BINARY_SUBSCR

23 104 STORE_NAME 9 (raw_flag)
106 LOAD_NAME 10 (len)
108 LOAD_NAME 6 (flag)
110 CALL_FUNCTION 1
112 LOAD_CONST 12 (7)
114 BINARY_SUBTRACT
116 LOAD_CONST 13 (36)
118 COMPARE_OP 3 (!=)

24 120 POP_JUMP_IF_FALSE 140
122 LOAD_NAME 7 (print)
124 LOAD_CONST 14 ('Wrong length!')
126 CALL_FUNCTION 1

25 128 POP_TOP
130 LOAD_NAME 1 (sys)
132 LOAD_METHOD 8 (exit)
134 LOAD_CONST 15 (2)
136 CALL_METHOD 1

27 138 POP_TOP
>> 140 LOAD_NAME 9 (raw_flag)
142 LOAD_CONST 1 (None)
144 LOAD_CONST 1 (None)
146 LOAD_CONST 8 (-1)
148 BUILD_SLICE 3
150 BINARY_SUBSCR

28 152 STORE_NAME 9 (raw_flag)
154 LOAD_CONST 16 (<code object <listcomp> at 0x0000014E616835D0, file "task.py", line 28>)
156 LOAD_CONST 17 ('<listcomp>')
158 MAKE_FUNCTION 0
160 LOAD_NAME 11 (range)
162 LOAD_CONST 6 (6)
164 CALL_FUNCTION 1
166 GET_ITER
168 CALL_FUNCTION 1

30 170 STORE_NAME 12 (ciphers)
172 SETUP_LOOP 86 (to 260)
174 LOAD_NAME 11 (range)
176 LOAD_CONST 18 (5)
178 CALL_FUNCTION 1
180 GET_ITER
>> 182 FOR_ITER 74 (to 258)

31 184 STORE_NAME 13 (row)
186 SETUP_LOOP 68 (to 256)
188 LOAD_NAME 11 (range)
190 LOAD_CONST 6 (6)
192 CALL_FUNCTION 1
194 GET_ITER
>> 196 FOR_ITER 56 (to 254)

32 198 STORE_NAME 14 (col)
200 LOAD_NAME 12 (ciphers)
202 LOAD_NAME 13 (row)
204 BINARY_SUBSCR
206 LOAD_NAME 14 (col)
208 DUP_TOP_TWO
210 BINARY_SUBSCR
212 LOAD_NAME 12 (ciphers)
214 LOAD_NAME 13 (row)
216 LOAD_CONST 11 (1)
218 BINARY_ADD
220 BINARY_SUBSCR
222 LOAD_NAME 14 (col)
224 BINARY_SUBSCR
226 INPLACE_ADD
228 ROT_THREE

33 230 STORE_SUBSCR
232 LOAD_NAME 12 (ciphers)
234 LOAD_NAME 13 (row)
236 BINARY_SUBSCR
238 LOAD_NAME 14 (col)
240 DUP_TOP_TWO
242 BINARY_SUBSCR
244 LOAD_CONST 19 (256)
246 INPLACE_MODULO
248 ROT_THREE
250 STORE_SUBSCR
252 JUMP_ABSOLUTE 196
>> 254 POP_BLOCK
>> 256 JUMP_ABSOLUTE 182

35 >> 258 POP_BLOCK
>> 260 LOAD_CONST 20 (b'')

36 262 STORE_NAME 15 (cipher)
264 SETUP_LOOP 70 (to 336)
266 LOAD_NAME 11 (range)
268 LOAD_CONST 6 (6)
270 CALL_FUNCTION 1
272 GET_ITER
>> 274 FOR_ITER 58 (to 334)

37 276 STORE_NAME 13 (row)
278 LOAD_CONST 0 (0)

38 280 STORE_NAME 14 (col)
282 SETUP_LOOP 46 (to 330)
>> 284 LOAD_NAME 14 (col)
286 LOAD_CONST 6 (6)
288 COMPARE_OP 0 (<)
290 EXTENDED_ARG 1

39 292 POP_JUMP_IF_FALSE 328
294 LOAD_NAME 15 (cipher)
296 LOAD_NAME 16 (bytes)
298 LOAD_NAME 12 (ciphers)
300 LOAD_NAME 13 (row)
302 BINARY_SUBSCR
304 LOAD_NAME 14 (col)
306 BINARY_SUBSCR
308 BUILD_LIST 1
310 CALL_FUNCTION 1
312 INPLACE_ADD

40 314 STORE_NAME 15 (cipher)
316 LOAD_NAME 14 (col)
318 LOAD_CONST 11 (1)
320 INPLACE_ADD
322 STORE_NAME 14 (col)
324 EXTENDED_ARG 1
326 JUMP_ABSOLUTE 284
>> 328 POP_BLOCK
>> 330 EXTENDED_ARG 1
332 JUMP_ABSOLUTE 274

42 >> 334 POP_BLOCK
>> 336 LOAD_NAME 3 (b64encode)
338 LOAD_NAME 15 (cipher)
340 CALL_FUNCTION 1

44 342 STORE_NAME 15 (cipher)
344 LOAD_NAME 15 (cipher)
346 LOAD_GLOBAL 4 (O0o)
348 COMPARE_OP 2 (==)
350 EXTENDED_ARG 1

45 352 POP_JUMP_IF_FALSE 364
354 LOAD_NAME 7 (print)
356 LOAD_CONST 21 ('Great, this is my flag.')
358 CALL_FUNCTION 1
360 POP_TOP

47 362 JUMP_FORWARD 8 (to 372)
>> 364 LOAD_NAME 7 (print)
366 LOAD_CONST 22 ('Wrong flag.')
368 CALL_FUNCTION 1
370 POP_TOP
>> 372 LOAD_CONST 1 (None)
374 RETURN_VALUE

Disassembly of <code object getFlag at 0x0000014E615C0D20, file "task.py", line 8>:
10 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Give me the flag')
4 CALL_FUNCTION 1
6 POP_TOP

11 8 LOAD_GLOBAL 1 (input)
10 LOAD_CONST 2 ('> ')
12 CALL_FUNCTION 1
14 STORE_FAST 0 (flag)

12 16 LOAD_FAST 0 (flag)
18 LOAD_METHOD 2 (encode)
20 CALL_METHOD 0
22 STORE_FAST 0 (flag)

13 24 LOAD_CONST 3 (b'Qre50rOeWr3CsrJ4ccefvNO8n9hclqDNztdbco6pZ3IwRV5Q')
26 STORE_GLOBAL 3 (O0o)

14 28 LOAD_FAST 0 (flag)
30 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x0000014E616835D0, file "task.py", line 28>:
28 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 26 (to 32)
6 STORE_DEREF 0 (col)
8 LOAD_CLOSURE 0 (col)
10 BUILD_TUPLE 1
12 LOAD_CONST 0 (<code object <listcomp> at 0x0000014E616786F0, file "task.py", line 28>)
14 LOAD_CONST 1 ('<listcomp>.<listcomp>')
16 MAKE_FUNCTION 8
18 LOAD_GLOBAL 0 (range)
20 LOAD_CONST 2 (6)
22 CALL_FUNCTION 1
24 GET_ITER
26 CALL_FUNCTION 1
28 LIST_APPEND 2
30 JUMP_ABSOLUTE 4
>> 32 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x0000014E616786F0, file "task.py", line 28>:
28 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 20 (to 26)
6 STORE_FAST 1 (row)
8 LOAD_GLOBAL 0 (raw_flag)
10 LOAD_CONST 0 (6)
12 LOAD_FAST 1 (row)
14 BINARY_MULTIPLY
16 LOAD_DEREF 0 (col)
18 BINARY_ADD
20 BINARY_SUBSCR
22 LIST_APPEND 2
24 JUMP_ABSOLUTE 4
>> 26 RETURN_VALUE

两个嵌套的函数可能有点难度,其他很简单,慢慢分析就行。

官方给的源码长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import os
import sys
from base64 import b64encode
O0o = b'/KDq6pvN/LLq6tzM/KXq59Oh/MTqxtOTxdrqs8OoR3V1X09J'


def getFlag():
global O0o
print('Give me the flag')
flag = input('> ')
flag = flag.encode()
O0o = b'Qp+ng3SeWoXClJN4cYm3frO8n5rIqL/Nrreuks7JR1JPM19w'
return flag


flag = getFlag()
if (flag[:6] != b'hgame{') or (flag[-1] != 125):
print('Incorrect format!')
sys.exit(1)
raw_flag = flag[6:-1]
if len(flag) - 7 != 36:
print('Wrong length!')
sys.exit(2)
raw_flag = raw_flag[::-1]
ciphers = [[raw_flag[6*row+col] for row in range(6)] for col in range(6)]
for row in range(5):
for col in range(6):
ciphers[row][col] += ciphers[row+1][col]
ciphers[row][col] %= 256
cipher = b''
for row in range(6):
col = 0
while col < 6:
cipher += bytes([ciphers[row][col]])
col += 1
cipher = b64encode(cipher)
if cipher == O0o:
print('Great, this is my flag.')
else:
print('Wrong flag.')

我的脚本长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from base64 import b64decode
key = b64decode(b'Qre50rOeWr3CsrJ4ccefvNO8n9hclqDNztdbco6pZ3IwRV5Q')
raw_flag_list = [0]*36
raw_flag = ''
key = [i for i in key]
for row in range(5, -1, -1):
for col in range(6):
if(row == 5):
raw_flag_list[row*6+col] = key[row*6+col]
else:
raw_flag_list[row*6+col] = key[row*6+col] - \
raw_flag_list[(row+1)*6+col]
for col in range(6):
for row in range(6):
raw_flag += chr(raw_flag_list[row*6+col])
flag = 'hgame{'+raw_flag[::-1]+'}'
print(flag)
#hgame{PYtH0n^0pcOdE-iS_s0+1nTeresTiNgg89!!}