更改函数的返回地址不如预期工作

huangapple go评论64阅读模式
英文:

Changing the return address of a function not working as expected

问题

I recently learned about Assembly x86 and how functions are implemented in it and how the stack program works.

However, I tried writing this program which calls a function f2 by changing the return address of the current called function f1, so that the instruction pointer starts f2 when finishing f1, therefore not returning directly to main.

It seems unstable and sometimes I get segmentation fault, while in another cases it works but does not return 0.

Why is that?

My guess is that the program stack is not given a contiguous space in memory at run time and so its behavior is not constant.

Sometimes it works if a change v[2] = (uintptr_t) f2; into v[another_index_greater_than_2] = (uintptr_t) f2;.

It is odd, since in theory v[1] should be the old base pointer pushed on the stack, while v[2] should be the return address of the function.

#include <iostream>;

using namespace std;

int main();

void f2()
{
    int v[1];
    cout << "f2";
    v[2] = (uintptr_t) main;
}

void f1()
{
    int v[1];
    cout << "f1";
    v[2] = (uintptr_t) f2;
}

int main()
{
    f1();
    cout << "Back to main";
    return 0;
}

I expected to see the 3 strings printed in order (f1, f2, main) and the program to return 0, but the behavior of the program seems to be random.

英文:

I recently learned about Assembly x86 and how functions are implemented in it and how the stack program works.

However, I tried writing this program which calls a function f2 by changing the return address of the current called function f1, so that the instruction pointer starts f2 when finishing f1, therefore not returning directly to main.

It seems unstable and sometimes I get segmentation fault, while in another cases it works but does not return 0.

Why is that?

My guess is that the program stack is not given a contiguous space in memory at run time and so its behavior is not constant.

Sometimes it works if a change v[2] = (uintptr_t) f2; into v[another_index_greater_than_2] = (uintptr_t) f2;.

It is odd, since in theory v[1] should be the old base pointer pushed on the stack, while v[2] should be the return address of the function.

#include &lt;iostream&gt;

using namespace std;

int main();

void f2()
{
    int v[1];
    cout &lt;&lt; &quot;f2\n&quot;;
    v[2] = (uintptr_t) main;
}

void f1()
{
    int v[1];
    cout &lt;&lt; &quot;f1\n&quot;;
    v[2] = (uintptr_t) f2;
}

int main()
{
    f1();
    cout &lt;&lt; &quot;Back to main&quot;;
    return 0;
}

I expected to see the 3 strings printed in order (f1, f2, main) and the program to return 0, but the behavior of the program seems to be random.

答案1

得分: 1

正如@sklott所说,编译器可能会通过类似优化的方式生成意外的代码。本答案假设这是预期的输出。

在函数序言中,rbp/ebp寄存器首先被推入。因此,在返回地址之前有一个被推入的rbp/ebp寄存器。
如果你编译为x64,则推入的rbp寄存器是8字节,但int可能是4字节

在这种情况下,你很可能会覆盖保存的rbp的高4字节。也许这会导致f2不被执行,然后返回到main。避免的rbp将被覆盖,在某些情况下可能会导致段错误。

假设的堆栈(不带canary):
+--------+----------------+------+
|rsp+0x8 |                | v[0] |
+--------+----------------+------+
|rsp+0x10| saved rbp      | v[1] |
+--------+----------------+------+
|rsp+0x18| return address | v[2] |
+--------+----------------+------+

实际堆栈(不带canary):
+--------+----------------+------+
|rsp+0xc |                | v[0] |
+--------+----------------+------+
|rsp+0x10| saved rbp      | v[1] |
|rsp+0x14|                | v[2] |
+--------+----------------+------+
|rsp+0x18| return address |      |
+--------+----------------+------+

要解决这个问题,请将int v[1]替换为uintptr_t v[1];
然而,(uintptr_t) main;将再次调用f1();,所以这将成为一个无限循环。


请注意,如果不在GCC中添加-fno-stack-protector,该代码将无法正常工作,因为canaries默认启用。但由于问题中似乎可以正常工作,我假设已经添加了这个选项。

英文:

As @sklott said, the compiler may produce unexpected code by something like optimization. This answer assumes that it's expected output.

In the function prologue, rbp/ebp register is pushed first. Therefore, there is a pushed rbp/ebp register before the return address.
If you compile for x64, the pushed rbp register is 8-bytes but int might be 4-bytes.

In this case, you're likely overwriting the higher 4-bytes on saved rbp. Maybe it'll cause f2 won't be run, and it'll return to main. The avoided rbp will be overwritten, it'll cause segmentation fault in some cases.

Assumed stack (without canary):
+--------+----------------+------+
|rsp+0x8 |                | v[0] |
+--------+----------------+------+
|rsp+0x10| saved rbp      | v[1] |
+--------+----------------+------+
|rsp+0x18| return address | v[2] |
+--------+----------------+------+

Actual Stack (without canary):
+--------+----------------+------+
|rsp+0xc |                | v[0] |
+--------+----------------+------+
|rsp+0x10| saved rbp      | v[1] |
|rsp+0x14|                | v[2] |
+--------+----------------+------+
|rsp+0x18| return address |      |
+--------+----------------+------+

To solve it, replace int v[1] with uintptr_t v[1];.
However, (uintptr_t) main; will call f1(); again, so it'll be an infinite loop.


Note that the code won't work without adding -fno-stack-protector in GCC because canaries are enabled by default. However, since it seems to work in the question, I assume that it's added.

huangapple
  • 本文由 发表于 2023年2月23日 21:23:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75545447.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定