C中的堆栈操作,不使用内联汇编
我一直在准备编码竞赛,并在互联网上遇到了这个问题:
#include void a(); void b(); void c(); int main() { a(); printf("\n"); return 0; } void a() { b(); printf("hi "); } void b() { c(); printf("there "); } void c() { int x; // code here and nowhere else }
解决方案是编写将打印出“hi there”而不是“there hi”的代码(不能使用额外的打印function,代码只能放在注释块中)。
由于我已经完成了一些基本的汇编编码,我意识到这可以通过使用整数x
作为基础的堆栈操作来完成。
我尝试使用gdb查找函数的返回地址,然后交换a
和b
的返回地址。 编译器抛出一个分段错误,因此我假设我没有使用正确的返回地址。
如何正确计算偏移量以找到返回地址? gdb上的信息框架命令没有帮助,因为使用给定的堆栈地址值不起作用。
我在Linux上使用gcc运行它。
我不确定以下内容是否会计算在内。 它可以在POSIX上移植。 基本上你在第一次调用之前更改printf的缓冲区并在它被刷新到终端之前进行操作
void c() { static int first = 1; if (first) { first = 0; char buf0[BUFSIZ]; char buf1[BUFSIZ]; setvbuf(stdout, buf0, _IOFBF, BUFSIZ); a(); memcpy(buf1, buf0 + 6, 3); memcpy(buf1 + 3, buf0, 6); memcpy(buf0, buf1, 9); buf0[8] = '\n'; fflush(stdout); exit(0); } }
您将收到有关隐式声明库函数memcpy
和exit
警告。 虽然气馁,但它对C89来说是合法的。 但在你的情况下,我认为没有诡计太肮脏。 您可以通过手动复制字符来避免使用memcpy。 您可以通过freopen
重定向stdout
来避免exit
。 如果系统具有奇怪的小缓冲区大小(小于9),则可以将BUFSIZ
更改为大常量。 这个解决方案的变体不需要你手动插入\n
而是让程序正常从main退出并使用printf("\n")
放置该行的末尾
除非你以与攻击者破坏某些进程堆栈相同的方式粉碎堆栈,否则无法解决此问题。
只有了解编译器的每个实现细节,才能完成覆盖堆栈,否则问题无法解决。
如果您知道编译的细节(特别是堆栈结构),您可以使用本地x变量的地址,以便从堆栈(FRAME_C)获取当前帧的地址; 在每个帧中是前一帧的基指针并修改它。
堆栈看起来像这样:
FRAME_MAIN = RET_OS some-data FRAME_A = RET_MAIN some-data FRAME_B = RET_A some-data FRAME_C = RET_B some-data(including the variable `x`)
使用&x
我们可以检测到FRAME_C的位置。
一个解决方案是
- 在函数c()中打印“Hi”
- 修改FRAME_B,使RET_A成为RET_MAIN
- 从函数c()
return
棘手的操作是2.但是如果每个帧都有已知的大小,那么我们可以修改帧B的返回指针RET_A并检测RET_MAIN类似的东西:
*(&x+FRAME_C_SIZE+some-small-offset1) = /* *&RET_A = */ *(&x+(FRAME_C_SIZE+FRAME_B_SIZE)+some-small-offset2). /* *&RET_MAIN */
如您所见,您需要了解有关编译器实现的大量详细信息,因此这根本不是一个可移植的解决方案。
其他解决方案是打印“hi,there”并将stdout
重定向到/dev/null
。 我认为不允许使用exit()或其他编译器依赖技巧,否则问题对于竞赛没有意义。
我的解决方案是针对x86 / x64和CL
编译器,但是想想gcc也存在。
仅限问题 – 存在等效于函数:
void ** _AddressOfReturnAddress();
和__declspec(noinline)
等效 – 告诉编译器永远不要内联特定的函数
let void* pb
– 在c();
之后的void b()
地址c();
和void* pa
是b();
之后的void a()
地址b();
因为a
和b
几乎相同 – 我们可以假设
(ULONG_PTR)pa - (ULONG_PTR)&a == (ULONG_PTR)pb - (ULONG_PTR)&b;
当然, a
和b
堆栈布局必须相同。 基于此和解决方案。 下一个代码测试/使用CL
编译器 – 在x86 / x64(windows)和/Ox (Full Optimization)
和/Od (Disable (Debug))
– 都工作。
extern "C" void ** _AddressOfReturnAddress(); void a(); void b(); void c(); int main() { a(); printf("\n"); return 0; } __declspec(noinline) void a() { b(); printf("hi "); } __declspec(noinline) void b() { c(); printf("there "); } __declspec(noinline) void c() { void** pp = _AddressOfReturnAddress(); void* pb = *pp; void* pa = (void*)((ULONG_PTR)&a + ((ULONG_PTR)pb - (ULONG_PTR)&b)); for (;;) { if (*++pp == pa) { *pp = pb; *_AddressOfReturnAddress() = pa; return; } } }