声明:本篇的作者 Moonshadow(CCSR小组重要成员之一)

题目:逆向题Chaoyang与传统逆向题目不同,为一个文本文件,链接: https://pan.baidu.com/s/1krpOfyCpLAL7ZN5C-PsogA 提取码: 48fe

目录

  1. 背景知识
    1. Python编译字节码的操作步骤(编译过程):
      1. 命令一: python –m py_compile foo.py
      2. 命令二: python –O –m py_compile foo.py
    2. 通过上述命令生成的字节码文件pyc文件或pyo文件可以通过python内置的dis进行反汇编,并输出指令序列,反汇编py函数如下:
  2. 逆向分析
    1. 函数调用
    2. For循环
    3. If条件分支
  3. 求解算法
  4. 后记
  5. 参考资料
  6. 再次感谢Moonshadow@CCSR的友情分享

背景知识

逆向题Chaoyang与传统逆向题目不同,为一个文本文件,文件中记录若干操作指令,查询得到,文本文件中的指令集为Python脚本的字节码反汇编指令集,格式如下所示:

Python提供的内置dis包对中间字节码进行反汇编,输出上图所示的执行指令序列。以下通过示例展示Python字节码的编译过程和反编译过程。

Python编译字节码的操作步骤(编译过程):

命令一: python –m py_compile foo.py

该命令生成pyc文件,即将python源代码py文件编译为pyc字节码文件,实现源代码的封装,以及执行代码的性能提升。

命令二: python –O –m py_compile foo.py

该命令生成优化的字节码文件,即pyo文件,该文件的大小比pyc小,因此可以提升加载执行时的性能(但并不能提升运行性能)。此外,另一个优化参数-OO在-O的基础上删除assert语句等,进一步提升性能。

通过上述命令生成的字节码文件pyc文件或pyo文件可以通过python内置的dis进行反汇编,并输出指令序列,反汇编py函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def load_bytecode(filename) :
file = open(filename,"rb")
header = file.read(8)
if not header.startswith('\x03\xF3\x0D\x0A'):
raise SystemExit('[!] Header mismatch. The input file is not a valid pyc file.')
rootCodeObject = marshal.load(file)
print("Code Frame: ")
dis.disassemble(rootCodeObject)
try:
if len(rootCodeObject.co_names) > 1 :
for index in range(2, len(rootCodeObject.co_consts)):
print("Code Block: " + rootCodeObject.co_names[index])
dis.disassemble(rootCodeObject.co_consts[index])
except AttributeError:
print("Disassemble "+rootCodeObject.co_names[index]+"error.")

通过上述代码调用dis包输出的反汇编指令序列,可通过手工或自动化方式反编译为Python源码。

逆向分析

通过查阅Python官方文档,Python字节码采用逆波兰表达式存储汇编指令,因此,反汇编的指令助记符和参数也为逆波兰表达式,Chaoyang文件中的字节码汇编指令如下:

笔者采用手工方式(实践中尚未发现有效的将汇编代码文本转为Python源代码的工具)对汇编指令序列进行反编译,获得Python源码,以提高代码可读性。

常见的汇编指令块对应的Python源代码举例如下(更多的示例请见文末参考资料[2]):

函数调用

For循环

If条件分支

最终获得Chaoyang文件中包含4个Python函数,分别为:

full_sieve(n, output) ——获得小于等于输入参数n的所有质数列表

get_fs(n, plist=None) ——因式分解,获得输入参数n的所有因子列表

get_p_factors(n, plist=None) ——所有小于等于输入参数n的质数列表中,可以被输入参数n整除的质因子

main_function() ——获得用户输入,计算后满足条件即输出flag

求解算法

main_function函数逻辑表明,本题的输入为用逗号(,)分割的2个int型数值。分析后可以看到,本体有多组答案,只要满足条件即可输出flag,满足条件的代码为:

对该条件写出求解算法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def solve_ get_fs ():
for i in range(40000, 60000) : # 为提高解题性能,循环范围为4W – 6W
res = 0
for x in get_fs(i):
res += x
if 165416 > res and abs(res - 165416) < 1000 :
print("num1=%d, res1=%d"%(i, res))
solve_ get_ factors (165416 - res)

def solve_ get_ factors (num):
for i in range(2, 10000) : #为提高解题性能,循环阈值取10000
res = 0
for x in get_p_factors(i):
res += x[0]
if res == num :
print("num2=%d, res2=%d"%(i, res))
if res > 165416 :
break

运行上述求解算法,共得到11组满足需求的输入值,示例如下:

第一组

num1=50880, res1=164592

num2=2463, res2=824

num2=7389, res2=824

第二组

num1=52272, res1=164920

num2=2455, res2=496

num2=2946, res2=496

num2=5892, res2=496

num2=6818, res2=496

num2=8143, res2=496

num2=8838, res2=496

第三组(以后省略)

任选以上一组结果,输入格式为num1,num2,即可返回flag。

完整解题脚本如下:

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
import dis
import math
import datetime
import sys

def full_sieve(n, output):
nroot = int(math.sqrt(n))

sieve = range(n+1)

sieve[1] = 0

for x in xrange(2, nroot+1) :
if sieve[x] != 0 :
m = n / x - x
sieve[x**2 : n+1 : x] = [0] * (m + 1)
if type(output) == dict :
pmap = {}
for x in sieve :
if x != 0:
pmap[x] = True
elif type(output) == list :
return [x for x in sieve if x != 0]

return None

def get_fs(n, plist = None):
if plist is None:
plist = full_sieve(n, ['output'])
fcount = {}
for p in plist:
if p > n :
break

if n % p == 0:
fcount[p] = 0
while n % p == 0:
n /= p
fcount[p] += 1

factors = [1]
for x in fcount :
level = []
exp = [x ** (i+1) for i in xrange(fcount[x])]
for j in exp:
level.extend([j * x for x in factors])
factors.extend(level)

return factors

def get_p_factors(n, plist = None):
if plist == None :
plist = full_sieve(n, ['output'])
fs = []
for p in plist:
count = 0
while n % p == 0:
n /= p
count += 1
if count > 0:
fs.append((p,count))
return fs

def foo():
flag = 'FLAG'
cred = raw_input('yinhangkahao: ')
try :
cred = map(int,((cred.split(','))))
except :
raise ValueError('..')
res = 0
for x in get_fs(cred[0]) :
res += x
for x in get_p_factors(cred[1]) :
res += x[0]

if res == 165416 :
print('flag{%s}'%flag)

def baopo3():
starttime = datetime.datetime.now()

find_flag = False
num1 = 2
while find_flag == False:
num2 = 2
res = 0
for x in get_fs(num1):
res += x
while find_flag == False:
res1 = res
for x in get_p_factors(num2) :
res1 += x[0]
if res1 == 165416:
find_flag = True
print(n, num1, num2)
endtime = datetime.datetime.now()
print (endtime - starttime).seconds
exit(0)
if num2 > sys.maxint :
break
print("dealing : num1=%d, num2=%d, res=%d" % (num1, num2, res1))
num2 +=1
if num1 > sys.maxint :
break
num1 += 1

def baopo2():
starttime = datetime.datetime.now()

n = 5
find_flag = False
while find_flag == False :
num1 = 500
num2 = 100
print("dealing : n=%d, num1=%d" % (n, num1))
endtime = datetime.datetime.now()
print("lasting time: %d" % (endtime - starttime).seconds)
res = 10
l1 = full_sieve(num1, ['output'])
fs_list = get_fs(l1, n)
print(fs_list)
for x in fs_list:
res += x[0]
l2 = full_sieve(num2, ['output'])
factors_list = get_p_factors(l2, n)
print(factors_list)
for x in factors_list:
res += x
if res == 165416:
find_flag = True
print(n, num1, num2)
endtime = datetime.datetime.now()
print (endtime - starttime).seconds
exit(0)
print("dealing : n=%d, num1=%d, num2=%d, res=%d" % (n, num1, num2, res))
n += 1
endtime = datetime.datetime.now()
print (endtime - starttime).seconds


def baopo():
starttime = datetime.datetime.now()

n = 5
find_flag = False
while find_flag == False :
num1 = 500
while find_flag == False :
num2 = 500
print("dealing : n=%d, num1=%d" % (n, num1))
endtime = datetime.datetime.now()
print("lasting time: %d"%(endtime-starttime).seconds)
while find_flag == False :
res = 5
l1 = full_sieve(num1,['output'])
fs_list = get_fs(l1,n)
print(fs_list)
for x in fs_list:
res += x[0]
l2 = full_sieve(num2,['output'])
factors_list = get_p_factors(l2,n)
print(factors_list)
for x in factors_list:
res += x
print("dealing : n=%d, num1=%d, num2=%d, res=%d"%(n,num1,num2,res))
if res == 165416 :
find_flag = True
print(n,num1,num2)
endtime = datetime.datetime.now()
print (endtime - starttime).seconds
exit(0)
if res > 165416 :
break
num2 += 1
if num1 > 165416 -5 :
break
num1 += 1
n += 1
endtime = datetime.datetime.now()
print (endtime - starttime).seconds

def baopo4():
for i in range(40000, 60000) :
res = 0
for x in get_fs(i):
res += x
if 165416 > res and abs(res - 165416) < 1000 :
print("num1=%d, res1=%d"%(i, res))
baopo5(165416 - res)

def baopo5(num):
for i in range(2, 10000) :
res = 0
for x in get_p_factors(i):
res += x[0]

if res == num :
print("num2=%d, res2=%d"%(i, res))

if res > 165416 :
break

def temp():
for i in range(2, 10000) :
res = 0
for x in get_p_factors(i):
res += x[0]
print(res)

if __name__ == '__main__':
# dis.dis(get_p_factors)
# result = full_sieve(13,['output'])
# print(result)
# result = get_fs()
# print(result)
# result = get_p_factors()
# print(result)
# baopo3()
baopo4()
# temp()

后记

本题主要考察Python字节码和反汇编技术的理解和应用,目前已知的若干Python反编译工具,均支持将pyc或pyo字节码文件反编译为Python源码文件。但调研中发现,目前尚没有将字节码指令序列文本转为Python源码的工具,可研发该工具用于Python字节码分析。

参考资料

[1] Python字节码指令集(官方)

https://docs.python.org/2/library/dis.html

[2] Python字节码指令集和示例代码

http://unpyc.sourceforge.net/Opcodes.html

[3] Deobfuscating Python Bytecode

https://www.fireeye.com/blog/threat-research/2016/05/deobfuscating_python.html

再次感谢Moonshadow@CCSR的友情分享