记一次手撸CPython bytecode

上周末的0CTF出现了一个pyc的题目,但是Pyopcode损坏,于是手撸了一波

题目: https://github.com/Hcamael/CTF_repo/tree/master/0CTF%202017/Re3%28py%29

通过pyc还原出py网上的资料挺多了,py也有专门的库可以还原,但是0CTF这题却无法还原,目测是opcode损坏,同时根据题目描述,也知道是要修复pyc文件。

这里用到两个库,一个dis,可以把二进制反编译CPython bytecode。一个是marshal,可以把字符串转换成pyopcode对象

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
>>> import dis, marshal
>>> f = open("crypt.pyc")
>>> f.read(4)
'\x03\xf3\r\n' # magic number
>>> f.read(4) # time
'f4oX'
>>> code = marshal.load(f)
# 对我们有用的属性有:
>>> code.co_argcount # 参数的个数
0
>>> code.co_varnames # 局部变量
()
>>> code.co_consts # 常量
(-1, None, <code object encrypt at 0x7f1987df65b0, file "/Users/hen/Lab/0CTF/py/crypt.py", line 2>, <code object decrypt at 0x7f1987e10430, file "/Users/hen/Lab/0CTF/py/crypt.py", line 10>)
# 从这个常量中我们可以看出,该py文件中定义了两个函数,encrypt和decrypt
>>> code.co_code
'\x99\x00\x00\x99\x01\x00\x86\x00\x00\x91\x00\x00\x99\x02\x00\x88\x00\x00\x91\x01\x00\x99\x03\x00\x88\x00\x00\x91\x02\x00\x99\x01\x00S'
# CPython bytecode的二进制, 可以通过dis反编译
>>> dis.disassemble_string(code.co_code)
0 <153> 0
3 <153> 1
6 MAKE_CLOSURE 0
9 EXTENDED_ARG 0
12 <153> 2
15 LOAD_DEREF 0
18 EXTENDED_ARG 1
21 <153> 3
24 LOAD_DEREF 0
27 EXTENDED_ARG 2
30 <153> 1
33 RETURN_VALUE
# 发现bytecode损坏,根本无法阅读

二进制对应的bytecode可以参考: https://github.com/Python/cpython/blob/2.7/Include/opcode.h

从上面的参考连接可以得知153没有对应的bytecode,所以猜测bytecode损坏

每个bytecode所代表的意义: https://docs.Python.org/2/library/dis.html

1
2
3
4
5
>>> code.co_name              # 当前对象名 
'<module>'
>>> code.co_names # 当前对象中使用的对象名
('rotor', 'encrypt', 'decrypt')
# 从上可以看出,encrypt和decrypt是我们定义的两个函数,那么rotor我们可以猜测是通过import rotor得来的

rotor的使用可以参考: https://docs.Python.org/2.0/lib/module-rotor.html

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
# 我们可以通过以下方式查看两个函数中的信息
>>> enc = code.co_consts[2]
>>> dec = code.co_consts[3]
>>> enc.co_argcount
1
>>> dec.co_argcount
1
# 两个函数中都有一个传入的参数
>>> enc.co_varnames
('data', 'key_a', 'key_b', 'key_c', 'secret', 'rot')
>>> dec.co_varnames
('data', 'key_a', 'key_b', 'key_c', 'secret', 'rot')
# 两个函数中的局部变量, 我们可以猜测,data是传入的参数,需要加解密的数据
>>> enc.co_consts
(None, '!@#$%^&*', 'abcdefgh', '<>{}:"', 4, '|', 2, 'EOF')
>>> dec.co_consts
(None, '!@#$%^&*', 'abcdefgh', '<>{}:"', 4, '|', 2, 'EOF')
# 两个函数中的常量,我们可以猜测key_a, key_b, key_c三个变量对应的值
>>> enc.co_code
"\x99\x01\x00h\x01\x00\x99\x02\x00h\x02\x00\x99\x03\x00h\x03\x00a\x01\x00\x99\x04\x00F\x99\x05\x00'a\x02\x00a\x01\x00'a\x03\x00'\x99\x06\x00F'\x99\x05\x00'a\x02\x00\x99\x06\x00F'\x99\x07\x00'h\x04\x00\x9b\x00\x00`\x01\x00a\x04\x00\x83\x01\x00h\x05\x00a\x05\x00`\x02\x00a\x00\x00\x83\x01\x00S"
>>> dec.co_code
"\x99\x01\x00h\x01\x00\x99\x02\x00h\x02\x00\x99\x03\x00h\x03\x00a\x01\x00\x99\x04\x00F\x99\x05\x00'a\x02\x00a\x01\x00'a\x03\x00'\x99\x06\x00F'\x99\x05\x00'a\x02\x00\x99\x06\x00F'\x99\x07\x00'h\x04\x00\x9b\x00\x00`\x01\x00a\x04\x00\x83\x01\x00h\x05\x00a\x05\x00`\x02\x00a\x00\x00\x83\x01\x00S"
# 发现两个函数bytecode的二进制是一样的,操作是一样的?
>>> enc.co_name
'encrypt'
>>> enc.co_names
('rotor', 'newrotor', 'encrypt')
>>> dec.co_name
'decrypt'
>>> dec.co_names
('rotor', 'newrotor', 'decrypt')
# 通过研究rotor的用法,猜测两个函数的区别可能是在于rotor.newrotor(key).encrypt(data)和rotor.newrotor(key).decrypt(data)

所以现在的问题就在于,key是怎么来的,然后就开始了手撸CPython bytecode

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
>>> dis.disassemble_string(dec.co_code)
0 <153> 1
3 BUILD_SET 1
6 <153> 2
9 BUILD_SET 2
12 <153> 3
15 BUILD_SET 3
18 STORE_GLOBAL 1 (1)
21 <153> 4
24 PRINT_EXPR
25 <153> 5
28 <39>
29 STORE_GLOBAL 2 (2)
32 STORE_GLOBAL 1 (1)
35 <39>
36 STORE_GLOBAL 3 (3)
39 <39>
40 <153> 6
43 PRINT_EXPR
44 <39>
45 <153> 5
48 <39>
49 STORE_GLOBAL 2 (2)
52 <153> 6
55 PRINT_EXPR
56 <39>
57 <153> 7
60 <39>
61 BUILD_SET 4
64 <155> 0
67 DELETE_ATTR 1 (1)
70 STORE_GLOBAL 4 (4)
73 CALL_FUNCTION 1
76 BUILD_SET 5
79 STORE_GLOBAL 5 (5)
82 DELETE_ATTR 2 (2)
85 STORE_GLOBAL 0 (0)
88 CALL_FUNCTION 1
91 RETURN_VALUE

右边的数字为操作数,括号里的是注释

因为题目啥信息也没给我们。。。所以修bytecode只能靠猜

我们先假设这里所有的153为同一个操作符,同理所有的39也为同一个

先看第一部分

1
2
3
4
5
6
 0 <153>               1
3 BUILD_SET 1
6 <153> 2
9 BUILD_SET 2
12 <153> 3
15 BUILD_SET 3

这是最容易猜的地方,右边的操作数为123,在看常量和局部变量的tuple,可以猜测是:

1
2
3
key_a = '!@#$%^&*'
key_b = 'abcdefgh'
key_c = '<>{}:"'

然后去上面给的参考文档里,查出对应的bytecode

1
2
3
4
5
6
 0 LOAD_CONST           1
3 STORE_FAST 1
6 LOAD_CONST 2
9 STORE_FAST 2
12 LOAD_CONST 3
15 STORE_FAST 3

再去opcode.h中查其对应的值进行替换

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
>>> dis.disassemble_string(dec.co_code.replace("\x99","\x64").replace("\x68","\x7d"))
0 LOAD_CONST 1 (1)
3 STORE_FAST 1 (1)
6 LOAD_CONST 2 (2)
9 STORE_FAST 2 (2)
12 LOAD_CONST 3 (3)
15 STORE_FAST 3 (3)
18 STORE_GLOBAL 1 (1)
21 LOAD_CONST 4 (4)
24 PRINT_EXPR
25 LOAD_CONST 5 (5)
28 <39>
29 STORE_GLOBAL 2 (2)
32 STORE_GLOBAL 1 (1)
35 <39>
36 STORE_GLOBAL 3 (3)
39 <39>
40 LOAD_CONST 6 (6)
43 PRINT_EXPR
44 <39>
45 LOAD_CONST 5 (5)
48 <39>
49 STORE_GLOBAL 2 (2)
52 LOAD_CONST 6 (6)
55 PRINT_EXPR
56 <39>
57 LOAD_CONST 7 (7)
60 <39>
61 STORE_FAST 4 (4)
64 <155> 0
67 DELETE_ATTR 1 (1)
70 STORE_GLOBAL 4 (4)
73 CALL_FUNCTION 1
76 STORE_FAST 5 (5)
79 STORE_GLOBAL 5 (5)
82 DELETE_ATTR 2 (2)
85 STORE_GLOBAL 0 (0)
88 CALL_FUNCTION 1
91 RETURN_VALUE

继续,发现肛不动了。。。。然后从底下开始肛

1
2
3
4
5
6
7
8
9
10
64 <155>               0
67 DELETE_ATTR 1 (1)
70 STORE_GLOBAL 4 (4)
73 CALL_FUNCTION 1
76 STORE_FAST 5 (5)
79 STORE_GLOBAL 5 (5)
82 DELETE_ATTR 2 (2)
85 STORE_GLOBAL 0 (0)
88 CALL_FUNCTION 1
91 RETURN_VALUE

根据之前得到的结论,可以猜测这里的代码是:

1
2
xxx = rotor.newrotor(secret)
return xxx.decrypt(data)

所以猜测上面的操作数,0指的是局部变量data,1指的是全局变量newrotor, 2猜测可能是全局变量decrypt, 4指的是局部变量secret, 5指的是局部变量rot,<155>的0可能指的是全局变量rotor

然后查找bytecode,改成:

1
2
3
4
5
6
7
8
9
10
64 LOAD_GLOBAL         0
67 LOAD_ATTR 1 (1)
70 LOAD_FAST 4 (4)
73 CALL_FUNCTION 1
76 STORE_FAST 5 (5)
79 LOAD_FAST 5 (5)
82 LOAD_ATTR 2 (2)
85 LOAD_FAST 0 (0)
88 CALL_FUNCTION 1
91 RETURN_VALUE

发现,合情合理,使人姓胡…..

然后和上面一样,整体替换bytecode:

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
>>> dis.disassemble_string(dec.co_code.replace("\x99","\x64").replace("\x68","\x7d").replace("\x61","\x7c").replace("\x60","\x6a").replace("\x9b","\x74"))
0 LOAD_CONST 1 (1)
3 STORE_FAST 1 (1)
6 LOAD_CONST 2 (2)
9 STORE_FAST 2 (2)
12 LOAD_CONST 3 (3)
15 STORE_FAST 3 (3)
18 LOAD_FAST 1 (1)
21 LOAD_CONST 4 (4)
24 PRINT_EXPR
25 LOAD_CONST 5 (5)
28 <39>
29 LOAD_FAST 2 (2)
32 LOAD_FAST 1 (1)
35 <39>
36 LOAD_FAST 3 (3)
39 <39>
40 LOAD_CONST 6 (6)
43 PRINT_EXPR
44 <39>
45 LOAD_CONST 5 (5)
48 <39>
49 LOAD_FAST 2 (2)
52 LOAD_CONST 6 (6)
55 PRINT_EXPR
56 <39>
57 LOAD_CONST 7 (7)
60 <39>
61 STORE_FAST 4 (4)
64 LOAD_GLOBAL 0 (0)
67 LOAD_ATTR 1 (1)
70 LOAD_FAST 4 (4)
73 CALL_FUNCTION 1
76 STORE_FAST 5 (5)
79 LOAD_FAST 5 (5)
82 LOAD_ATTR 2 (2)
85 LOAD_FAST 0 (0)
88 CALL_FUNCTION 1
91 RETURN_VALUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
18 LOAD_FAST           1 (1)
21 LOAD_CONST 4 (4)
24 PRINT_EXPR
25 LOAD_CONST 5 (5)
28 <39>
29 LOAD_FAST 2 (2)
32 LOAD_FAST 1 (1)
35 <39>
36 LOAD_FAST 3 (3)
39 <39>
40 LOAD_CONST 6 (6)
43 PRINT_EXPR
44 <39>
45 LOAD_CONST 5 (5)
48 <39>
49 LOAD_FAST 2 (2)
52 LOAD_CONST 6 (6)
55 PRINT_EXPR
56 <39>
57 LOAD_CONST 7 (7)
60 <39>
61 STORE_FAST 4 (4)

猜测这部分就是局部变量secret计算的方法,最后一句STORE_FAST 4,猜测就是把上面计算后的值储存到secret

整体看看,发现主要就剩<39>PRINT_EXPR不通顺了。。。然后他们都是没操作数的,所以排除了调用函数,调用属性之类的

之后联想到最开头对key_a, key_b, key_c的赋值,然后目前的bytecode中没有任何运算操作

再来看这部分

1
2
3
18 LOAD_FAST           1 ("!@#$%^&*")
21 LOAD_CONST 4 (4)
24 PRINT_EXPR

所以猜测PRINT_EXPR, 是字符串和整型之间的操作运算

1
2
3
29 LOAD_FAST           2 ("abcdefgh")
32 LOAD_FAST 1 ("!@#$%^&*")
35 <39>

这里猜测<39>是字符串和字符串之间的操作运算

到这里,我们来想想,整型和字符串之间的操作有啥?

1
2
3
4
>>> "a"*3
>>> "aaa"[1]
>>> "aaa"[:1]
>>> "aaa"[1:]

字符串和字符串之间呢?

1
>>> "aaa" + "bbb"                 # 我只想到了这一个

从上面可以猜测出,<39>可能是字符串拼接的操作,然后PRINT_EXPR需要一个一个去试,现在我们可以还原出decrypt函数了:

1
2
3
4
5
6
7
8
9
10
11
# import rotor
def decrypt(data):
key_a = "!@#$%^&*"
key_b = "abcdefgh"
key_c = '<>{}:"'
secret=key_a*4 + "|" + (key_b+key_a+key_c)*2 + "|" + key_b*2 + "EOF"
# secret=key_a[4] + "|" + (key_b+key_a+key_c)[2] + "|" + key_b[2] + "EOF"
# secret=key_a[4:] + "|" + (key_b+key_a+key_c)[2:] + "|" + key_b[2:] + "EOF"
# secret=key_a[:4] + "|" + (key_b+key_a+key_c)[:2] + "|" + key_b[:2] + "EOF"
rot = rotor.newrotor(secret)
return rot.decrypt(data)

简直难受…….

记一次手撸CPython bytecode

https://nobb.site/2017/03/20/0x2f/

Author

Hcamael

Posted on

2017-03-20

Updated on

2017-08-08

Licensed under