首页 > 修改教程 > gg修改器改游戏有用吗_gg修改器只能改游戏吗
gg修改器改游戏有用吗_gg修改器只能改游戏吗
  • gg修改器最新版
  • 大小:4.80MB版本:v6.33
  • 语言:简体中文系统:Android
绿色无毒,安全可靠!部分设备误报拦截请通过!

gg修改器改游戏有用吗_gg修改器只能改游戏吗

作者:佚名 来源:网友分享 日期:2024-04-27 10:47:46

大家好,今天小编为大家分享关于gg修改器改游戏有用吗_gg修改器只能改游戏吗的内容,赶快来一起来看看吧。

Linux 内核热补丁可以修复正在运行的 linux 内核,是一种维持线上稳定性不可缺少的措施,现在比较常见的比如 kpatch 和 livepatch。内核热补丁可以修复内核中正在运行的函数,用已修复的函数替换掉内核中存在问题的函数从而达到修复目的。

函数替换的思想比较简单,就是在执行旧函数时绕开它的执行逻辑而跳转到新的函数中,有一种比较简单粗暴的方式,就是将原函数的第一条指令修改为“ jump 目标函数”指令,即直接跳转到新的函数以达到替换目的。

那么,问题来了,这么做靠谱吗?直接将原函数的第一条指令修改为 jump 指令,会破坏掉原函数和它的调用者之间的寄存器上下文关系,存在安全隐患!本文会针对该问题进行探索和验证。

安全性冲击:问题呈现

对于函数调用,假设存在这样两个函数 funA 和 funB,其中 funA 调用 funB 函数,这里称 funA 为 caller(调用者),funB 为 callee(被调用者),funA 和 funB 都使用了相同的寄存器 R,如下所示:

图1 funA 和 funB 都使用了寄存器 R,funA 再次使用 R 时已经被 funB 修改

因此,当 funA 再次使用到 R 的数据已经是错误的数据了。如果 funA 在调用 funB 前保存寄存器 R 中的数据,funB 返回后再将数据恢复到 R 中,或者 funB 先保存 R 中原有的数据,然后在返回前恢复,就可以解决这类问题。

唯一的调用约定

那寄存器该由 caller 还是 callee 来保存?这就需要遵循函数的调用约定(call convention),不同的 ABI 和不同的平台,函数的调用约定是不一样的,对于 Linux 来说,它遵循的是 System V ABI 的 call convention,x86_64 平台下函数调用约定有且只有一种,调用者 caller 和被调用者 callee 需要对相应的寄存器进行保存和恢复操作:

调用约定,gcc 它遵守了吗?

设问:当函数实现很简单,只用到了少量寄存器,那没使用到的还需要保存吗?

答案:it depends。根据编译选项决定。

众所周知,GCC 编译器有 -O0、-O1、-O2 和 -Ox 等编译优化选项,优化范围和深度随 x 增大而增大(-O0是不优化,其中隐含的意思是,它会严格遵循 ABI 中的调用约定,对所有使用的寄存器进行保存和恢复)。

Linux 内核选用的都是 -O2 优化。GCC 会选择性的不遵守调用约定,也就是设问里提到的,不需要保存没使用到的寄存器。

当【运行时替换】撞见【调用约定】

GCC 之所以可以做这个优化,是因为 GCC 高屋建瓴,了解程序的执行流。当它知道 callee,caller 的寄存器分配情况,就会大胆且安全地做各种优化。

但是,运行时替换破坏了这个假设,GCC 所掌握的 callee 信息,极有可能是错误的。那么这些优化可能会引发严重问题。这里以一个具体的实例进行详细说明,这是一个用户态的例子( x86_64 平台)

//test.c 文件
//编译命令:gcc test.c -o test -O2 (kernel 采用的是 O2 优化选项)
//执行过程:./test
//输入参数:4

#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <math.h>

#define noinline __attribute__ ((noinline)) //禁止内联

static noinline int c(int x)
{
return x * x * x;
}

static noinline int b(int x)
{
return x;
}

static noinline int newb(int x)
{
return c(x * 2) * x;
}

static noinline int a(int x)
{
int volatile tmp = b(x); // tmp = 8 ** 3 * 4
return x + tmp; // return 4(not 8) + tmp
}

int main(void)
{
int x;
scanf("%d", &x);

if (mprotect((void*)(((unsigned long)&b) & (~0xFFFF)), 15,
PROT_WRITE | PROT_EXEC | PROT_READ)) {
perror("mprotect");
return 1;
}

/* 利用 jump 指令将函数 b 替换为 newb 函数 */
((char*)b)[0] = 0xe9;
*(long*)((unsigned long)b + 1) = (unsigned long)&newb
- (unsigned long)&b - 5;

printf("%d", a(x));
return 0;
}

该例子说明,直接使用 jump 指令替换函数在 -O2 的编译优化下,会出现问题,安全性受到了质疑和冲击!!!

安全性冲击:分析问题

上述例子中,我们将函数 b 用 jump 指令替换为 newb 函数,在 -O2 的编译优化下出现了计算错误的结果,因此,我们需要对函数的调用执行过程进行仔细分析,挖掘问题所在。首先,我们先来查看一下该程序的反汇编(指令:objdump -d test),并重点关注 a、b 和 newb 函数:

图2 -O2 编译优化的反汇编结果

汇编解释:

main:

-> 将参数 4 存放到 edi 寄存器中

-> 调用 a 函数:

-> 调用 b 函数,直接跳转到 newb 函数:

-> 将 edi 寄存器中的值存放到 edx 寄存器

-> edi 寄存器与自身相加后结果放入 edi

-> 调用 c 函数:

-> 将 edi 寄存器中的值存到 eax 寄存器

-> edi 乘以 eax 后结果放入 eax

-> edi 乘以 eax 后结果放入 eax

-> 返回到 newb 函数

-> 将 edx 与 eax 相乘后结果放入 eax

-> 返回到 a 函数

-> 将 edi 与 eax 相加后结果放入 eax

-> 返回 main 函数

(注意:b 函数中没有对 edi 寄存器进行写操作,而且它的代码段被修改为 jump 指令跳转到 newb 函数)

数据出错的原因在于,在函数 newb 中,使用到了 a 函数中使用的 edi 寄存器,edi 寄存器中的值在 newb 函数中被修改为 8,当 newb 函数返回后,edi 的值仍然是 8,a 函数继续使用了该值,因此,计算过程变为:8^3 * 4 + 8 = 2056,而正确的计算结果应该是 8^3 * 4 + 4 = 2052。

接下来不进行编译优化(-O0),其输出结果是正确的 2052,反汇编如下所示:

图3 不进行编译优化的反汇编

从反汇编中可以看到,函数 a 在调用 b 函数前,将 edi 寄存器的值存在了栈上,调用之后,将栈上的数据再取出,最后进行相加。这就说明,-O2 优化选项将 edi 寄存器的保存和恢复操作优化掉了,而在调用约定中,edi 寄存器本就该属于 caller 进行保存/恢复的。至于为什么编译器会进行优化,我们此刻的猜想是:

a 函数本来调用的是 b 函数,而且编译器知道 b 函数中没有使用到 edi 寄存器,因此调用者 a 函数没有对该寄存器进行保存和恢复操作。但是编译器不知道的是,在程序运行时,b 函数的代码段被动态修改,利用 jump 指令替换为 newb 函数,而在 newb 函数中对 edi 寄存器进行了数据读写操作,于是出现了错误。

这是一个典型的没有保存 caller-save 寄存器导致数据出错的场景。而编译内核采用的也是 -O2 选项。如果将该场景应用到内核函数热替换是否会出现这类问题呢?于是,我们带着问题继续探索。

安全性冲击:探索问题

不再观察到 bug

我们构造了一个内核函数热替换的实例,将上面的用户态的例子移植到我们构造的场景中,通过内核模块修改原函数的代码段,用 jump 指令直接替换原来的 b 函数。然而加载模块后,结果是正确的 2052,经过反汇编我们发现,内核中 a 函数对 edi 寄存器进行了保存操作:

图4 内核中 a 函数的反汇编

内核和模块编译时采用的是 -O2 优化选项,而此处 a 函数并没有被优化,仍然保存了 edi 寄存器。

此时我们预测:对于内核函数的热替换来说,使用 jump 做函数替换是安全的。

神奇的 -pg 选项

我们猜想是否是内核编译时使用其它的编译选项导致问题不能复现。果不其然,经过探索我们发现内核编译使用的 -pg 选项导致问题不再复现。

通过翻阅 GCC 手册得知,-pg 选项是为了支持 GNU 的 gprop 性能分析工具所引入的,它能在函数中增加一条 call mount 指令,去做一些分析工作。

在内核中,如果开启了 CONFIG_FUNCTION_TRACER,则会使能 -pg 选项。

图5 开启 CONFIG_FUNCTION_TRACER 使能 -pg 选项

FUNCTION_TRACE 即我们常说的 ftrace 功能,ftrace 大大提升了内核的运行时调试能力。ftrace 功能除了 -pg 选项,还要求打开 -mfentry 选项,后者的作用是将函数对 mcount 的调用放到函数的第一条指令处,然后通scripts/recordmcount.pl 脚本将该条 call 指令修改为 nop 指令。但 -mfentry 与本文主题没有关联,不再细说。

为了验证这个结论,我们回到上一节的用户态例子,并且增加了 -pg 编译选项:“gcc test.c -o test -O2 -pg”,此时运行结果果然正确了。查看其反汇编:

图6 增加 -pg 选项后的汇编

可以看到,每个函数都有 call mcount 指令,而且 a 函数中将 edi 寄存器保存到 ebx 中,在 newb 函数中又保存 ebx 寄存器。为什么在增加了 call mount 指令后,会做寄存器的保存操作?我们猜想,会不会是因为,由于 call mount 操作相当于调用了一个未知的函数( mcount 没有定义在同一个文件中),因此,GCC 认为这样未知的操作可能会污染了寄存器的数据,所以它才进行了保存现场的操作。

于是我们去掉了 -pg 选项,手动增加了 call mount 的行为进行验证:在另一个源文件 mcount.c 中增加一个函数 void mcount() { asm(“nop
“); },在 test.c 文件中增加对 mcount 函数的声明,a 函数中增加对该函数的调用:

extern void mcount(); //声明 mcount 函数

static noinline int a(int x){
int volatile tmp = b(x); // tmp = 8 ** 3 * 4
mcount();
return x + tmp; // return 4(not 8) + tmp
}

以上就是关于gg修改器改游戏有用吗_gg修改器只能改游戏吗的全部内容,希望对大家有帮助。

相关文章

热门下载

大家还在搜