SCTF-cython-wp
记录下打SCTF遇到的一个python题,cython编译的pyd文件如果直接用IDA分析的话会非常的复杂,可能python代码一行就会在IDA里多出几十行出来,一些参数检查和类型转换的伪代码中嵌入很多goto的奇奇怪怪的控制流,无厘头的跳转会让人头皮发麻,我尝试用IDA静态分析结合动态调试调一下午大概分析出了sub14514会调用这几个用户定义的函数sub_50804,sub50520,然后最终的加密始终没办法很好的还原,只是察觉到这是个类似TEA的加密,但是循环次数又不对,最后在这放弃了,看到站队里大佬WP使用python注入的方式还原加密处代码,感觉挺有意思的,在这里复现下解题过程
获取py源码
附件是python打包的exe,按照流程正常解包,用pyinstxtractor.py脚本解包(最好用高版本的,低版本的解包出的pyc还需要自己修补文件头),运行指令
.\pyinstxtractor2.py .\ez_cython.exe
在ez_cython.exe_extracted里找到ez_cython.pyc文件进行反编译
uncompyle6 .\ez_cython.pyc
拿到ez_cython.py源代码
1 | import cy |
发现有个报错信息mainParse error at or near `COME_FROM’ instruction at offset 108_0,dis提取字节码分析
1 | 1 0 LOAD_CONST 0 (0) |
得到python字节码,扔给GPT要一份py源码
1 | import cy |
可以看到这里导入了cy这个库,在解包文件夹里找到 cy.cp38-win_amd64.pyd 文件,放到py文件的同级目录下运行,运行报错发现GPT给的str_hex函数有问题,替换成uncompyle6给的定义,成功运行
分析程序
main函数大致运行逻辑是让用户输入一个字符,然后将字符加入到列表list中,然后在用户输入end之后对这个列表转换成十六进制,并在cy模块里的sub14514函数进行验证,不成功则提示是否重新输入。所以主要验证逻辑在cy模块中,先进入IDA里分析该pyd模块,找到字符串sub14514交叉引用定位到该函数执行入口,对应的是IDA里的sub_180009420函数
1 | __int64 __fastcall sub_180009420(__int64 a1, __int64 a2) |
在这里可以看出这个pyd的编写应该也是用python写的,而不是C,根据GPT描述,
虽然
.pyd
文件通常是用C或C++编写的,但你可以使用Python来生成.pyd
文件,通过使用工具和库如Cython或pybind11,它们允许你将Python代码编译成C扩展,从而生成.pyd
文件。
既然源码是用python写的,而且这个sub_14514接受的参数是python里的list类,那么就可以考虑自己定义一个类似list的类,然后重写list的成员函数,从而在加密过程的时候如果调用了这个成员函数,那么就会进入到我们自己定义的函数中,如果我们自己在定义的时候打印调试信息显示这一步在干什么,我们是不是就能通过调试信息获取整个加密流程呢?
怎么重写呢?在C++里我们可以先父类继承,子类定义同名函数(参数列表也必须相同),从而实现重写函数,那么在python中也可以通过类似的方法实现重写,比如说定义一个自己的AList类继承自list
1 | class AList(list): |
随后在定义一个列表里的数据类型,重写数据间的运算方法
1 | class Symbol: |
把列表类和数据类定义好之后,准备注入到sub14514函数。注入前我们还需要知道明文(str2hex之后的)长度,在IDA里动调分析一下,xdbg附加,下断,定位,发现程序验证的时候会首先调用某个get_key函数
直接动调或者源码调用这个函数可以看到该函数返回了一个list的类型,值为SyC10VeRf0RVer
,接着往下定位到对比函数
xxxxxxxxxx #include <stdio.h>#include <unistd.h>#include <sys/mman.h>// 一个全局变量,用来存放机器指令unsigned char code[8] = {0};// 一个函数指针,用来指向机器指令的地址typedef int (*func_ptr)();int main(){ // 获取当前系统的内存页大小 int pagesize = sysconf(_SC_PAGESIZE); printf(“pagesize = %d\n”, pagesize); // 计算全局变量code的地址所在的内存页的起始地址 unsigned long start = (unsigned long)code & ~(pagesize - 1); printf(“start = %p\n”, (void *)start); // 计算全局变量code的地址所在的内存页的结束地址 unsigned long end = ((unsigned long)code + sizeof(code) + pagesize - 1) & ~(pagesize - 1); printf(“end = %p\n”, (void *)end); // 计算全局变量code所占用的内存页的长度 size_t len = end - start; printf(“len = %ld\n”, len); // 给全局变量code所在的内存页可执行权限 int ret = mprotect((void *)start, len, PROT_READ | PROT_EXEC); if (ret == -1) { perror(“mprotect”); return -1; } // 在全局变量code中写入一段机器指令,其功能是将rax加1并返回 // 该指令的二进制码为:48 ff c0 c3 code[0] = 0x48; code[1] = 0xff; code[2] = 0xc0; code[3] = 0xc3; // 将函数指针指向全局变量code的地址 func_ptr f = (func_ptr)code; // 调用函数指针,执行机器指令 int result = f(); printf(“result = %d\n”, result); return 0;}c++
1 | 00000212D2E33 00000212D329B850 P¸)Ó.... |
有此信息我们就可以编写脚本注入函数查看32个值的加密流程,运行的脚本如下
1 | import cy |
有关列表的加密流程:
1 | s5[0] = ((a[0] + ((((a[31] >> 3) ^ (a[1] << 3)) + ((a[1] >> 4) ^ (a[31] << 2))) ^ ((a[1] ^ 2654435769) + (a[31] ^ 49)))) & 4294967295) |
可以看到就是一个类似TEA的加密,把算法逆过来就行,解密脚本
EXP
1 | var1 = [2654435769,1013904242,3668340011,2027808484,387276957] |
学习使用Cython编译pyd
安装
1 | pip install cython |
编写一个py脚本实现pyd里加密的函数
1 | #main2.py |
创建一个编译脚本setup.py
1 | from distutils.core import setup |
命令行运行编译指令
1 | python setup.py build_ext --inplace |
生成main2.cp311-win_amd64.pyd文件,再取一个python文件import即可调用里面的myPrint函数
1 | import main2 |
参考链接: