人生苦短,你需要用Python来做CTF

0x00 前言

前一阵子,为了活跃一下群里的气氛,出了一道小题涉及到一些编码,反编译,解密的知识的题目让大家玩,如果你比较感兴趣的话也可以先试着做一做。

0x01 获取key

下载得到题目包you_need_python.zip,解压得到flag.py和key_is_here_but_do_you_know_rfc4042和两个文件,尝试运行flag.py:

1.png

要求输入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文件的内容:

2.png

文件内容是一些乱码,但是文件名提示了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
#coding utf-8
#utf9_to_utf8.py
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()

3.png

得到一堆符号串,但是经常仔细观察,除了“”符号外,其他符号都是Python中的算数运算符,“(”,“)”括号表示优先级,然后开脑洞“”为数字“1”,“”为数字“2”,依次类推“___”为数字“9”,在熟悉了utf9模块的使用后尝试编写转换代码,代码执行后得到数字:5287002131074331513,尝试转换为16进制然后转换为ASCII字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#coding=utf-8
#key_show.py
import binascii
_ = 1
__ = 2
___ = 3
____ = 4
_____ = 5
______ = 6
_______ = 7
________ = 8
_________ = 9
print binascii.a2b_hex(hex(eval("_____*((__//__+___+______-____%____)**((___%(___-_))+________+(___%___+_____+_______%__+______-(______//(_____%___)))))+__*(((________/__)+___%__+_______-(________//____))**(_*(_____+_____)+_______+_________%___))+________*(((_________//__+________%__)+(_______-_))**((___+_______)+_________-(______//__)))+_______*((___+_________-(______//___-_______%__%_))**(_____+_____+_____))+__*(__+_________-(___//___-_________%_____%__))**(_________-____+_______)+(___+_______)**(________%___%__+_____+______)+(_____-__)*((____//____-_____%____%_)+_________)**(_____-(_______//_______+_________%___)+______)+(_____+(_________%_______)*__+_)**_________+_______*(((_________%_______)*__+_______-(________//________))**_______)+(________/__)*(((____-_+_______)*(______+____))**___)+___*((__+_________-_)**_____)+___*(((___+_______-______/___+__-_________%_____%__)*(___-_+________/__+_________%_____))**__)+(_//_)*(((________%___%__+_____+_____)%______)+_______-_)**___+_____*((______/(_____%___))+_______)*((_________%_______)*__+_____+_)+___//___+_________+_________/___"))[2:][:-1])

运行得到key。

0x02 分析flag.py

4.png

查询有关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)

5.png

得知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
#!/usr/bin/env python
#coding = utf-8
#PyCodeObject_to_pyc.py
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
#!/usr/bin/env python
# encoding: utf-8
#source.py
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 编写解密代码

#coding:utf-8
#decrypt.py

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 后话

我才不会告诉你集各路赛棍大牛的群号在博客上。