解决任务 4 中的问题有很多方法。另一种方法是在调用 system() 之前调用 setuid(0)。 setuid(0) 调用将真实用户 ID 和有效用户 ID 都设置为 0,将进程转变为非 Set-UID 进程(它仍然具有 root 权限)。这种方法要求我们将两个函数链接在一起。这种方法被推广为链接多个函数,并且进一步推广为链接多段代码。这就是返回导向编程(ROP)。
使用 ROP 解决任务 4 中的问题相当复杂,它超出了本实验的范围。然而,我们希望给大家一个 ROP 的体验,让你们处理 ROP 的一个特例。在 retlib.c 程序中,有一个名为 foo() 的函数,程序中从未被调用。该函数是为本任务准备的。你的任务是利用程序中的缓冲区溢出问题,使得程序从 bof() 函数返回时,调用 foo() 10次,然后给你 root shell。在你的实验报告中,你需要描述你的输入是如何构造的。结果将如下所示:
$ ./retlib
...
Function foo() is invoked 1 times
Function foo() is invoked 2 times
Function foo() is invoked 3 times
Function foo() is invoked 4 times
Function foo() is invoked 5 times
Function foo() is invoked 6 times
Function foo() is invoked 7 times
Function foo() is invoked 8 times
Function foo() is invoked 9 times
Function foo() is invoked 10 times
bash-5.0# ← Got root shell!
让我们回顾一下我们在任务 3 中所做的。我们在堆栈上构造数据,使得当程序从 bof() 返回时,它跳转到 system() 函数,并且当 system() 返回时,程序跳转到 exit() 函数。我们将在这里使用类似的策略。我们不会跳转到 system() 和 exit(),而是在堆栈上构造数据,使得当程序从 bof 返回时,它返回到 foo;当 foo 返回时,它返回到另一个 foo。这个过程重复 10 次。当第 10 个 foo返回时,它返回到 execv() 函数,给我们 root shell。
我们在本任务中所做的只是 ROP 的一个特例。你可能已经注意到 foo() 函数不接受任何参数。否则,调用它 10 次将变得更加复杂。通用的 ROP 技术允许你按顺序调用任何数量的函数,允许每个函数有多个参数。SEED书(第 3 版)提供了如何使用通用 ROP 技术解决任务 4 中问题的详细说明。它涉及调用 sprintf() 四次,然后调用 setuid(0),再调用 system("/bin/sh") 给我们 root shell。这种方法相当复杂,SEED 书中用了 15 页的内容来解释。