0x00 前言
前一阵子,为了活跃一下群里的气氛,出了一道小题涉及到一些编码,反编译,解密的知识的题目让大家玩,如果你比较感兴趣的话也可以先试着做一做。
0x01 获取key
下载得到题目包you_need_python.zip,解压得到flag.py和key_is_here_but_do_you_know_rfc4042和两个文件,尝试运行flag.py:
要求输入key,由另一个文件名key_is_here_but_do_you_know_rfc4042,可知key需要从key_is_here_but_do_you_know_rfc4042中获得,查看key_is_here_but_do_you_know_rfc4042文件的内容:
文件内容是一些乱码,但是文件名提示了rfc4042,于是网上查询相关资料,知道rfc4042中定义了utf9和utf18两种Unicode转换编码格式,在了解转换原理之后,根据题意推测是将uft9编码转化了utf8编码,通过资料查询得知python已有utf9模块(如果没有查询到也可以自己动手编写转换代码),使用pip安装utf9模块:
1 2
| pip search utf9 pip install utf9
|
编写代码将文件内容的uft9编码转化utf8编码:
1 2 3 4 5 6 7 8 9 10 11
| import utf9 utf9_file = open('key_is_here_but_do_you_know_rfc4042','rb') utf9_data = utf9_file.read() decoded_data = utf9.utf9decode(utf9_data) print decoded_data decoded_file = open('decoded','w') decoded_file.write(decoded_data) decoded_file.close()
|
得到一堆符号串,但是经常仔细观察,除了“”符号外,其他符号都是Python中的算数运算符,“(”,“)”括号表示优先级,然后开脑洞“”为数字“1”,“”为数字“2”,依次类推“___”为数字“9”,在熟悉了utf9模块的使用后尝试编写转换代码,代码执行后得到数字:5287002131074331513,尝试转换为16进制然后转换为ASCII字符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import binascii _ = 1 __ = 2 ___ = 3 ____ = 4 _____ = 5 ______ = 6 _______ = 7 ________ = 8 _________ = 9 print binascii.a2b_hex(hex(eval("_____*((__//__+___+______-____%____)**((___%(___-_))+________+(___%___+_____+_______%__+______-(______//(_____%___)))))+__*(((________/__)+___%__+_______-(________//____))**(_*(_____+_____)+_______+_________%___))+________*(((_________//__+________%__)+(_______-_))**((___+_______)+_________-(______//__)))+_______*((___+_________-(______//___-_______%__%_))**(_____+_____+_____))+__*(__+_________-(___//___-_________%_____%__))**(_________-____+_______)+(___+_______)**(________%___%__+_____+______)+(_____-__)*((____//____-_____%____%_)+_________)**(_____-(_______//_______+_________%___)+______)+(_____+(_________%_______)*__+_)**_________+_______*(((_________%_______)*__+_______-(________//________))**_______)+(________/__)*(((____-_+_______)*(______+____))**___)+___*((__+_________-_)**_____)+___*(((___+_______-______/___+__-_________%_____%__)*(___-_+________/__+_________%_____))**__)+(_//_)*(((________%___%__+_____+_____)%______)+_______-_)**___+_____*((______/(_____%___))+_______)*((_________%_______)*__+_____+_)+___//___+_________+_________/___"))[2:][:-1])
|
运行得到key。
0x02 分析flag.py
查询有关marshal, zlib, base64模块和exec函数的资料,反向推测源代码或字节码先是使用marshal模块序列化,之后使用zlib压缩,最后使用base64编码,而exec语句可以用来执行储存在字符串或者文件中的python语句或python字节码。所以尝试提取exec语句执行的内容:
1 2 3
| import marshal, zlib, base64 code = marshal.loads(zlib.decompress(base64.b64decode('eJxtVP9r21YQvyd/ieWm66Cd03QM1B8C3pggUuzYCSWstHSFQijyoJBhhGq9OXJl2ZFeqAMOK6Q/94f9Ofvn1s+d7Lgtk/3O997du/vc584a0eqpYP2GVfwDEeOrKCU6g2LRRyiK4oooFsVVUSqkqxTX6J1F+SfSNYrrdKPorC76luhbpOEGCZNFZw2KG3Rmk26QtuXi3xTb7ND6/aVu0g2RuvhEcZNut5lAGbTvAFbyH57TkYLKy8J6xpDvQxiiiaIlcdqJxVcHbXY6bXNlZgviPCrO0+StqfKd88gzNh/qRZyMdWHE29TZZvIkG7eZFRGGRcBmsXJaUoKCQ9fWKHwSqNeKFnsM5PnwJ7q2aKk4AFhcWtQCh+ChB5+Lu/RmyYUxmtOEYxas7i/2iuR7Ti14OEOSmU0RADd4+dQzbM1FJhukAUeQ+kZROuLyioagrau76kc1slY1NNaY/y3LAxDQBrAICJisV2hMdF2lxQcyFuMoqcX3+TCl6xotqzSpkqmxYVmjXVjAXiwBsEfBrd1VvTvLCj2EXRnhoryAKdpxcIgJcowUB68yAx/tlCAuPHqDuZo0CN3CUGHwkPhGMA7aXMfphjbmQLhLhJcHa0a+mpgB191c1U1lnHJQbgkHx+WGxeJbejnpkzSavo2jkxZ7i725npGAaTc8FXmUjbUETHUmkxXN5zqL5WiWxwE7Bc11yyYzNJpN02jerq+DzNNodfxOX8kE4FcmYKscDdYD1oPGGucXYNmgs1F+NTf3GOt3Mg7b+NTVruqoQyX1hOEUacKw+AGbP38ZOq9THRXaSbL5pXGQ8bho/Z/lrzQaHxdoCrlev+t6nZ7re57r+57rHXag93Deh37k+vuw9zorO/Qj/B50cAf2oyOsvut3D+ADWxdxfN/1Drqu39mHzvcRswv/Hvz7sHeg9w8Qzy99DzuFwxhPhs6zWTbOI3OZRiaZZcVj5wVwOklx7OwVxR47PR46r/SVM8ulBJic9zku/eqY/MqJxiDj+Gd55wS3f35pbLCzHoEwzKKpDkN5i+TR+1AYCWTo5IV0Z0P9H3phDDd6lMzPdS5bbo9eJGbTsW9nbDqLL1N9Iq+rRxDbll2x67a9Lf27hw5uK1s1rZr6DOPF+FI='))) print "type of code:%s" %type(code)
|
得知code类型为Python代码对象(PyCodeObject),为了便于题目讲解,以下简单阐述个人对Python解释器运行原理,pyc文件格式以及Python代码对象的理解。
当你使用命令python demo.py(这里的python其实是python解释器中的CPython)时 ,会启动 Python解释器,Python解释器首先会检查当前目录是否存在相应的demo.pyc或demo.pyo文件(pyo文件是经过Python解释器编译优化,然后将内存中的字节码对象序列化并加上pyo文件头信息的可储存的二进制文件),如果都存在优先检查运行demo.pyo,如果不存在相应的demo.pyc或demo.pyo那么Python解释器就会编译demo.py,因为Python解释器检查运行pyc文件和pyo文件两者过程类似,下面以pyc文件为例,如果存在相应的pyc文件Python解释器则验证demo.pyc文件头的前四个字节幻数(magic number)的值,以确保当前文件是pyc文件格式且当前Python解释器版本能支持,如果验证不通过则给出提示退出,如果验证通过继续检查demo.pyc文件头的后四个字节py源文件修改时间(mtime,modify time),如果demo.pyc文件头的mtime和现在demo.py源文件的修改时间一样,那么Python解释器就会把demo.pyc(不包括文件头的8个字节)加载到内存并将demo.pyc文件反序列化为代码对象交给Python虚拟机(Python虚拟机只是Python解释器中实现的一部分,为了便于理解人们把这部分抽象命名为Python虚拟机)处理执行,运行完毕Python解释器退出。如果demo.pyc文件头的mtime早于现在py源文件的修改时间,说明之前的demo.py源文件经过了修改需要重新编译。
Python解释器所做的工作就不深入了,可以简单理解为Python解释器将demo.py加载到内存编译一个对应的字节码对象,然后交由Python虚拟机程序处理执行,Python 虚拟机会从编译得到的代码对象中依次读入每一条字节码指令,并在当前的上下文环境中执行这条字节码指令。而执行完毕之后Python编译器会根据情况生成相应的pyc文件,生成过程为Python解释器将内存的Python代码对象反序列化并加上相应的py文件头信息一起写入到硬盘,所以pyc文件只是Python代码对象在硬盘上的表现形式,生成pyo文件过程也类似,只是多了Python解释器优化的过程。
0x03 提取得到pyc文件
所以我们要做的便是编写代码将code这个Python代码对象加上相应pyc文件头信息提取出来写入磁盘生成pyc文件,生成pyc文件目的是便于我们反编译pyc文件得到相应的py源码。当然你也可以通过Python自带的dis模块慢慢分析pyc文件中的字节码指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import py_compile, imp, os, marshal, zlib, base64 code = marshal.loads(zlib.decompress(base64.b64decode('eJxtVP9r21YQvyd/ieWm66Cd03QM1B8C3pggUuzYCSWstHSFQijyoJBhhGq9OXJl2ZFeqAMOK6Q/94f9Ofvn1s+d7Lgtk/3O997du/vc584a0eqpYP2GVfwDEeOrKCU6g2LRRyiK4oooFsVVUSqkqxTX6J1F+SfSNYrrdKPorC76luhbpOEGCZNFZw2KG3Rmk26QtuXi3xTb7ND6/aVu0g2RuvhEcZNut5lAGbTvAFbyH57TkYLKy8J6xpDvQxiiiaIlcdqJxVcHbXY6bXNlZgviPCrO0+StqfKd88gzNh/qRZyMdWHE29TZZvIkG7eZFRGGRcBmsXJaUoKCQ9fWKHwSqNeKFnsM5PnwJ7q2aKk4AFhcWtQCh+ChB5+Lu/RmyYUxmtOEYxas7i/2iuR7Ti14OEOSmU0RADd4+dQzbM1FJhukAUeQ+kZROuLyioagrau76kc1slY1NNaY/y3LAxDQBrAICJisV2hMdF2lxQcyFuMoqcX3+TCl6xotqzSpkqmxYVmjXVjAXiwBsEfBrd1VvTvLCj2EXRnhoryAKdpxcIgJcowUB68yAx/tlCAuPHqDuZo0CN3CUGHwkPhGMA7aXMfphjbmQLhLhJcHa0a+mpgB191c1U1lnHJQbgkHx+WGxeJbejnpkzSavo2jkxZ7i725npGAaTc8FXmUjbUETHUmkxXN5zqL5WiWxwE7Bc11yyYzNJpN02jerq+DzNNodfxOX8kE4FcmYKscDdYD1oPGGucXYNmgs1F+NTf3GOt3Mg7b+NTVruqoQyX1hOEUacKw+AGbP38ZOq9THRXaSbL5pXGQ8bho/Z/lrzQaHxdoCrlev+t6nZ7re57r+57rHXag93Deh37k+vuw9zorO/Qj/B50cAf2oyOsvut3D+ADWxdxfN/1Drqu39mHzvcRswv/Hvz7sHeg9w8Qzy99DzuFwxhPhs6zWTbOI3OZRiaZZcVj5wVwOklx7OwVxR47PR46r/SVM8ulBJic9zku/eqY/MqJxiDj+Gd55wS3f35pbLCzHoEwzKKpDkN5i+TR+1AYCWTo5IV0Z0P9H3phDDd6lMzPdS5bbo9eJGbTsW9nbDqLL1N9Iq+rRxDbll2x67a9Lf27hw5uK1s1rZr6DOPF+FI='))) def PyCodeObject_to_pyc(py_code_obj, pyc_file): with open(pyc_file, 'wb') as pyc: pyc_magic = imp.get_magic() pyc.write(pyc_magic) mtime = long(os.fstat(pyc.fileno()).st_mtime) py_compile.wr_long(pyc, mtime) marshal.dump(py_code_obj, pyc) pyc.flush() pyc.close() def main(): PyCodeObject_to_pyc(code, 'extract.pyc') if __name__ == '__main__': main()
|
0x04 反编译pyc文件得到py源文件
将生成的pyc通过Python工具如uncompyle2等反编译得到py源码,这里直接通过在线的pyc反编译网站得到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
| import hashlib def sha1(string): return hashlib.sha1(string).hexdigest() def calc(strSHA1): r = 0 for i in strSHA1: r += int('0x%s' % i, 16) return r def encrypt(plain, key): keySHA1 = sha1(key) intSHA1 = calc(keySHA1) r = [] for i in range(len(plain)): r.append(ord(plain[i]) + int('0x%s' % keySHA1[i % 40], 16) - intSHA1) intSHA1 = calc(sha1(plain[:i + 1])[:20] + sha1(str(intSHA1))[:20]) return ''.join(map((lambda x: str(x)), r)) if __name__ == '__main__': key = raw_input('[*] Please input key:') plain = raw_input('[*] Please input flag:') encryptText = encrypt(plain, key) cipherText = '-185-147-211-221-164-217-188-169-205-174-211-225-191-234-148-199-198-253-175-157-222-135-240-229-201-154-178-187-244-183-212-222-164' if encryptText == cipherText: print '[>] Congratulations! Flag is: %s' % plain exit() else: print '[!] Key or flag is wrong, try again:)' exit()
|
0x05 分析py源文件中加密算法
sha1函数使用sha1算法计算返回40位16进制散列值,calc函数计算40位16进制散列值中每位的整型值并相加,最后返回整型的总和值。
encrypt函数为核心加密算法函数:
1 2 3 4 5 6 7 8
| def encrypt(plain, key): keySHA1 = sha1(key) intSHA1 = calc(keySHA1) r = [] for i in range(len(plain)): r.append(ord(plain[i]) + int('0x%s' % keySHA1[i % 40], 16) - intSHA1) intSHA1 = calc(sha1(plain[:i + 1])[:20] + sha1(str(intSHA1))[:20]) return ''.join(map((lambda x: str(x)), r))
|
由第4行的for可以知道到明文长度和密文长度相同,核心加密语句为第6,7行,算法使用ord函数取得明文每个字符的ASCII整型值,int函数内容为明文每个字符位置模40访问由调用sha1函数返回的40位16进制keySHA1字符串中的16进制数并转化为10进制数与由调用calc函数返回的整型值相减,然后将ord函数和int计算所得值作为密文添加到r列表,第7行更新intSHA1值,第9行转换为“-185-147-211…”格式并返回。
这里我们知道了密文cipherText,密钥key,加密算法encrypt,从而能逆推出解密算法,只要把密文值减去int函数中的值并对结果使用chr函数取得明文plain。
0x06 编写解密代码
import hashlib
def sha1(string):
return hashlib.sha1(string).hexdigest()
def calc(strSHA1):
r = 0
for i in strSHA1:
r += int("0x%s" % i, 16)
return r
def decrypt(strCipher,strKey):
listCipher = map(lambda x: int(x),strCipher.replace('-',' -')[1:].split(' '))
strKeySHA1 = sha1(strKey)
intSHA1 = calc(strKeySHA1)
strPlain = ''
for i in range(len(listCipher)):
strPlain += chr(listCipher[i] + intSHA1 - int("0x%s" % strKeySHA1[i%40],16))
intSHA1 = calc(sha1(strPlain[:(i + 1)])[:20] + sha1(str(intSHA1))[:20])
return strPlain
if __name__ == '__main__':
strCipher= '-185-147-211-221-164-217-188-169-205-174-211-225-191-234-148-199-198-253-175-157-222-135-240-229-201-154-178-187-244-183-212-222-164'
strKey = 'zijisuan'
strPlain = decrypt(strCipher, strKey)
print strPlain
最终获得flag。
0x07 后话
我才不会告诉你集各路赛棍大牛的群号在博客上。