昨天汇报完论文,这两天要准备考试,抓住休息的空档,解决一下之前遗留的题目。
题目介绍

这道题是n1book上的题目,之前刷buuctf的时候尝试了一下,发现easy_python并不easy 😫
之前对python字节码这块完全不了解,只会使用一些网站和工具尝试反编译
看了官方的wp,才知道原来python bytecode也可以混淆
初步尝试
题目提供的是一个pyc文件,首先尝试使用marshal和dis获取bytecode
1 2 3 4 5 6 7 8 9
| import dis,marshal ifile=open('easy_python.pyc','rb') header=ifile.read(8)
code=marshal.load(ifile)
dis.dis(code)
|
题目中提示了python环境,这里必须使用python2.7来运行
第三行的read header的操作是必要的,否则marshal会出错 (咱也不清楚为啥,模仿其他人的操作
运行结果:

emmm,在这里卡了很久,然后看了一下官方提供的wp https://pan.baidu.com/s/16p8ADuKJZI4xb_HWOS-OwA
(密码:rt7o)大致意思是需要对pyc 字节码解混淆之后才能够dis
原理分析
这里我先了解了一下字节码解混淆的相关知识 https://www.anquanke.com/post/id/185481
python字节码在处理过程大致如下:
首先所有的指令可以分为两类,不需要参数和需要参数的,Python字节码在设计的时候故意把没有参数的指令分配在了对应编号的低位,高位都是有参数的,以Include/opcode.h中的HAVE_ARGUMENT分界。他们的在二进制级别上的组织是这样的:
[指令] 不需要参数的指令只占用一个字节
[指令] [参数低字节] [参数高字节] 需要参数的指令占用三个字节,一个字节指令,两个字节参数
例子如下:
1 2 3 4 5 6 7 8 9 10 11
| >>> def add(a,b): ... return a+b ... >>> add.__code__.co_code '|x00x00|x01x00x17S' >>> import dis >>> dis.dis(add.__code__.co_code) 0 LOAD_FAST 0 (0) 3 LOAD_FAST 1 (1) 6 BINARY_ADD 7 RETURN_VALUE
|
|x00x00 表示成16进制: 7c 00 00
7c对应的是指令的索引,当索引大于dis.HAVE_ARGUMENT的时候,指令是有参数的
1 2 3
| >>> dis.opname[ord('|')] 'LOAD_FAST' >>>
|
而0x7c对应的指令是LOAD_FAST,参数=0*256+0
所以|x00x00解释成指令 LOAD_FAST 0 (0)
但是如果遇到junk code的时候,可能会由于解析的时候数组越界,导致dis失败,比如下面的代码:
1 2 3 4 5 6 7 8 9 10
| >>> list(map(ord,code.co_code[:9])) [113, 158, 2, 136, 104, 110, 126, 58, 140] >>> dis.opname[113] 'JUMP_ABSOLUTE' >>> 2*256+158 670 >>> dis.opname[136] 'LOAD_DEREF' >>> 110*256+104 28264
|
应当被解析成:
JUMP_ABSOLUTE 670
LOAD_DEREF 28264
显然第二条指令越界了,但是由于jump跳转,第二条指令实际上是不会被执行到的,也就是所谓的dead code()
pyc文件作者是故意加入不影响执行的非法指令触发分析软件崩溃,阻碍对该pyc文件的分析。
工具使用
在github上面找了很久有关python bytecode反混淆的,最后发现了
使用python2.7配置相应环境,然后运行
1 2
| pip install networkx==1.11 pydotplus py2 main.py -i easy_python.pyc -o de_easypy.pyc
|

将输出的de_easypy.pyc 拖到pyc反编译网站https://tool.lu/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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import sys import os import hashlib import time import base64
class rc4: def __init__(self, public_key = None): if not public_key: pass self.public_key = 'none_public_key' key = hashlib.md5(self.public_key).hexdigest() self.keya = hashlib.md5(key[0:16]).hexdigest() self.keyb = hashlib.md5(key[16:32]).hexdigest()
def encode(self, string): string = hashlib.md5(string + self.keyb).hexdigest()[0:16] + string self.result = '' self.docrypt(string) return base64.b64encode(self.result)
def decode(self, string): string = base64.b64decode(string) self.result = '' self.docrypt(string) result = self.result if result[:16] == hashlib.md5(result[16:] + self.keyb).hexdigest()[0:16]: return result[16:]
def docrypt(self, string): string_lenth = len(string) result = '' box = list(range(256)) randkey = [] cryptkey = self.keya + hashlib.md5(self.keya).hexdigest() key_lenth = len(cryptkey) for i in xrange(255): randkey.append(ord(cryptkey[i % key_lenth])) for i in xrange(255): j = 0 j = (j + box[i] + randkey[i]) % 256 tmp = box[i] box[i] = box[j] box[j] = tmp for i in xrange(string_lenth): a = j = 0 a = (a + 1) % 256 j = (j + box[a]) % 256 tmp = box[a] box[a] = box[j] box[j] = tmp self.result += chr(ord(string[i]) ^ box[(box[a] + box[j]) % 256])
if __name__ == '__main__': rc = rc4('zBhzAVLG6XTu2w0H') string = raw_input('Input your flag:') rlt = rc.encode(string) print rlt if rlt == 'oxurpmahzeM2kHKblmTkWlLpb2i5jXKrBN/uf3+Xn5n0lYKMJA==': print 'True!' else: print 'False!'
|
encode和decode操作是对应的,理论上将比较字符串的内容作为输入,然后调用decode函数就能获取flag,但是可能在处理过程中出现了一些差错,直接decode的得到的结果是不对的。

最终办法
这里需要换个思路,直接修改py源码是没办法得到正确结果的,如果能修改原始pyc文件,将encode替换成decode,然后输入oxurpmahzeM2kHKblmTkWlLpb2i5jXKrBN/uf3+Xn5n0lYKMJA== 就可以得到flag
去混淆之后使用dis再次解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 55 79 NOP 80 NOP 81 NOP 82 NOP 83 NOP >> 84 LOAD_NAME 8 (raw_input) 87 LOAD_CONST 6 ('Input your flag:') 90 CALL_FUNCTION 1 93 STORE_NAME 9 (string) 96 LOAD_NAME 7 (rc) 99 LOAD_ATTR 10 (encode) 102 LOAD_NAME 9 (string) 105 CALL_FUNCTION 1 108 STORE_NAME 11 (rlt) 111 JUMP_FORWARD 79 (to 193) 114 NOP
|
encode附近的字节码
使用dis命令可以获取LOAD_ATTR对应的索引
1 2 3
| >>> hex(dis.opname.index('LOAD_ATTR')) '0x6a' >>>
|
于是 LOAD_ATTR 10 (encode)对应的 十六进制为 6a 0a 00

10表示的是encode早co_name中的索引,可以通过code.co_names查看所有的co_name

修改encode最简单的办法就是直接在pyc文件中搜索encode并替换成decode(注意不要替换b64encode)

然后将修改的pyc脚本运行起来

成功获得flag: n1book{JUnk_c0D3_p7c}