前言
这是一篇闲蛋疼的文章, 讲述了闲蛋疼的我是如何历经千辛万苦终于在 Python 中让 'a' is 'b'
得到 True
.
(虽然这件事情毫无意义)
(不过拿来坑下一任或许是个好主意呢)
动机
这个想法最早在六月中旬就产生了, 主要是看了知乎上的两个回答,
然后我想, 那能不能来个 'God' is 'girl' -> True
之类的.
当时也尝试了一下, xjb读了一下源码, 尝试使用 ‘a’ 的内存覆盖 ‘b’ 的内存, 然而最终只做到了 hash('a') == hash('b')
.
而且读完 is
的源码后, 觉得这不大可能实现, 于是就放弃了. (因为发现 is
比较的是两个对象的指针, 而不是内容, 所以两个不同的对象进行比较是无论如何都不可能 True 的, 即使 hash 什么的都一模一样)
四个月后我又回想起了这个问题, 我觉得我应该有能力再次挑战了, 然后果然挑战成功了!
于是写下这篇文章, 记录一下自己在这个过程中学到的东西.
先试试常规手段
1. 直接交换
False = True
很简单粗暴的想法, 然而 python 的回应也很简单粗暴: 关键字不能赋值.
1
2
3
4
5
|
In [1]: False = True
File "<ipython-input-1-e3c38088f793>", line 1
False = True
^
SyntaxError: can't assign to keyword
|
在 Python2 中, 这行代码其实是可以执行的, 不过这只是改变了字面量 False
的值, 并没有达到我的要求.
1
2
3
4
5
6
7
|
In [1]: False = True
In [2]: 1 == 2
Out[2]: False
In [3]: False
Out[2]: True
|
2. globals()
既然不能直接赋值, 那试试用 globals()
来赋值?
1
2
3
4
5
6
7
8
9
|
In [1]: globals()['a'] = 1
In [2]: a
Out[2]: 1
In [3]: globals()['False'] = True
In [4]: False
Out[4]: False
|
仍然失败, 想想也是, 在 Python3 中 False
已经是一个关键字了, 优先级肯定高于变量. 即使有同名变量也会被遮蔽.
3. 重载 __eq__
想对身为关键字的 True
和 False
做点什么实在是太难了,
换个思路试试: 重载 str.__eq__
, 让它不管三七二十一全部返回 True
! 妙啊妙啊, 我真是太聪明了.
虽然不能做到 'a' is 'b'
但能实现 'a' == 'b'
的话也是一个进步啊.
1
2
3
4
5
6
7
|
In [10]: int.__eq__ = lambda : True
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-71a46054a87d> in <module>()
----> 1 int.__eq__ = lambda : True
TypeError: can't set attributes of built-in/extension type 'int'
|
然而又失败了…
4. 重载 __repr__
不死心的我又想试试 __repr__
, 让 bool
对象的 __repr__
始终返回 True
仍然失败了, 嘛…毕竟是内置类型.
看来内置的玩意儿通过常规方法是改不了, 那就读源码, 用 ctypes 强行改一波内存.
后来我还想到一种适用于 ipython 的方法, 即自定义一个 ipython formatter, 相当于 ipython 专用的 __repr__
比如我曾经实现过的一个小插件, 可以让一个 bytes 不管是否包含可见字符, 都以十六进制的形式输出
https://gist.github.com/Aloxaf/de3de8e7c0b8913335847afd3ff76cc7
然而还是没有什么卵用…bool类型跟开挂一样无视了 ipython formatter
这个很奇怪, 不过原因以后再找
还是老老实实读源码吧
0. 搭建调试环境
这几个月熟悉了一下棒棒的 gdb, 这次要利用起来
1
2
3
4
5
6
|
# gitee 的唯一用处
wget https://gitee.com/Aloxaf/cpython/repository/archive/v3.7.1.zip
unzip v3.7.1.zip
cd cpython
./configure --with-pydebug
make -j4
|
非常迅速地编译完了, 接下来运行 gdb ./python
.
不出意外的话会提示你给 python-gdb.py
添加一个啥 add-auto-load-safe-path
到 ~/.gdbinit
, 照它说的添加就行. 可以提升调试体验.
(如果你的 gdb 什么插件都没装的话, 建议安装一个 pwndbg, 能让你从此爱上 gdb)
然后运行 echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
允许 attach 进程
接着创建一个 Python 文件, 写入如下内容
1
2
3
4
5
|
from time import sleep
while True:
print(1 == 1, 1 == 2)
sleep(1)
|
然后 ./python test.py&
执行, 记录 pid 并用 gdb ./python $PID
attach 到 Python 进程上.
~~阅读源码我没找到啥好软件(有棒棒的"跳转到定义"的那种), ~~
我用的 clion, 然而跳转太残废了, 也有可能是我不会配吧 (一般拿 clion 写的是 rust 233
所以我采用了 ripgrep
来搜索定义…然后在 clion 里手动定位….
(这个效率太感人了, 如果谁有什么好的方式还望不啬赐教.)
Google了一下这个问题, Viewing CPython Code in CLion
只需要创建一个小小的 CMakeLists.txt, 就能使用自动跳转了!!
1
2
3
4
5
6
7
8
9
10
11
12
|
make_minimum_required(VERSION 3.0)
project(cpython)
file(GLOB SOURCE_FILES
Python/*.c
Parser/*.c
Objects/*.c
Modules/*.c)
include_directories(Include)
add_executable(cpython ${SOURCE_FILES})
|
1. PyObject *
因为有了先前的经验, 这里直接 b Python/ceval.c:2585
就可以断在 COMPARE_OP
的地方.
(先前是不断单步走到这儿的…)
1
2
3
4
5
6
7
8
9
10
11
12
|
────────────────────────────[ SOURCE (CODE) ]─────────────────────────────
2580 TARGET(COMPARE_OP) {
2581 PyObject *right = POP();
2582 PyObject *left = TOP();
2583 PyObject *res = cmp_outcome(oparg, left, right);
2584 Py_DECREF(left);
► 2585 Py_DECREF(right);
2586 SET_TOP(res);
2587 if (res == NULL)
2588 goto error;
2589 PREDICT(POP_JUMP_IF_FALSE);
2590 PREDICT(POP_JUMP_IF_TRUE);
|
通过 py-list
可以查看当前执行到 .py 文件的哪一行.
p res
输出了 $1 = False
, res 明明是 PyObject *
类型, 却直接输出了内容, 这就是开始那个 python-gdb.py
的作用.
p *res
解引用看一下这个结构体, 发现看不懂
1
2
3
4
5
6
|
$2 = {
_ob_next = 0x7fd80d9afba0,
_ob_prev = 0x7fd80d9afc08,
ob_refcnt = 138,
ob_type = 0x6b2d00 <PyBool_Type>
}
|
rg '} PyObject'
搜索一下定义, 在 Include/object.h:110
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
/* Nothing is actually declared to be a PyObject, but every pointer to
* a Python object can be cast to a PyObject*. This is inheritance built
* by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*.
*/
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
|
大概意思就是不管什么对象大家都可以是 PyObject *
, Python 内部也以这个指针来传递各种对象, 显得统一. 要取这个对象内容的时候再根据 ob_type
特事特办
第一次调试到这里的时候我没认真看注释,
天真地以为通过 memmove(id(True), id(False), sizeof(PyObject))
就能用 True 的内容覆盖掉 False, 事实上 sizeof(PyObject)
根本根本不是 True Object 的真实大小.
那么, 把 sizeof(PyObject)
改大一点行不行? 其实还是不行, 原因在下面…
2. Py_False
和 _Py_FalseStruct
那么问题来了, False
这玩意儿真实的(C)类型是什么?
在 Python/ceval:4686
行可以找到 cmp_outcome
的定义, 结尾是这么写的.
1
2
3
4
|
v = res ? Py_True : Py_False;
Py_INCREF(v);
return v;
}
|
也就是说 (上一个)res 的值是 Py_True
或 Py_False
, 而它俩的定义在 Include/boolobject.h:21
1
2
3
4
5
6
7
8
9
|
/* Py_False and Py_True are the only two bools in existence.
Don't forget to apply Py_INCREF() when returning either!!! */
/* Don't use these directly */
PyAPI_DATA(struct _longobject) _Py_FalseStruct, _Py_TrueStruct;
/* Use these macros */
#define Py_False ((PyObject *) &_Py_FalseStruct)
#define Py_True ((PyObject *) &_Py_TrueStruct)
|
可以发现, Py_False
这玩意儿只是个宏, 代表的是 _Py_FalseStruct
的地址.
显然, 即使我们用 _Py_TrueStruct
的内容覆盖 _Py_FalseStruct
的内容, Py_True
和 Py_False
的值也不会受影响.
而且 is
操作符的实现非常简单粗暴, 也不大可能对 is
操作符动什么手脚
1
2
3
|
case PyCmp_IS:
res = (v == w);
break;
|
就这么结束了吗? 不, 上次在 python repl 环境中无法重载内置类型的方法, 那直接写内存有没有可能重载呢?
3. bool_repr
在 Python/boolobject.c:12
可以看到 bool_repr
的实现,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static PyObject *false_str = NULL;
static PyObject *true_str = NULL;
static PyObject *
bool_repr(PyObject *self)
{
PyObject *s;
if (self == Py_True)
s = true_str ? true_str :
(true_str = PyUnicode_InternFromString("True"));
else
s = false_str ? false_str :
(false_str = PyUnicode_InternFromString("False"));
Py_XINCREF(s);
return s;
}
|
第一反应: 能不能改掉这个函数, 比如把这个 ==
改成 !=
啥的? –不可能的 .text 段的东西哪儿能说改就改.
那有没有可能 hook 这个函数呢? read -s ./python
看一下, 真的有 bool_repr
这个函数, 可以 hook 的样子! –想多了这是 Debug 版才保留了这些符号…
有没有其他办法? 注意到这个函数中为了提高速度缓存了 true_str
和 false_str
, 那直接 false_str=true_str
不就行了吗? 在 gdb 中执行 p false_str=true_str
然后继续执行, 可以在命令行里看到后面的输出都变成了 True True
!
这个方法真是绝妙啊, 唯一的问题就是在 Python 中没有办法获得 false_str
和 true_str
的地址, 修改也就无从谈起了…
4. PyUnicode_Type
不要慌, 还没走到绝路.
false_str
的真实类型是什么? 这玩意儿一定有某个地方存着 “False” 这个字符串吧, 而且这个对象是动态生成的, 我肯定有权限修改.
p *false_str
看一下, 发现是 PyUnicode_Type
1
2
3
4
5
6
|
$41 = {
_ob_next = 0x6b2ba0 <_Py_FalseStruct>,
_ob_prev = 0x6b2b60 <_Py_TrueStruct>,
ob_refcnt = 6,
ob_type = 0x6cf020 <PyUnicode_Type>
}
|
在 Objects/unicodeobject.c:12538
可以找到 unicode_repr
函数, 部分内容如下
1
2
3
4
5
6
7
8
|
repr = PyUnicode_New(osize, max);
if (repr == NULL)
return NULL;
okind = PyUnicode_KIND(repr);
odata = PyUnicode_DATA(repr);
PyUnicode_WRITE(okind, odata, 0, quote);
PyUnicode_WRITE(okind, odata, osize-1, quote);
|
repr 为返回值, 可以看出 repr 由 PyUnicode_New
初始化, 通过 Py_Unicode_DATA
获取其中的 data 部分, 目测为字符串存放的位置
先到 Objects/unicodeobject.c:1233
看看 PyUnicode_New
函数
1
2
3
4
5
6
7
8
9
10
11
|
unicode = (PyCompactUnicodeObject *)obj;
if (is_ascii)
data = ((PyASCIIObject*)obj) + 1;
else
data = unicode + 1;
...
...
if (is_ascii) {
((char*)data)[size] = 0;
_PyUnicode_WSTR(unicode) = NULL;
}
|
! 将 obj 转为 PyASCIIObject*
再 +1 再转为 char*
, 似乎就是字符串存放的真正地址了?
1
2
|
pwndbg> p (char *)((PyASCIIObject *)false_str + 1)
$47 = 0x7fd80d9afbe0 "True" //注意因为先前已经 false_str=true_str 了所以这里是 True
|
wow, 没错! 这真是相当 excited.
又因为sizeof(PyASCIIObject) == 64
, 那么 false_str + 64
也就是 id('False') + 64
就是字符串的位置了.
写成 Python 代码如下
1
2
3
4
5
|
import ctypes
print(True, False) # 确保已经生成了对应的字符串
false_str_addr = id('False') + 64 # Debug 版
for i, c in zip(range(5), b'True\x00'):
ctypes.c_char.from_address(false_str_addr + i).value = c # 直接 strcpy 出错了 不造为啥
|
不过有一点比较迷, 就是系统自带的 Python, 偏移量并不是64, 而是 48. 难道是gcc的迷之优化?
最终代码: 互换 True False
注意到 PyASCIIObject
有个 length
指定了字符串长度, 修改的时候最好一起改掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
pwndbg> p *(PyASCIIObject *)true_str
$1 = {
ob_base = {
_ob_next = 0x6b2ba0 <_Py_FalseStruct>,
_ob_prev = 0x6b2b60 <_Py_TrueStruct>,
ob_refcnt = 6,
ob_type = 0x6cf020 <PyUnicode_Type>
},
length = 4,
hash = -8576775955766428395,
state = {
interned = 1,
kind = 1,
compact = 1,
ascii = 1,
ready = 1
},
wstr = 0x0
}
|
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import ctypes
print(True, False) # 确保已经生成了对应的字符串
false_addr = id('False')
true_addr = id('True')
read_char = lambda n: ctypes.c_char.from_address(n)
tmp = b''.join([read_char(false_addr + i).value for i in range(100)])
str_offset = tmp.index(b'False') # 定位字符串所在位置
length_offset = tmp.index(b'\x05') # 定位长度所在位置(这个有点不放心...)
# 修改字符串
for i, c in zip(range(5), b'True\x00'):
read_char(false_addr + str_offset + i).value = c
for i, c in zip(range(6), b'False\x00'):
read_char(true_addr + str_offset + i).value = c
# 修改长度
read_char(false_addr + length_offset).value = b'\x04'
read_char(true_addr + length_offset).value = b'\x05'
|
效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
In [2]: print True, False
------> print(True, False)
False True
In [3]: 1 == 2
Out[3]: True
In [4]: 1 == 1
Out[4]: False
In [5]: 'God' is 'girl'
Out[5]: True
In [6]: 'cat' is 'dog'
Out[6]: True
|
正确用法:
1
2
3
4
5
6
7
8
|
In [2]: 'Emacs' is 'the best editor'
Out[2]: False
In [3]: 我给你一次重新组织语言的机会
Out[3]: ...
In [4]: 'Emacs' is 'the best editor'
Out[4]: True
|