文章目录
  1. 1. 遇到问题
  2. 2. pyc
  3. 3. celery
  4. 4. 排除pyc干扰
  5. 5. pyc逆向
  6. 6. 上手逆
  7. 7. 防逆向

修改了程序源码,使用celery调用task调了半天,结果发现程序竟然一直没有生效。尝试过使源程序报错,从日志里找到信息,但是程序依然运行良好。

遇到问题

修改了程序源码,使用celery调用task调了半天,结果发现程序竟然一直没有生效。尝试过使源程序报错,从日志里找到信息,但是程序依然运行良好。

搜了关于celery rabbitmq yara的相关问题,没有找到结果,最后无意中找到了问题的所在,自己还是太嫩。。

pyc

pyc文件是用来保存python虚拟机编译生成的byte code 的。在python的运行过程中,如果遇到import首先在设定好的path中寻找对应的.pyc或者.dll 文件。如果没有这些文件,则编译成对应的PycodeObject并向.pyc文件写入中间结果。也就是在你 import 别的 py 文件时,那个 py 文件会被存一份 pyc 加速下次装载。具体原理大神说的很清楚https://www.zhihu.com/question/30296617

celery

命令行执行celery worker -A –loglevel=info时,必须可导入,所以可以为PY模块或包,但需要注意的不管是包还是模块都必须正确指定Celery入口文件(如果为包则默认的入口文件名为celery.py)的绝对导入名称(app/work.app),Celery通过动态导入获取实例化后的应用,通过实例化时指定的配置以及include来依次导入任务执行文件中的任务指定单元,然后就是等待任务,可以看出Celery是通过相对/绝对导入来查找定义的任务执行单元,PY导入成功后会生成PYC文件,所以代码修改后一定要先删除PYC文件

排除pyc干扰

1
sudo find /project -name "*.pyc" | xargs rm -rf

在打包时忽略 .pyc 文件或许是个更方便的办法。
tar和zip都可以加上 –exclude=*.pyc 参数来排除 pyc 文件

pyc逆向

既然遇到了pyc问题,就顺便学学pyc的逆向吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
[demo.py]
class A:
pass


def Fun():
pass


value = 1
str = “Python”
a = A()
Fun()

Python在执行demo.py文件时,首先进行编译,编译的结果有字节码(py文件中操作的处理结果)和Python源代码中包含的静态的信息,如字符串常量等。编译的结果存储在Python运行期的一个对象(PyCodeObject对象)中,当Python运行结束后,这些信息甚至还会被存储在一种文件(Pyc文件,通过marshal序列化)中。

所以PyCodeObject就是Python源代码编译之后的关于程序的静态信息的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PyCodeObject长这样
[compile.h]
/* Bytecode object */
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
PyObject *co_code; /* instruction opcodes */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
/* The rest doesn't count for hash/cmp */
PyObject *co_filename; /* string (where it was loaded from) */
PyObject *co_name; /* string (name, for reference) */
int co_firstlineno; /* first source line number */
PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
} PyCodeObject;

对应一个作用域,会创建一个PyCodeObject与这段Code Block对应。

1
2
3
4
5
6
7
8
9
10
11
[CodeObject.py]
class A:
pass


def Fun():
pass


a = A()
Fun()

在Python编译完成后,一共会创建3个PyCodeObject对象,一个是对应CodeObject.py的,一个是对应class A这段Code(作用域),而最后一个是对应def Fun这段Code的。可以通过Pyc文件保存PyCodeObject

其中co_lnotab记录byte code和source code的对应信息,记录这些信息间的增量值,所以,对应的co_lnotab就应该是:0,1, 6,1, 44,5

Byte code在co_code中的偏移 .py文件中源代码的行数
0 1
6 2
50 7

一个pyc文件的构成就像下面的结构一样,一板一眼,按顺序往下代就是,遇到不理解的常量什么的就去查python源码头文件,对于opcode的意义可以查https://docs.Python.org/2/library/dis.html

Python .pyc file structure代码

============== <- 文件起始。下面是文件头信息  
pyc_magic     (=0xD1 0xF2 0x0D 0x0A,4字节,简单校验.pyc的魔数)  
mtime         (4字节,.pyc对应的源文件的修改时间)  
============== <- 顶层PyCodeObject起始。下面都属于顶层代码对象  
TYPE_CODE     (='c',1字节,PyCodeObject的类型标识)  
co_argscount  (4字节,位置参数的个数)  
co_nlocals    (4字节,局部变量(包括位置参数)的个数)  
co_stacksize  (4字节,求值栈的最大深度)  
co_flags      (4字节,用来表示参数中是否有*args或者 **kwargs)  
co_code       (PyStringObject,Code Block编译所得的字节码)  
co_consts     (PyTupleObject,常量池)  
co_names      (PyTupleObject,所有用到的符号的集合)  
co_varnames   (PyTupleObject,局部变量名集合,在本代码段中赋值,但没有被内层代码引用的变量)  
co_freevars   (PyTupleObject,自由变量的变量名集合,在本层引用,在外层赋值的变量)  
co_cellvars   (PyTupleObject,被闭包捕获的局部变量的变量名集合,本层赋值,且被内层代码段引用的变量)  
co_filename   (PyStringObject,源文件名,Code Block所对应的.py文件的完整路径)  
co_name;      /* string (name, for reference) Code Block的名字,通常是函数名或类名*/  
co_firstlineno(4字节,该代码对象中源码的首行对应行号)  
co_lnotab     (PyStringObject,字节码偏移量与源码行号的对应关系)  
============== <- 顶层PyCodeObject结束。文件结束 

Pystringobject layout in .pyc files代码

=========== <- PyStringObject起始  
TYPE_STRING(='s',1字节,PyStringObject的类型标识)  
length     (4字节,字符串内容的长度)  
data       (byte数组,字符串内容)  
=========== <- PyStringObject结束  

co_consts常量表

#define TYPE_NULL               '0'  
#define TYPE_NONE               'N'  
#define TYPE_FALSE              'F'  
#define TYPE_TRUE               'T'  
#define TYPE_STOPITER           'S'  
#define TYPE_ELLIPSIS           '.'  
#define TYPE_INT                'i'  
#define TYPE_INT64              'I'  
#define TYPE_FLOAT              'f'  
#define TYPE_BINARY_FLOAT       'g'  
#define TYPE_COMPLEX            'x'  
#define TYPE_BINARY_COMPLEX     'y'  
#define TYPE_LONG               'l'  
#define TYPE_STRING             's'  
#define TYPE_INTERNED           't'  
#define TYPE_STRINGREF          'R'  
#define TYPE_TUPLE              '('  
#define TYPE_LIST               '['  
#define TYPE_DICT               '{'  
#define TYPE_CODE               'c'  
#define TYPE_UNICODE            'u'  
#define TYPE_UNKNOWN            '?'  
#define TYPE_SET                '<'  
#define TYPE_FROZENSET          '>'  

上手逆

最简单的没做过处理的可以用这个

1
2
https://github.com/wibiti/uncompyle2
python setup.py install

有经验的py程序员会在发布程序的时候修改pyc的字节比如头8个字节,这修改成不合法的,然后你反编译就失败了

可以用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
>>> import dis, marshal
>>> f = open("xxx.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 # 常量
>>> code.co_name # 当前对象名
>>> code.co_names # 当前对象中使用的对象名
>>> 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

可以用showfile.py让结果更为清晰

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
import dis, marshal, struct, sys, time, types  

def show_file(fname):
f = open(fname, "rb")
magic = f.read(4)
moddate = f.read(4)
modtime = time.asctime(time.localtime(struct.unpack('L', moddate)[0]))
print "magic %s" % (magic.encode('hex'))
print "moddate %s (%s)" % (moddate.encode('hex'), modtime)
code = marshal.load(f)
show_code(code)

def show_code(code, indent=''):
old_indent = indent
print "%s<code>" % indent
indent += ' '
print "%s<argcount> %d </argcount>" % (indent, code.co_argcount)
print "%s<nlocals> %d</nlocals>" % (indent, code.co_nlocals)
print "%s<stacksize> %d</stacksize>" % (indent, code.co_stacksize)
print "%s<flags> %04x</flags>" % (indent, code.co_flags)
show_hex("code", code.co_code, indent=indent)
print "%s<dis>" % indent
dis.disassemble(code)
print "%s</dis>" % indent

print "%s<names> %r</names>" % (indent, code.co_names)
print "%s<varnames> %r</varnames>" % (indent, code.co_varnames)
print "%s<freevars> %r</freevars>" % (indent, code.co_freevars)
print "%s<cellvars> %r</cellvars>" % (indent, code.co_cellvars)
print "%s<filename> %r</filename>" % (indent, code.co_filename)
print "%s<name> %r</name>" % (indent, code.co_name)
print "%s<firstlineno> %d</firstlineno>" % (indent, code.co_firstlineno)

print "%s<consts>" % indent
for const in code.co_consts:
if type(const) == types.CodeType:
show_code(const, indent+' ')
else:
print " %s%r" % (indent, const)
print "%s</consts>" % indent

show_hex("lnotab", code.co_lnotab, indent=indent)
print "%s</code>" % old_indent

def show_hex(label, h, indent):
h = h.encode('hex')
if len(h) < 60:
print "%s<%s> %s</%s>" % (indent, label, h,label)
else:
print "%s<%s>" % (indent, label)
for i in range(0, len(h), 60):
print "%s %s" % (indent, h[i:i+60])
print "%s</%s>" % (indent, label)

show_file(sys.argv[1])

1
showfile.py test.pyc >test.xml

很多情况会遇到损坏的pyc,于是就需要手动来逆,尝试逆了下一个文件

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
03f3 0d0a	python版本
6634 6f58 .pyc对应的源文件的修改时间
63 ='c',1字节,PyCodeObject的类型标识
00 00 00 00 4字节,位置参数的个数
00 00 00 00 4字节,局部变量(包括位置参数)的个数
02 0000 00 4字节,求值栈的最大深度
40 0000 00 4字节,用来表示参数中是否有*args或者 **kwargs
73 ='s',1字节,PyStringObject的类型标识
2200 0000 4字节,字符串内容的长度
9900 00
99 0100
8600 00
91 0000
9902 00 opcode
88 0000
9101 00
99 0300
8800 00
91 0200
9901 00
53

28 ="(" TYPE_TUPLE
04 元组个数
0000 00
69 ffff ffff 69="i"(TYPE_INT) -1
4e ="N" TYPE_NONE
63 ='c',1字节,PyCodeObject的类型标识
01 00 0000 4字节,位置参数的个数
0600 0000 4字节,局部变量(包括位置参数)的个数
0300 0000 4字节,求值栈的最大深度
4300 0000 4字节,用来表示参数中是否有*args或者 **kwargs
73 ='s',1字节,PyStringObject的类型标识
5c 000000 4字节,字符串内容的长度

99 0100 6801 0099 0200 6802 0099 0300
6803 0061 0100 9904 0046 9905 0027 6102
0061 0100 2761 0300 2799 0600 4627 9905 opcode
0027 6102 0099 0600 4627 9907 0027 6804
009b 0000 6001 0061 0400 8301 0068 0500
6105 0060 0200 6100 0083 0100 53

28 ="(" TYPE_TUPLE
08 元组个数
00 0000
4e ="N" TYPE_NONE
73 ='s',1字节,PyStringObject的类型标识
0800 0000 4字节,字符串内容的长度
2140 2324 255e 262a "!@#$%^&*"
74 ="t" TYPE_INTERNED
08 0000 00
61 6263 6465 6667 68 "abcdefgh"

73 ='s',1字节,PyStringObject的类型标识
0600 0000 4字节,字符串内容的长度

防逆向

obscutor.py

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
import ast
from ast import Assign, Name, Call, Store, Load, Str, Num, List, Add, BinOp
from ast import Subscript, Slice, Attribute, GeneratorExp, comprehension
from ast import Compare, Mult
import codegen
import random
import sys


def random_string(minlength, maxlength):
return ''.join(chr(random.randint(0x61, 0x7a))
for x in xrange(random.randint(minlength, maxlength)))


def import_node(name, newname):
"""Import module obfuscation"""
# import sys -> sys = __import__('sys', globals(), locals(), [], -1)
return Assign(
targets=[Name(id=newname, ctx=Store())],
value=Call(func=Name(id='__import__', ctx=Load()),
args=[Str(s=name),
Call(func=Name(id='globals', ctx=Load()), args=[],
keywords=[], starargs=None, kwargs=None),
Call(func=Name(id='locals', ctx=Load()), args=[],
keywords=[], starargs=None, kwargs=None),
List(elts=[], ctx=Load()), Num(n=-1)],
keywords=[], starargs=None, kwargs=None))


def obfuscate_string(s):
"""Various String Obfuscation routines."""
randstr = random_string(3, 10)

table0 = [
# '' -> ''
lambda: Str(s=''),
]

table1 = [
# 'a' -> 'a'
lambda x: Str(s=chr(x)),
# 'a' -> chr(0x61)
lambda x: Call(func=Name(id='chr', ctx=Load()), args=[Num(n=x)],
keywords=[], starargs=None, kwargs=None),
]

table = [
# 'abc' -> 'abc'
lambda x: Str(s=x),
# 'abc' -> 'a' + 'bc'
lambda x: BinOp(left=Str(s=x[:len(x)/2]),
op=Add(),
right=Str(s=x[len(x)/2:])),
# 'abc' -> 'cba'[::-1]
lambda x: Subscript(value=Str(s=x[::-1]),
slice=Slice(lower=None, upper=None,
step=Num(n=-1)),
ctx=Load()),
# 'abc' -> ''.join(_x for _x in reversed('cba'))
lambda x: Call(
func=Attribute(value=Str(s=''), attr='join', ctx=Load()), args=[
GeneratorExp(elt=Name(id=randstr, ctx=Load()), generators=[
comprehension(target=Name(id=randstr, ctx=Store()),
iter=Call(func=Name(id='reversed',
ctx=Load()),
args=[Str(s=x[::-1])],
keywords=[], starargs=None,
kwargs=None),
ifs=[])])],
keywords=[], starargs=None, kwargs=None),
]

if not len(s):
return random.choice(table0)()

if len(s) == 1:
return random.choice(table1)(ord(s))

return random.choice(table)(s)


class Obfuscator(ast.NodeTransformer):
def __init__(self):
ast.NodeTransformer.__init__(self)

# imported modules
self.imports = {}

# global values (can be renamed)
self.globs = {}

# local values
self.locs = {}

# inside a function
self.indef = False

def obfuscate_global(self, name):
newname = random_string(3, 10)
self.globs[name] = newname
return newname

def obfuscate_local(self, name):
newname = random_string(3, 10)
self.locs[name] = newname
return newname

def visit_Import(self, node):
newname = self.obfuscate_global(node.names[0].name)
self.imports[node.names[0].name] = newname

def visit_If(self, node):
if isinstance(node.test, Compare) and \
isinstance(node.test.left, Name) and \
node.test.left.id == '__name__':
for x, y in self.imports.items():
node.body.insert(0, import_node(x, y))
node.test = self.visit(node.test)
node.body = [self.visit(x) for x in node.body]
node.orelse = [self.visit(x) for x in node.orelse]
return node

def visit_Str(self, node):
return obfuscate_string(node.s)

def visit_Num(self, node):
d = random.randint(1, 256)
return BinOp(left=BinOp(left=Num(node.n / d), op=Mult(),
right=Num(n=d)),
op=Add(), right=Num(node.n % d))

def visit_Attribute(self, node):
if isinstance(node.value, Name) and isinstance(node.value.ctx, Load):
node.value = self.visit(node.value)
return Call(func=Name(id='getattr', ctx=Load()), args=[
Name(id=node.value.id, ctx=Load()), Str(s=node.attr)],
keywords=[], starargs=None, kwargs=None)
node.value = self.visit(node.value)
return node

def visit_FunctionDef(self, node):
self.indef = True
self.locs = {}
node.name = self.obfuscate_global(node.name)
node.body = [self.visit(x) for x in node.body]
self.indef = False
return node

def visit_Name(self, node):
# obfuscate known globals
if not self.indef and isinstance(node.ctx, Store) and \
node.id in ('teamname', 'flag'):
node.id = self.obfuscate_global(node.id)
#elif self.indef:
#if isinstance(node.ctx, Store):
#node.id = self.obfuscate_local(node.id)
#node.id = self.locs.get(node.id, node.id)
node.id = self.globs.get(node.id, node.id)
return node

def visit_Module(self, node):
node.body = [y for y in (self.visit(x) for x in node.body) if y]
node.body = [y for y in (self.visit(x) for x in node.body) if y]
return node


class GlobalsEnforcer(ast.NodeTransformer):
def __init__(self, globs):
ast.NodeTransformer.__init__(self)
self.globs = {}

def visit_Name(self, node):
node.id = self.globs.get(node.id, node.id)
return node

if __name__ == '__main__':
if len(sys.argv) != 2:
print 'Usage: python %s <pyfile>' % sys.argv[0]
exit(0)

if sys.argv[1] == '-':
root = ast.parse(sys.stdin.read())
else:
root = ast.parse(open(sys.argv[1], 'rb').read())

# obfuscate the AST
obf = Obfuscator()
root = obf.visit(root)

# resolve all global names
root = GlobalsEnforcer(obf.globs).visit(root)

print codegen.to_source(root)

一个简单的py文件,写了递归相乘

1
2
3
4
5
6
7
8
9
10
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

if __name__ == '__main__':
fact()

混淆后

1
2
3
4
5
6
7
8
9
def broztlk(n):
return prux(n, 0 * 132 + 0 * 0 * 251 + 77 + 0 * 103 + 1)

def rci(num, product):
if (num == 0 * 171 + 0 * 0 * 163 + 55 + 0 * 36 + 1):
return product
return rci(num - 0 * 57 + 0 * 1 * 113 + 81 + 0 * 166 + 1, num * product)
if (__name__ == '__ma' + 'in__'):
broztlk()

如果再做一些变量替换就更看不出来它在干嘛了吧

  • 用py2exe/pyinstaller啥的打包一下
    逆向只是时间问题
  • 借助cython
    cython可以将python文件转换成c, 并编译成pyd文件. 一般将核心模块编译成pyd, 这样被破解的风险就大大降低了. 关于如何使用cython可以参考官网或者这篇文章 或者 这篇
  • 使用自己的Bytecode指令集
    也就是说自己编一个python虚拟机,代价较大

  • 修改字节码
    修改一些字节码使得代码逻辑无误,但是破坏了原来的pyc的结构,此时使用dis、marshal因为逆向的逻辑而无法解析,具体方法参考大佬http://blog.csdn.net/ir0nf1st/article/details/61650984

参考链接:
http://phantom0301.cc/2017/03/24/pythonopcode/
http://0x48.pw/2017/03/20/0x2f/#
http://blog.csdn.net/ir0nf1st/article/details/61650984
https://stackoverflow.com/questions/261638/how-do-i-protect-python-code

文章目录
  1. 1. 遇到问题
  2. 2. pyc
  3. 3. celery
  4. 4. 排除pyc干扰
  5. 5. pyc逆向
  6. 6. 上手逆
  7. 7. 防逆向