本文全部内容,以SSD6的Exercise1为示例。
代码如下图。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
int prologue [] = {
0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x20206F74, 0x74786565, 0x65617276, 0x32727463,
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A
};
int data [] = {
0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
0x466D203A, 0x65693A72, 0x43646E20, 0x6F54540A,
0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
0x6F786F68, 0x6E696373, 0x6C206765, 0x796C656B,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x20206F74, 0x74786565, 0x65617276, 0x32727463,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x21687467, 0x63002065, 0x6C6C7861, 0x78742078,
0x6578206F, 0x72747878, 0x78636178, 0x00783174
};
int epilogue [] = {
0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
0x20206F74, 0x74786565, 0x65617276, 0x32727463
};
char message[150];
void usage_and_exit(char * program_name) {
fprintf(stderr, "USAGE: %s key1 key2 key3 key4\n", program_name);
exit(1);
}
void process_keys12 (int * key1, int * key2) {
*((int *) (key1 + *key1)) = *key2;
}
void process_keys34 (int * key3, int * key4) {
*(((int *)&key3) + *key3) += *key4;
}
char * extract_message1(int start, int stride) {
int i, j, k;
int done = 0;
for (i = 0, j = start + 1; ! done; j++) {
for (k = 1; k < stride; k++, j++, i++) {
if (*(((char *) data) + j) == '\0') {
done = 1;
break;
}
message[i] = *(((char *) data) + j);
}
}
message[i] = '\0';
return message;
}
char * extract_message2(int start, int stride) {
int i, j;
for (i = 0, j = start;
*(((char *) data) + j) != '\0';
i++, j += stride)
{
message[i] = *(((char *) data) + j);
}
message[i] = '\0';
return message;
}
int main (int argc, char *argv[])
{
int dummy = 1;
int start, stride;
int key1, key2, key3, key4;
char * msg1, * msg2;
int* p = &dummy;
int* q = &key1;
key3 = key4 = 0;
if (argc < 3) {
usage_and_exit(argv[0]);
}
key1 = strtol(argv[1], NULL, 0);
key2 = strtol(argv[2], NULL, 0);
if (argc > 3) key3 = strtol(argv[3], NULL, 0);
if (argc > 4) key4 = strtol(argv[4], NULL, 0);
process_keys12(&key1, &key2);
start = (int)(*((char *) &dummy));
stride = (int)(*(((char *) &dummy) + 1));
if (key3 != 0 && key4 != 0) {
process_keys34(&key3, &key4);
}
msg1 = extract_message1(start, stride);
if (*msg1 == '\0') {
process_keys34(&key3, &key4);
msg2 = extract_message2(start, stride);
printf("%s\n", msg2);
}
else {
printf("%s\n", msg1);
}
return 0;
}
首先看刚进入main函数的前几个int变量。
int dummy = 1;
int start, stride;
int key1, key2, key3, key4;
理论上来说他们的地址空间应该是连续的,那么实际上如何呢?
每个变量相差12个字节,也就是3个int,居然没有挨着。
再看看内存呢。
可以看到的确是每个int相隔12字节,这个是编译器自己的优化,我们暂且不讨论。
但可以知道的是,我们能通过key1的地址,找到dummy的地址,并修改dummy的值。
dummy一旦被修改,start和stride也会被赋相应的值, 如此可得到message1.
再看后面,如何通过key3,key4,使程序跳过message1执行message2?
这里涉及到函数的运行机制。
概念
首先,明确几个概念。
ESP(Extended Stack Pointer):为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。
EBP(Extended Base Pointer):扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。
活跃记录(Activation Records and Stacks):栈顶的那个记录被称作活跃记录,也就是说在函数栈中,只有栈顶的记录可被程序指令访问和操作。
函数帧:执行函数所需要的信息集合,具体内容后面会提到。
程序当前地址:程序运行时当前指令所在地址,也是PC(Program Counter)程序地址寄存器中的值。
函数返回地址:函数执行完毕后应返回上一层函数继续执行指令的地址,也是指令地址。
函数调用过程
如上图所示 ,main函数中要调用两个函数,.1和2。左图中此时mian函数已经调用了2,当前的栈顶活跃记录为函数2,注意观察左图中有三个箭头,他们代表着在函数调用过程中函数栈上的三个关键指针,FramePointer对应ebp,StackPointer对应esp,或者说这两个指针的值分别存储在这两个寄存器中,另外一个没有名字的箭头,为上一层函数的FramePointer,当函数返回时,通过它来恢复调用该函数前的函数栈的信息。
再看右图,此时函数2调用函数1,此时发生的事件是:
1,保存当前FramePointer,一般保存在一个专用栈中。
2,将FramePointer指向StackPointer指向的地址。
3,将StackPointer指向栈顶。
函数返回过程
返回过程发生的事件为:
1,将StackPointer指向FramePointer指向的地址。
2,将专用栈中保存的上一层FramePointer出栈。
以上是有关函数调用和返回的简易理解,下面是详细的细节。
函数调用过程的变量
上图中有两个函数,bar为调用方,baz为被调用方。
在图一中,可以看到在真正进入baz之前,我们的指令还做了两件事:
1,将函数的参数压入函数栈。
2,将函数的返回地址压入栈中。
关于返回地址:可以看到函数的返回地址指向的是pop b指令,这条指令在call baz之后,含义是,返回地址所存储的地址值,是调用该函数的指令的下一条指令的地址,这很好理解,毕竟执行完函数还是需要回到被调用方继续执行接下来的指令。
关于函数参数:这里只有一个参数,当函数参数大于等于二得时候,参数列表的顺序和他们入栈的顺序是相反的。
更详细的信息是,函数内部所需要的局部变量存在哪里?
关于局部变量:由图可知,局部变量是按照顺序(不同的编译器可能不一样,但一定是在EBP和ESP之间)存在EBP和ESP之间的,并且空白的部分用0xCCCCCCCC填充,这也是为什么我们查看内存的时候经常会看到一连串的C的原因。
到这里所有函数调用相关的知识已经介绍完了,回过头来看问题。
我们可以通过key3,key4来修改process_key34的返回地址来达到执行message2的目的。
通过上面学习到的知识我们知道函数返回地址和参数是挨着的,并且参数列表入栈的顺序和列表序是相反的,又知道是参数先入栈,返回地址后入栈,所以返回地址刚好在key3的下面,如下图所示。
至于为什么是栈是从高地址向低地址进行的,这个是规定,记住就好。
那么偏移量显而易见了。
但是我们要将返回地址改成什么呢? 改成message2之前的process_key34地址就对了。
这里我们只能查看反汇编,查看两个process_key34结束地址的偏移量来确定二者的偏移。
到这里这道题就可以解开啦。
总结
- 指令地址和数据地址是不一样的,不要把PC和esp,ebp这种混为一谈;返回地址返回的是PC,而esp和epb控制的是栈指针,需要区分开。
- 总体上来说,程序运行的整个过程,是由指令来控制的,但是程序需要调用各种函数,这时就需要使用栈来管理,而esp,ebp,返回地址都是为管理这个栈和保证程序顺利运行来服务的,并且在esp和ebp之间会存有局部变量,在活跃记录的生命周期中存活。
- 不同的编译器数据的间隔长度可能不同,要根据实际测试的情况解题。