Python 逆向中的一些岩浆知识

前言

python 作为一门由 pvm 执行的脚本语言,有着许多奇怪的特性,而现在陆续出现了许多的 python 逆向题目,且由于 python 应用更加广泛,实战中也经常遇到 python 逆向问题。那么这一篇就简单写一下我遇到的感觉能在 python 逆向之中有抄近路的功效的神奇方法

从总思路说起

对于一个 python 逆向题,无论是使用什么 pyinstaller 打包也好,除了使用 cython 直接编译为 pyd,最终很多都是要归为 pyc 逆向的(当然有些会直接在 py 源码里面)。然后对于我们来说能反编译回 python 代码的,一般会使用 pycdc 或者在线的那个反编译器,如果编译不回去的话我们会使用 marshal 来查看字节码,然后根据字节码来逆向。然而,很多情况下我们使用 marshal 加载之后就会遇上出题人给我们设置的障碍,出现一堆报错。而有的时候,我们拿到的脚本甚至会有类似于壳一样的解密操作。所以,当遇到这些先进的加密手段的时候,我们该怎么办呢?

CPython

对于 pvm 这种虚拟机,是有着许多的实现的。其中最广泛的开源实现应该是我们的 cpython。我们从官网直接下载的正是使用 cpython 虚拟机的版本。

针对种种不能反编译的情况的常用解决方案

我们知道,即使回不到 python 伪代,中间语言仍然是可以阅读的。也就是说,第一个任务其实是回到中间语言。对于 pvm 的指令可以去网上找。而我们使用 dis 模块进行反编译的时候常常会遇到一些错误

dis.disassemble(code)
  File "/usr/lib/python3.8/dis.py", line 64, in disassemble
    labels = findlabels(code)
  File "/usr/lib/python3.8/dis.py", line 166, in findlabels
    oparg = ord(code[i]) + ord(code[i+1])*256
IndexError: string index out of range

我相信,许多人都遇到过。而现在常用的解决方法是把这个 exception 给 patch 掉。但是,这是一种治标不治本的办法。我们可以看到有的用了这样的代码

0 JUMP_ABSOLUTE         2764
              3 LOAD_CONST           65535 (consts[65535])
              6 <218>                50673
              9 SET_ADD              18016

不难理解,其实就是做了个绝对跳转中间插入无效字节

而对于这种花,有一种方法,就是由于 JUMP ABSOLUTE 的 opcode 是 6e,然后只要把后面的 oparg 改成 0,然后再把后面的垃圾去掉就可以。

但是,只是这样却还不太够。如果遇到了更变态,更加看不出的怎么办?

这里介绍一种技巧,就是魔改我们的 cpython。查阅资料可以得知,经过一些预处理之后会到_PyEval_EvalFrameDefault 来执行字节码,里面会有一个 switch 来进行执行字节码。那么,我们动手的地方肯定也是这里

trace

先不提我们上面讲到的那个函数,先提一嘴 trace。其实 python 是内置了一个 trace 的,不过功能比较垃圾,只针对特定事件进行跟踪:

  • call: 函数调用
  • return: 函数返回
  • line: 一行新代码
  • exception: 异常事件

而且,这玩意相当于一个 debugger,是可以被检测到的,因此不是很满足我们的需求。

注意看,眼前的这个文章正在向你展示 cpython 的一个奇怪特性。就在我上面提到的 function 的不远处有这样一个东西

Python 逆向中的一些岩浆知识

也就是说,只要你编译的时候编译为 debug interpreter,那么你就嫩拥有一个带有 lltrace 功能的 python,而且这个是几乎无法被检测的!

至于怎么优化输出的 trace,改源码。。。不过还是比较麻烦的,只能说是提供了一个思路吧,我看是有人实现了,但是没开源就是

DUMP

另一种简单的去除保护思路,尤其是对于解密型的,就是 dump。那么,python 脚本怎么 dump 呢?

首先,dump 就是当我们在运行函数的时候,让他停下来,此时函数已解密,然后取出解密的函数的过程。

还记得我们前面提到的_PyEval_EvalFrameDefault 吗?有一个比较没有技术含量的办法就是直接在这里改就行,tstate->frame ->f_code,就这么轻轻松松拿到了我们的 code object,然后直接写出就好啦、

tstate->frame = frame;
co = frame->f_code;

if (strstr(PyUnicode_AsUTF8(co->co_filename), ".py")){
    FILE *file;
    file = fopen("./dumped.txt", "ab");
    PyMarshal_WriteObjectToFile(co, file, 2);
    fclose(file);
}

 

然而上面这种方法看起来好像并不是通用的,仅在某些版本 python 可以使用。其次,有一种比较高级的,不需要重新编译 cpython 的方法,就是 hook python 的 dll,项目在这里 PyInjector,就是经典 hook,不过强大的是他可以执行你的 python 脚本,用它内置的就可以 dump 了

自定义解释器

这一套 lua 玩的倒是挺 6 的,但是 python 里面好像见得不是很多,但是确实是存在的。其实核心已经讲过了,就是那个函数,找到之后再分析我们得 opcode 就可以了

正文完
 1
评论(没有评论)