#include <stdio.h>
void a(); void b(); void c(); //函数原型
int main() { a(); printf("\n"); return 0; } //main
void a() { b(); printf("one "); }
void b() { c(); printf("two "); }
void c() {
int x;
// 在此处加代码
}
要求在不改变其他代码的情况下,只在//在此处加代码 加上一段代码(多长都行,但是不能用printf之类的)让整段代码输出one two 而不是two one。
小伙伴们有没有什么好办法呢?
想不出,没关系,我们慢慢来,这里给出了两种可行的做法
首先要关闭内联函数编译选项,一旦a,b,c被内联展开后我们除了修改源码别无它法了.
这里需要一些黑科技,我们知道在一个函数调用结束后会执行ret指令,改指令相当于取栈顶的地址并跳转
等价于
pop ecx
jmp ecx
那么我们要更改a(),b()的执行顺序那么我们只需要将a,b栈帧中的返回地址对调一下就好.
但是stack frame的安排会随着编译参数的不同而改变,若是想要swap call stack里的两个返回地址的话代码根本没有移植性.另外.text段是不可写的,直接修改代码什么的是不可能的了,Windows下可以通过VirtualProtectEx改,但是要求中明确表明不能用其他函数什么的...汗...
我们还是老老实实swap call stack里的两个返回地址吧, 不用汇编的方法也是有的,不过这个高度依赖编译参数
拿VS2013来说吧,编译选项:Release|Win32 优化:已禁用 (/Od)
#include <stdio.h>
void a(); void b(); void c(); //函数原型
int main() { a(); printf("\n"); return 0; } //main
void a() { b(); printf("one "); }
void b() { c(); printf("two "); }
void c() {
int x;
// 在此处加代码
int *rpa, *rpb;
rpa = &x+3; //指向c()的返回地址
rpb = rpa + 2; //指向b()的返回地址
x = *rpa;
*rpa = *rpb,*rpb = x; //Swap!!!
} //return WTF?
好的,让我们来看看发生了什么
观察反汇编
这是函数b的反汇编:
void b()
{
push ebp
mov ebp,esp
c();
call c (0AA1040h)
printf("two ");
push 0AA2108h
printf("two ");
call dword ptr ds:[0AA2090h]
add esp,4
}
pop ebp
ret
在b()调用c()之前先保存了ebp的值,然后再call c (0AA1040h) ,也就是说栈里面返回到a()和b()中间插了个ebp.现在来看函数c的反汇编:
void c()
{
push ebp
mov ebp,esp
sub esp,10h
mov eax,dword ptr ds:[00DC3000h]
xor eax,ebp
mov dword ptr [ebp-4],eax
int x;
int *rpa,*rpb;
rpa = &x+3;
lea eax,[ebp+4]
mov dword ptr [rpa],eax
rpb = rpa + 2;
mov ecx,dword ptr [rpa]
add ecx,8
mov dword ptr [rpb],ecx
x = *rpa;
mov edx,dword ptr [rpa]
mov eax,dword ptr [edx]
mov dword ptr [x],eax
*rpa = *rpb;
mov ecx,dword ptr [rpa]
mov edx,dword ptr [rpb]
mov eax,dword ptr [edx]
mov dword ptr [ecx],eax
*rpb = x;
mov ecx,dword ptr [rpb]
mov edx,dword ptr [x]
mov dword ptr [ecx],edx
}
mov ecx,dword ptr [ebp-4]
xor ecx,ebp
call __security_check_cookie (0DC10AAh)
mov esp,ebp
pop ebp
ret
观察上面的
rpa = &x+3;
lea eax,[ebp+4]
mov dword ptr [rpa],eax
栈帧示意图:
我们会发现x的地址实际上就是ebp-0x08,其中ebp-0x04是开启了编译选项安全检查:(/GS)之后用于检查ebp是否发生改变防止溢出的,和最后的__security_check_cookie有关.这里可以不管它.rpa = &x+3;就是指向ebp+4为返回b()的地址,根据前面的分析返回b()的地址和返回a()的地址中间相隔了4byte,那么rpb = rpa + 2;就是指向返回a()的地址.最后交换一下返回地址那么c()执行完了之后就会跳转到a()执行printf("one ");然后跳转到b()执行printf("two ");最后跳回到main退出.
特别指出一点:因为a()和b()的栈帧简单结构相同所以才可以直接swap返回地址,要不还要整个栈帧交换...多写个循环...
如果改了编译参数或平台的话地址要自己重新掰手指算咯,所以嘛根本没有移植性,这就是典型的黑科技.
另外调试的时候发现了编译器好奇葩的一点.明明c()函数是有副作用的,在开启了O1或O2下他喵的居然就直接给cut掉了...什么都没了...关闭了内联函数就只剩下个ret(这算是bug嘛?不过嘛,黑科技都用上了就不要对编译器强求太多啦)...情何以堪啊...还是写内联汇编安全...
还有另外一种呢,那就是把函数a,b给拷贝出来,放到c的栈帧里面去,栈的内存是可执行的,不过需要关闭链接时的数据执行保护(DEP)选项(/NXCOMPAT:NO)
为了防止出现递归现象,我们需要把a,b函数里面对其他函数的调用给去掉
在a,b中各有两次函数的调用,一次是调用b和c,第二次是调用printf.
在调用b,c时的调用是短跳转,通过相对位移来进行的跳转,对应的机械码为
0xE8 0x00 0x00 0x00 0x00
后面4字节是位移大小,那么我们只需要将这5字节填充为空指令nop(0x90)即可,然后执行
代码如下:
编译选项:Release|Win32 内联函数拓展:已禁用 (/Ob0)或只适用于 __inline (/Ob1)
数据执行保护(DEP):否 (/NXCOMPAT:NO)
#include <stdio.h>
void a(); void b(); void c(); //函数原型
int main() { a(); printf("\n"); return 0; } //main
void a() { b(); printf("one "); }
void b() { c(); printf("two "); }
void c() {
int x;
// 在此处加代码
typedef void(*func)(void);
typedef unsigned char byte;
byte buffer_a[1 << 7];
byte buffer_b[1 << 7];
int index = 0;
byte *pa_code = (byte*)a;
byte *pb_code = (byte*)b;
func fa = (func)&buffer_a, fb = (func)&buffer_b;
bool find = false;
while (*pa_code != 0xc3)
{
if (*pa_code == 0xe8){
if (!find){
for (int i = 0; i < 5; i++)
{
buffer_a[index++] = 0x90;
pa_code++;
} //用nop替换call b
find = true;
}
else
{
buffer_a[index] = 0xe8;
int addr = *(int*)(pa_code + 1);
int *newAddr = (int*)(&buffer_a[index + 1]);
*newAddr = addr - (int)(&buffer_a[index] - pa_code);
index += 5, pa_code += 5;
} //重新计算call跳转位移
}
buffer_a[index++] = *(pa_code++);
}
buffer_a[index] = 0xc3;
index = 0;
find = false;
while (*pb_code != 0xc3)
{
if (*pb_code == 0xe8){
if (!find){
for (int i = 0; i < 5; i++)
{
buffer_b[index++] = 0x90;
pb_code++;
} //用nop替换call b
find = true;
}
else
{
buffer_b[index] = 0xe8;
int addr = *(int*)(pb_code + 1);
int *newAddr = (int*)(&buffer_b[index + 1]);
*newAddr = addr - (int)(&buffer_b[index] - pb_code);
index += 5, pb_code += 5;
} //重新计算call跳转位移
}
buffer_b[index++] = *(pb_code++);
}
buffer_b[index] = 0xc3;
fa();
fb();
volatile int i = *(int *)0; //exit(0); 暴力退出,搞个大新闻
}
因为没有#include <stdlib.h>所以没办法调用exit(0);退出程序,又不能调用其它函数,只能弄个指针错误,搞个大新闻,暴力退出.
这个绝对是黑科技,嗯,专门黑人的科技...