社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

嵌入Python解释器的程序逆向

看雪学苑 • 7 月前 • 136 次点击  




前言


最近在玩一款单机游戏,想试着改一下里面各类数据,却发现生命值、金币等数据都没办法修改。往里面一看,发现这游戏居然在完全单机的情况下,都会往一个本地的 Python 服务器发包。重要的逻辑都在这个服务器里面处理了。

这个 Python 服务器是以 DLL 的形式作为 Unity 插件引入的。DLL 导出了服务器初始化、发包、收包的函数,由 GameAssembly.dll 那边直接调用。

查了一下网上的信息,据说这款游戏之前逻辑是放客户端的,后来就一直在往这个 Python 服务器里挪,还把客户端的各种调试方法都删了。这可能也是为什么客户端里有一个类的方法通过 il2cpp dumper 出来后偏移都一样的原因。这个类包含了很多看名字就觉得很有用的调试方法,例如应用伤害等;可惜都是空的,ida里看这个地址就是 retn 0;






为什么无法搜索内存改值?


细心的小伙伴可能会问了,虽然这是服务器,但它也在程序的进程内,内存空间是在一起的,怎么会没有办法修改呢?

简单的说,就是 Python 的值打一枪换一个地方,所以 CE 这种搜固定内存的变化的方法是很难直接找到对应的值进行修改的。你可以自己启动 Python,输入 a=1234, 用 CE 搜索,再输入 a = a+1,再用 CE 搜索,是搜不到任何对应内存的。

下面我用一段官方的示例代码(https://docs.python.org/3/c-api/intro.html#coding-standards),来说明一下。这段 Python 代码和 C 代码是等价的,用于将 dict[key] 自增 1.

def incr_item(dict, key):
try:
item = dict[key]
except KeyError:
item = 0
dict[key] = item + 1
int
incr_item(PyObject *dict, PyObject *key)
{
/* Objects all initialized to NULL for Py_XDECREF */
PyObject *item = NULL, *const_one = NULL, *incremented_item = NULL;
int rv = -1; /* Return value initialized to -1 (failure) */

item = PyObject_GetItem(dict, key);
if (item == NULL) {
/* Handle KeyError only: */
if (!PyErr_ExceptionMatches(PyExc_KeyError))
goto error;

/* Clear the error and use zero: */
PyErr_Clear();
item = PyLong_FromLong(0L);
if (item == NULL)
goto error;
}
const_one = PyLong_FromLong(1L);
if (const_one == NULL)
goto error;

incremented_item = PyNumber_Add(item, const_one);
if (incremented_item == NULL)
goto error;

if (PyObject_SetItem(dict, key, incremented_item) < 0)
goto error;
rv = 0; /* Success */
/* Continue with cleanup code */

error:
/* Cleanup code, shared by success and failure path */

/* Use Py_XDECREF() to ignore NULL references */
Py_XDECREF(item);
Py_XDECREF(const_one);
Py_XDECREF(incremented_item);

return rv; /* -1 for error, 0 for success */
}

可以看到 Python 底层对象都是 PyObject。存储很喜欢 dict 这种形式。每个类的函数、成员都是用 dict 存的。Hook 对 dict 的操作就能够监视到很多信息。

关注其中
incremented_item = PyNumber_Add(item, const_one);在获取到item后,并不会直接对item内部的内存进行自增,而是调用函数进行加法,创建了一个新的对象incremented_item,然后再PyObject_SetItem(dict, key, incremented_item)换回去。所以两个值在内存上并不会是同一个地址。

另外,注意其中
Py_XDECREF(item);这是减少一次引用计数的意思。python 底层实现里 PyObject 都是有引用计数的。这也意味着我们如果直接修改内存中的值,会同时修改所有使用这个对象的地方。

所以,最好的方法还是在 Python 解释器以及前面的字节码这部分把问题解决掉,而不是在内存里解决。






通过 hook 解释器底层函数来修改值


那就来逆这个 Python 服务器吧。ida 搜索到大量 Python 相关的字符串,鉴定为 Cpython-36。



弄一份 Cpython-36 的源码,同时安装一下可执行文件。源码链接(https://www.python.org/downloads/release/python-368/)

先试着用下载下来的带符号 python36.dll 的特征码搜索,发现搜不到。这可能是因为 build 版本不同,还可能是因为这个嵌入的 python 解释器是从源码编译的。

没法直接搜特征,所以只能对照着源码的字符串,去定位一些关键函数。这个步骤就和做数独一样。

个人感觉hook后能够获得大量信息的函数有
PyUnicode_FromString, _PyObject_GenericGetAttrWithDict, PyObject_SetItem, PyObject_Call。还有一些有用的辅助函数有PyObject_Repr, PyUnicode_AsUTF8, PyGILState_Ensure, PyGILState_Release
找到这些函数的地址后,写一个 dll 来进行 hook。可以 include 一下
Python36-x64\include\Python.h,虽然不能直接用自己这个 Python 解释器里的函数,但是头文件里很多宏是对对象直接操作的,还是比较有用的。

hook 之后如何获取运行信息呢?PyUnicode_FromString 的参数就是 const char * ,直接打印就好。但是其他几个参数都是 PyObject,所以我们需要把 PyObject转为可读的字符串,以便进行进一步的分析。

利用嵌入的 Python 解释器自己的 PyObject_Repr 和 PyUnicode_AsUTF8 就可以获得可读信息。下面是我用的代码。

函数地址是嵌入的 Python 解释器对应函数的地址。有些对象可以直接用自己下载的那个解释器的函数,应该是字符串对象这种不需要执行具体指令的对象。但是其他对象很容易崩。

string GetPyObjectString(void* obj)
{
//auto gstate = oPyGILState_Ensure();

//auto pyobj = reinterpret_cast(obj);
auto str_obj = oPyObject_Repr(obj);
if (!str_obj) {
PyErr_Print();
//oPyGILState_Release(gstate);
return {};
}
const char* str = oPyUnicode_AsUTF8(str_obj);
if (!str) {
PyErr_Print();
Py_DECREF(str_obj);
//oPyGILState_Release(gstate);
return {};
}
string ret{ str };
Py_DECREF(str_obj);
//oPyGILState_Release(gstate);
return ret;
}

尽管这样,对于一些对象还是会在 oPyObject_Repr 里似乎是死锁崩溃。对 Python 对象操作前据说要先拿全局锁,但我试着用 oPyGILState_Ensure 获取 Python C API 全局锁,并没有解决崩溃的问题。

因此,有些对象不方便用这种方法获取可读信息,就可以只获取其类型信息,用
string obj_class = Py_TYPE(obj)->tp_name;。同时,尽量减少查看的类型,过滤不感兴趣的调用可以有效减少崩溃。

下面是对 _PyObject_GenericGetAttrWithDict 的 Hook 示例。

void* My_PyObject_GenericGetAttrWithDict(void* obj, void* name, void* dict) {
static unordered_set intersted_name{
R"#('m_HP')#",
};
ostringstream oss;
auto name_str = GetPyObjectString(name);
if (intersted_name.find(name_str) == intersted_name.end()) {
oss << "Skip: "< LogMsg(oss.str());
return o_PyObject_GenericGetAttrWithDict(obj, name, dict);;
}
string obj_class = Py_TYPE(obj)->tp_name;
oss << "_PyObject_GenericGetAttrWithDict: " << endl
<< '\t' << obj < << '\t' << name << ":" << name_str << endl
<< '\t' << dict << endl;
// 避免被回收,提前解析字符串
auto ret = o_PyObject_GenericGetAttrWithDict(obj, name, dict);
auto dict_str = GetPyObjectString(ret);
auto t = Py_TYPE(ret);
oss << '\t' << " -> " << ret << " Type: " << t->tp_name << " Value: " << dict_str;
if (obj_class == "Player") {
if (name_str == R"('m_HP')") {
((PyLongObject*)ret)->ob_digit[0] = 78900; // 血量
oss << " hijked -> " << ret << Py_TYPE(ret)->tp_name << " : " << GetPyObjectString(ret);
}
}

LogMsg(oss.str());
return ret;
}

这个函数里,检测到获取 Player 类的 m_HP 变量时,就将变量存储的值修改为想要的值。示例中是通过打印信息提前知道了返回值在这种情况下是 int,所以用((PyLongObject*)ret)->ob_digit[0] = 10000;设置。

这样直接改存储值其实不好,一方面所有用同一个对象的地方都会被修改,一方面存储0这样的常量的地方是改不了。更好的做法是像 Python 那样新建对象、设置对象、减少引用。但无所谓,现在
玩家的 m_HP 已经在 Python 解释器层面无法被减少了。






改值之外?


只是改值满足不了我,我还想进行更多修改,怎么办?

PyObject_Call 能够监听很多东西。看日志发现程序导入 zlib,从一个特殊格式的文件里读入信息,并进行解压。解压后立即进行了模块导入。看解压的内容,'\33\r\r\n'刚好是 Python36 字节码的 MaigcNumber。可以推断这是实际运行的业务代码。类名、函数名都是能够从字节码里还原出来的。



然而,尝试了python-uncompyle6(https://github.com/rocky/python-uncompyle6),以及 python3.6里直接 dis.dis(data[16:]) 后,都失败了。

根据日志的 PyObject_Call 看到嵌入的解释器能够直接导入该字节码的。

一方面可以试试纯用 Python C API 接口,例如
PyObject_Dir在嵌入的解释器里查,但是这样逆向写起来实在算不上方便;
一方面说不定能够用PyRun_InteractiveLoop调出交互命令行来,这样应该更加方便。

还有纯逆向直接调用 PyObject_Call,但是想想都觉得很麻烦。

如果接下来想要获得更大的自由度,应该考虑看一看 Python 字节码层面。直接摆弄解释器还是太底层了。




结论


本文介绍了一种 Hook Python 解释器底层函数进行逆向分析的方法,在解释器层面实现了获取特定值的劫持。这对于嵌入 Python 或者魔改了解释器的 Python 程序分析有一定的帮助。但个人感觉有点弄复杂了,希望各位大佬指教。




看雪ID:mb_fefksfsl

https://bbs.kanxue.com/user-home-979936.htm

*本文为看雪论坛优秀文章,由 mb_fefksfsl 原创,转载请注明来自看雪社区



# 往期推荐

1、记由长城杯初赛Time_Machine掌握父子进程并出题

2、 从Clang到Pass加载与执行的流程

3、OLLVM混淆源码解读

4、VMProtect保护壳爆破步骤详解(入门级)

5、Attitude Adjustment -- Fast Quaternion Attitude



球分享

球点赞

球在看



点击阅读原文查看更多

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/171689
 
136 次点击