2016-12 HXBCTF一道python打包的逆向分析
本帖最后由 6767 于 2017-7-15 01:11 编辑这个是2016年12月HXBCTF的线上赛的逆向题目
本题为2016年下半年湖湘杯一道逆向。 从本思路处理,需要一定的python虚拟机知识,和python打包逆向的知识。 本题的所有命令都在git-shell下执行,兼容类unix。
预处理
<login.exe>文件可以从这里下载 https://dn.jarvisoj.com/challengefiles/login.exe.0e043cc84e9273f1e34b6b27330c8e5a。
看到有同学说看不懂,我就讲的更细致一点。
首先拿到文件一定要分析,是什么东西写的,一般直接丢ida看看关键函数名 ,或者看字符串有什么特性,
这个文件里用strings login.exe|less 看有这么些东西:
...more
bapi-ms-win-crt-utility-l1-1-0.dll
bmain.exe.manifest
bpyexpat.cp35-win32.pyd
bpython35.dll
bselect.cp35-win32.pyd
bunicodedata.cp35-win32.pyd
opyi-windows-manifest-filename main.exe.manifest
xbase_library.zip
zout00-PYZ.pyz
#python35.dll
由于以前用过python做二进制分发,这个马上就可以认出来是py2exe一类的东西,至于为什么是只能说多尝试,多积累经验,python目前还是比较小众的。
使用pyinstaller extractor https://sourceforge.net/projects/pyinstallerextractor/ 提取器,会有很多文件,我们关注这个文件:main <.pyc>。
https://github.com/6769/CTFTime/raw/master/XHBCTF/RE/Login/photo1.png
这个主文件是一个pyObject转储的字符串文件,pyObj相关的资料大家可以看python的文档,也可以看看源代码剖析。 将其转换为正常的pycfile(添加一个pyc文件头,4字节MAGIC_NUMBER,4bytes Timestamp,具体做法是),用uncompyle6 将其反向到源代码:
# Embedded file name: main.py
# Compiled at: 2016-08-16 19:36:25
# Size of source mod 2**32: 89 bytes
import hxbctf
print(hxbctf.Login(0, input('UserName: ')))
# okay decompiling tryrepair.main.pyc
本身的Bug
点击运行这个exe,会提示无法运行,放进虚拟机里也是一个德行,当时做题的时候真是x了狗了。
https://github.com/6769/CTFTime/raw/master/XHBCTF/RE/Login/photo2.png
在网上搜索了一阵子,发现这个使用Onefile参数压缩的pyinstaller bootloader有一个奇怪的bug ,主要是由vc运行库的压缩引发的。
当vcruntime被upx压缩时在64位Windows下无法运行,这可能会导致一些错误,比如一直弹无法找到dll。
知道错误就好办了,参考pyinstaller打包文件的格式是n个文件直接二进制append的类型,所以只要修改索引表Table of Contents就好了。
https://camo.githubusercontent.com/15b13d276e5a890a829151c9a10639e39dfbbcfb/68747470733a2f2f707974686f6e686f737465642e6f72672f5079496e7374616c6c65722f5f696d616765732f5a6c6962417263686976652e706e67
现在我们使用010editor来更改login.exe文件中的字符串, 执行strings login.exe |grep dll字符串搜索,找到字符串bVCRUNTIME140.dll的位置,
并将其重命名为bccruntime140.dll或者任何其他字符串以绕过此错误。
记得在系统的搜索路径下放置一个 VCRUNTIME140.dll文件。
仔细分析
首先file *所有文件,看看有什么,很明显的一条是upx-compressed.
https://github.com/6769/CTFTime/raw/master/XHBCTF/RE/Login/photo3.png
用ls *.dll |xargs upx -d解压缩这些文件。对pyd 文件也执行一样的操作。
如果我们在所有二进制文件被upx解压缩之后使用strings hxbctf进行搜索,会发现“python.dll”报告了一个字符串引用hxbctf。这里的技巧是充分利用shell提供的工具快速在目录里搜索,否则一个文件一个文件找就累死人了。
这里已经很清楚了,下一步逆核心dll。
Crack
一般逆向就几步走,找关键词,定位代码,分析逻辑。
如果使用 python 前端shell python.exe来运行核心DLL,会发现hxbctf已经被编译成这个python core文件中的内置模块。
拉进IDA,Shift+F12看字符串。 幸运的是,字符串“Congratulations”是.rodata段中的一个纯文本。 按下X反查使用点,很快就能找到他的使用位置,只有一处:
在dll,段位置.text:1E183D20,按F5 直接看C代码,这是主要功能:
//已修改注释和部分变量名
int __cdecl coreCTFfunction(int a1, void *pyobj)
{
int result; // eax@1
int i; // edx@5
int v4; // eax@6
int v5; // ecx@6
char *userName; // @2
int paraNumber1; // @1
char Dst; // @8
char inputStrDstcache; // @1
memset(inputStrDstcache, 0, 0x200u);
result = PyArg_ParseTuple(pyobj, "is", (unsigned int)¶Number1);// http://wiki.jikexueyuan.com/proj ... s/python_c_api.html
if ( result )
{
sub_1E04E550((int)"Password: ", (char)userName);
getInputScanf("%s", inputStrDstcache);
if ( paraNumber1 == 0x1352 && strlen(inputStrDstcache) == 16 && !strncmp("HXB_Admin", userName, 9u) )
{
i = 0;
while ( 1 )
{
v4 = inputStrDstcache;
v5 = i ^ v4 ^ secrectKey;
if ( i != (v4 ^ secrectKey) )
break;
if ( ++i >= 16 )
{
memset(&Dst, v5, 0x200u);
printfFormator(&Dst, "Congratulations!\nflag{%s}.", inputStrDstcache);
return Py_BuildValue("s", (unsigned int)&Dst);
}
}
}
result = Py_BuildValue("s", (unsigned int)"Access Denied!");
}
return result;
}
找出secKey中的字符串:.rdata:1E253040 secrectKey
这里是算法:Xor与每个输入的char和他的索引;
sec='Pxvk4kYcIVlJSeO?'
res=[]
for i in range(len(sec)):
n1=ord(sec)
res.append(chr(i^n1))
print(''.join(res))
## -- End pasted text --
#got >>>Pyth0n_dA_fA_hA0
在一个python shell里执行:
import hxbctf
>>> hxbctf.Login(0x1352,'HXB_Admin')
Password: Pyth0n_dA_fA_hA0
'Congratulations!\nflag{Pyth0n_dA_fA_hA0}.'
>>>
我们已经得到了我们想要的了
总结
题目核心其实非常简单,但是在64bit下的bug平添巨大的麻烦。
就虚拟机本身而言,题目隐藏得还是不错的,如果动态解密字符串的话。
相似的题目还有2017年5月出的更加暴力的西安的SSCTF的bpython。
参考
强行动态调试解出,这个很暴力:http://bbs.pediy.com/thread-215002.htm
python 源代码剖析这本书
关于pyc文件头的分析,以下对应于py3.5 的,py2和py3.3还不一样的。
//In Python 3.5.2 source code tree, file:
//Line:981
static PyObject *
run_pyc_file(FILE *fp, const char *filename, PyObject *globals,
PyObject *locals, PyCompilerFlags *flags)
{
PyCodeObject *co;
PyObject *v;
long magic;
long PyImport_GetMagicNumber(void); //00000000: 160d 0d0a b9fa b257 5900 0000 e300 0000
magic = PyMarshal_ReadLongFromFile(fp); // 160d 0d0a
if (magic != PyImport_GetMagicNumber()) {
if (!PyErr_Occurred())
PyErr_SetString(PyExc_RuntimeError,
"Bad magic number in .pyc file");
return NULL;
}
/* Skip mtime and size */
(void) PyMarshal_ReadLongFromFile(fp); // b9fa b257;time stamp,1471347385,'Tue Aug 16 19:36:25 2016'
(void) PyMarshal_ReadLongFromFile(fp); // 5900 0000 ;source code <.py> size,here is 59 bytes
//these bytes are all droppedwhen reading in.
if (PyErr_Occurred())
return NULL;
v = PyMarshal_ReadLastObjectFromFile(fp);// e300 0000......to the end of pyc file.
//pyObject structure
if (v == NULL || !PyCode_Check(v)) {
Py_XDECREF(v);
PyErr_SetString(PyExc_RuntimeError,
"Bad code object in .pyc file");
return NULL;
}
co = (PyCodeObject *)v;
v = PyEval_EvalCode((PyObject*)co, globals, locals);
if (v && flags)
flags->cf_flags |= (co->co_flags & PyCF_MASK);
Py_DECREF(co);
return v;
}
canic 发表于 2017-7-14 23:13
将其转换为正常的pycfile(添加一个pyc文件头,4字节MAGIC_NUMBER,4bytes Timestamp),用uncompyle6 将其 ...
对py3.5 ,直接在main文件前面加这160d 0d0a b9fa b257 5900 000012bytes 的16进制就好了。main的内容和头关系不是特别大,除了前4个magic_number,其他随便改。具体可以看我的更新的内容。 大神,这句话:这个主文件是一个pyObject转储的字符串文件,pyObj相关的资料大家可以看python的文档,也可以看看源代码剖析。 将其转换为正常的pycfile(添加一个pyc文件头,4字节MAGIC_NUMBER,4bytes Timestamp,具体做法是),用uncompyle6 将其反向到源代码:
怎么找python36的MAGIC_NUMBER,你只写了个具体做法然后就没有了,谢谢赐教!!! 感谢分享, 学习了了 思路清奇{:1_921:},666 感觉很NB的样子,只是我看不懂! 将其转换为正常的pycfile(添加一个pyc文件头,4字节MAGIC_NUMBER,4bytes Timestamp),用uncompyle6 将其反向到源代码
——这个添加pyc头的步骤,能否详细说一下,菜鸟表示做不出来。 学习了,谢谢
感谢分享, 学习了了 你好,请教一下,那个main文件是如何转成py文件的???谢谢! Quincy379 发表于 2018-2-6 21:22
你好,请教一下,那个main文件是如何转成py文件的???谢谢!
看到了,谢谢啦!
页:
[1]
2