一、不直接访问变量本身而修改变量数据
【示例代码】
#include <stdio.h>
#include <windows.h>
void test(int x, int y) {
int* p = &x;
p++;
printf("before: %d\n", *p);
*p = 30;
printf("after: %d\n", y);
}
int main() {
int a = 10;
int b = 20;
test(a, b);
system("pause");
return 0;
}
【图解】
【解析】
如上图所示,当调用 test(a, b) 时,参数 x 和 y 会被压入栈中(顺序取决于调用约定,C/C++ 的默认约定 __cdecl 是从右往左压参的)。指针变量 p 首先指向 x 的地址,然后执行 p++ 向高地址移动4个字节指向到了 y 的地址,此时 *p 即为 y 的值20,接着执行 *p=30,间接修改 y 的值为30。
【运行结果】
二、非法调用第三方函数
【示例代码】
#include <stdio.h>
#include <windows.h>
void* res = NULL;
void bug() {
int x;
int* q = &x;
q += 2;
*q = res;
printf("bug()\n");
}
void test(int x, int y) {
int* p = &x;
p--;
res = *p;
*p = bug;
printf("test()\n");
}
int main() {
int a = 10;
int b = 20;
test(a, b);
__asm {
sub esp,4
}
printf("main()\n");
system("pause");
return 0;
}
【解析】
当函数调用返回时,是利用被调用之前所保存的返回地址而跳转的,如果我们改写这个地址,函数便可跳转到其他地方。
对于上述代码而言,函数的调用过程为:main函数 → test函数 → bug函数 → main函数,具体流程如下:
- 在main函数中
调用 test(a, b) 时,先将返回地址压栈,即 test(a, b) 的下一条指令地址,然后程序跳转到 test 函数执行
- 在test函数中
(1) 指针变量 p 指向 x 的地址
(2) p-- 让 p 指向存放函数返回地址的位置
(3) 保存返回地址到 res
(4) 将返回地址修改为 bug 函数的地址
(5) 打印 “test()”
(6) test 函数执行结束,程序读取栈上的返回地址,跳转到 bug 函数执行
- 在bug函数中
(1) 定义变量 x,定位 bug 函数
(2) 指针变量 q 指向 x 的地址
(3) q+=2 让 q 指向存放函数返回地址的位置
(4) 将该位置的数据修改为原始的返回地址
(5) 打印 "bug()"
(6) bug 函数执行结束,程序读取栈上的返回地址,跳转到 main 函数执行
- 在main函数中
(1) 执行 __asm { sub esp, 4} 内联汇编指令,将栈顶指针 esp 向低地址方向移动4个字节,目的是平衡栈帧。
(2) 当正常调用一个函数时,是通过 call 指令操作的,该指令会先将返回地址压栈,再进行函数跳转;函数执行完成后通过 ret 指令返回,该指令会先将返回地址弹出,再跳转到该地址。整个过程一入一出,栈帧平衡。
(3) 在上述代码中调用 bug 函数时,我们是通过修改 test 函数的返回地址进行非法跳转的,并没有执行 call 指令,也就没有数据压栈;而在 bug 函数返回时,又执行了 ret 指令,有数据弹出 。整个过程无入有出,栈帧不平衡,这将导致程序无法正确返回或访问数据,进而引发崩溃或未定义行为。
(4) 所以为了确保栈帧平衡,需要调整栈顶指针 esp 到正确的位置上。
【运行结果】
【注意】
上述代码行为都是未定义的,因为标准并未规定函数参数的栈布局。编译器可能以不同的方式实现(如从左往右压参、使用寄存器传参等),导致运行结果也会有所不同,甚至崩溃。