PWN 学习笔记
原理知识:
基础篇(栈的原理):
https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/stack_intro/
1
2
3
4https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/stack_intro/
http://www.cnblogs.com/clover-toeic/p/3755401.html
http://www.mamicode.com/info-detail-1990426.html
https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/stackoverflow_basic/
file 命令: 用来探测给定文件的类型。file命令对文件的检查分为文件系统、魔法幻数检查和语言检查3个过程
1
2root@kali:~/Desktop# file what_the_fuck
what_the_fuck: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f5874ff98d454f054743d010a0456a89f09aa535, strippedchecksec命令: 该脚本来查询该文件使用了哪些防护技术
1
2
3
4
5
6
7root@kali:~/Desktop# checksec what_the_fuck
[*] '/root/Desktop/what_the_fuck'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)数据保护机制:
NX 类似于(windows上的DEP)
1
2
3在AMD64位CPU上,在页面表(page table)中的页面信息加了一个特殊的位,NX位(No eXecute)。
如果NX位为0,这个页面上可以执行指令。
如果NX位为1,这个页面上不允许执行指令。如果试图执行指令的话,就会产生异常。Stack Canary
1
2
3在缓冲区和控制信息(如 EBP 等)间插入一个 canary word。
这样,当缓冲区被溢出时,在返回地址被覆盖之前 canary word 会首先被覆盖。
通过检查 canary word 的值是否被修改,就可以判断是否发生了溢出攻击PIE 地址空间分布随机化 ALSR
1
2
3
4
5
6
7
8
9内存地址随机化机制(address space layout randomization),有以下三种情况
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。
Built as PIE:位置独立的可执行区域(position-independent executables)。
这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。RELRO 符号重定向表格
1
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。
linux保护机制整理
DEP,ASLR,更强的Selinux,内核代码段只读,PXN
DEP,ASLR,Selinux等技术在PC时代就已经比较成熟了。内核代码段只读也是可以通过修改ptmx_fops指针表等方案来绕过
DEP(windows)、NX(linux)堆栈代码执行保护
绕过方法:1
2使用 (ROP)Return-Oriented Programming.绕过 (如ret2data、ret2libc、ret2strcpy、ret2gets、ret2syscall)
gadget:virtualprotect、jmp esp、mona.pyASLR 地址随机化
绕过方法:
1
2
3
4
51、直接RET替换(一般进程也会加载没有随机化的模块,可以找到JMP ESP指令的跳板直接调用)
2、替换EIP一部分(找到没有随机化的模块然后使用利息泄漏确定EIP的位置,再算出模块的基地址,最后算出要跳的函数地址)
3、NOP喷射(DEP没开的情况下,创建一大块NOP+shellcode,Heap Spray是在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。
统slide code(滑板指令)一般是NOP指令,譬如0x0C(0x0C0C代表的x86指令是OR AL 0x0C),0x0D等等,不影响程序的执行的。)
4、暴力(如果漏洞不会造成程序崩溃,可以暴力测试256种模块基地址来测试,只到有满足的)最LOW
Self-Protection Project(KSPP)
内核代码段和常量数据只读保护
1
2
3
4
5
6
7mark_rodata_ro
写保护的实现是通过set_memory_ro函数内部调用实现set_page_attributes来实现的。
攻击方法:
针对这种页保护的防御,较常用的方法是,从物理页表中取相应的页表条目,找到页表描述符,修改相应的权限。我们可以通过利用内核中现成的代码来完成 页表属性的修改。我们发现可以通过内核导出函数set_memory_rw来打开内核页表的读写权限。set_memory_rw函数的定义如下:
int set_memory_rw(unsigned long virt, int numpages)PXN( PrivilegedExecute-Never ) “特权执行从不”
PXN的绕过方法:1
2
3
4
5
6
7
8
9
10
11
12利用ROP技术绕过PXN
ROP主要原理是通过控制内存中的一段数据,通过控制数据来控制代码执行流,如组合执行内核中特定的代码片段,从而达到修改内核中的关键数据,达到提权限的目的。这种攻击方式是需要进行不同机型中查找到多段代码片段,如果需要root的机型较多,则需要攻击者投入较多精力去做适配。
利用RET2DIR技术
利用原理是,Linux内核在设计的时候,在用户空间映射内存的时候,为了提高内存的操作效率,内核也相应地在内核的低端内存区地址映射一段影子内存。
者利用该缺陷,将用户空间的攻击代码映射到内核的低端内存可执行区或者将特定数据进行喷射到内核的低端内存,进行内存布局,然后利用发现的漏洞,让内核执行攻击代码,从而达到提权的作用。这项技术在32位arm设备上有65%以上的成功率,而在64位arm中有96%的成功率。
与ROP不同,RET2DIR这项技术不需要对内核代码进行重利用和组合,就可以直接将攻击代码或数据映射到内核的低端内存。
由于64位ARM内核的设备都已经开启了PXN防护,这项技术成为通用root工具绕过64位ARM内核的PXN必备技术。在KingRoot的cve-2015-3636和cve-2016-1805漏洞利用中都使用到了该技术绕过PXN防护。
通过内核特定函数完成PXN绕过。
该技术在2016年MOSEC大会上由360团队公开,该技术巧妙地利用kernel_setsockopt函数的特性,通过控制r0, 让内核执行set_fs(KERNEL_DS),实现任意地址读写权限的效果。KNOX绕过
1
2
3
4三星KNOX里对内核保护主要由TIMA完成。TIMA 使用 ARM TrustZone硬件,持续的监控linux内核的完整性。
Linux内核采用的CRC完整性认证机制不同,TIMA采用了数字证书签名技术对加载的内核模块进行合法性验证,以确保每个加载的模块都是合法的。
在2014年SyScan360大会上360团队的陈章琪和申迪介绍了TIMA LKM验证机制的绕过方法。他们的思路是通过Patch内核的代码,绕过TIMA验证。具体的攻击方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24if(memcmp(hdr->e_ident, ELFMAG, SELFMAG) != 0
|| hdr->e_type != ET_REL || elf_check_arch(hdr)
|| hdr->e_shentsize != sizeof(Elf_shdr)){
err = -ENOEXEC;
goto free_hdr;
}
if(len < hdr->e_snoff + hdr->e_shnum * sizeof(Elf_shdr)){
err = -ENOEXEC;
goto free_hdr;
}
if(lkmauth_bootmode != BOOTMODE_RECOVERY &&
lkmauth(hdr, len) != RET_LKMAUTH_SUSSESS){
pr_err
("TIMA:lkmauth--unable to load kernel module;module len is %1u,\n",len);
err = -ENOEXEC;
goto free_hdr;
}
info->hdr = hdr;
info->len = len;
return 0;1
2
3
4
5
6
7
8
9
10
11
12
13X2.0,TIMA引入了实时内核保护(RKP)技术。RKP可在TrustZone内对操作系统进行持续不断且富有策略性的实时监控,以防止篡改内核。
RKP可对内核内部发生的重要事件进行审计(可在ARM中进行检查)。如果确定某个事件对 OS 内核的完整性具有影响,则 RKP 将会停止该事件,或记录怀疑存在篡改行为的认证结论,并将其发送至 MDM。这可以防止恶意修改和注入内核代码,包括强制内核破坏自身的数据。
在2.0版本的RKP除了保护页表之外,还保护一些关键的内核对象(如cred,real_cred)。RKP将存储关键安全信息对象的kmem_cache里所有的页都设为只读,只能在TIMA里面对kmem_cache里的页进行写操作。
在MOSEC2016大会上,科恩实验室的方家弘介绍了KingRoot产品中修改这些关键安全数据,实现DKOM的方法。KingRoot的方法是利用cve-2015-1805的任意地址写的漏洞,修改file_operations里的int (*check_flags)(int) 函数指针,使得函数指针指向override_creds函数。
通过控制check_flags函数输入参数,使得TIMA主动修改cred的值,从而绕过RKP防护,达到提权的效果。
除了防止运行时修改关键的安全数据结构之外,RKP还对一些系统调用进行监控,如execve系统调用。
对于任何的ROOT进程,sec_restrict_fork函数将判断是否该进程的路径是来自/data目录,正常情况下,该目录是存放用户程序的唯一路径。三星希望这样可以阻止类似SU这样的程序可以给/DATA/目录下的用户程序赋权限的情况发生。但是,我们可以依然可以修改一些关键数据来绕过sec_restrict_fork函数的判断分析漏洞
写exp
PWNTOOLS的基本使用方法
1 | #!/usr/bin/env python |
命令 | 功能 |
---|---|
elf = ELF("./easypwn") |
在本地静态加载可执行文件 |
mian = elf.symbols['main'] |
获取本地加载的elf文件的某个函数的加载地址 |
io.process('./easypwn') |
在本地加载可执行文件 |
io.recvuntil('elf_String\n') |
运行程序后直到接收到字符串(elf_String)之后 |
io.sendline('string') |
向进程发送一行数据结尾包含换行符(0x0a) |
io.send('string') |
向进程发送数据不包含换行符 |
p64(data)/p32(data) |
将数据打包为64/32位运行环境字节码 |
u64(data)/u32(data) |
将数据解包为64/32位环境字节 |
log.info(data) |
将data显示为运行时调试信息 |
一些基础知识点
x64下前6个参数不是保存在栈中,而是通过寄存器传值
64位汇编
当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。参数个数大于 7 个的时候
- H(a, b, c, d, e, f, g, h);
- a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
- h->8(%esp)
- g->(%esp)
- call H
ELF文件加载到内存中的状态
BSS段:
- BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。
- BSS是英文Block Started by Symbol的简称。
- BSS段属于静态内存分配。
数据段:
- 数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。
- 数据段属于静态内存分配。
代码段:
- 代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。
- 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。
- 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap):
- 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
- 当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack):
- 栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。
- 除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
- 由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
确定缓冲区大小的peda方法:
1 | pattern_create 60 |
确定缓冲区大小的gef方法:
1 | gef➤ pattern create 200 |
使gef的调试信息输出到不同的终端窗口
1 | tty #这个命令查看当前打开的终端数量 |
objdump使用相关技巧
1 | objdump -f test 显示test的文件头信息 |
常用的查找偏移的办法
strings工具
1
2root@kali:~/Desktop/PWN/guess# strings -a -tx /lib/x86_64-linux-gnu/libc.so.6 | grep "environ"
142be __environROPgadget工具
1
2
3
4
5
6
7
8
9root@kali:~/Desktop/PWN/guess# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "__environ"
Gadgets information
============================================================
Unique gadgets found: 0
root@kali:~/Desktop/PWN/guess# ROPgadget --binary easypwn --only 'pop|ret'
...
root@kali:~/Desktop/PWN/guess# ROPgadget --binary easypwn --only 'pop|ret' | grep 'ebx'
...目前感觉上面的两种工具都有自己的优点:
- strings工具更好的锁定libc文件中的字符串的位置,搜索速度更快
- ROPgadget工具能够更好的锁定汇编代码的片段提供的信息更加详细,但搜索速度较慢
gef工具与pwndbg调试工具区别
- 最近刚刚发现的这两个工具的一个不同之处,具体原因解释的可能不对,但这个现象是存在的。
- 遇到程序fork()或者其他什么能够产生另一个进程的程序的时候,gef不会直接跳转到这个进程去让你调试,如果你所下的断点包含在fork()子进程的代码部分里面,gef会直接执行,并不会在该位置停下,总结一下就是你的这个断点的执行效果不在当前进程的时候gef不能够帮你断下来,他会直接开了子进程,然后执行到该断点处继续执行,切记不会停下来,此时对于父进程而言,是gef正在调试的进程则会处在等待子进程相应返回的状态,待到子进程返回之后,父进程继续执行。换句话说,就是你的断点不在当前进程时,gef不会帮你断下来。如果父进程有wait() 函数等待子进程状态,而你在调试的时候是单步步过的call wait这句命令,那么gef会陷入阻塞状态等待子进程返回,而此时子进程又断了下来(因为单步执行),gef就会一直等这个暂停的进程,使用gdb,attach到该子进程后使其继续执行,父进程的gef等待状态在收到子进程结束的信息后会结束等待状态。
- 然而pwndbg则会直接帮你跳转到子进程去,在断点的位置停下,并且结束后自动返回父进程进行调试
GDB调试技巧
汇编语言ret后带一个参数的执行方式
1 | si |
- 上面的内容是一个简单的例子,ret 0xc 的执行效果,首先从栈顶弹出一个值最为返回地址,去相应的位置执行,然后从栈顶弹出偏移0xc的内容,或者理解为 esp - 0xc,此程序为32位系统,则效果为从栈顶弹出三个值
1 | pwndbg> b main |
ROP(Return Oriented Programming)
参考链接:https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/basic_rop/
按照ctf_wiki上的顺序,逐步深入的学习
基本ROP
ret2text,ret2syscall,ret2shellcode,ret2libc
- 可以参考天舒的:https://blog.csdn.net/weixin_40850881/article/details/80216764
- 我自己的博客:
ret2resolve
使用readelf查看动态段 .dynamic 的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28root@kali:~/Desktop/ret2resolve# readelf -d babystack
Dynamic section at offset 0xf14 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x80482c8
0x0000000d (FINI) 0x80484f4
0x00000019 (INIT_ARRAY) 0x8049f08
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f0c
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804822c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 80 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80482b0
0x00000011 (REL) 0x80482a8
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x8048288
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x804827c
0x00000000 (NULL) 0x0JMPREL中保存的是.rel.plt表中的信息
1
0x00000017 (JMPREL) 0x80482b0
PLTRELSZ是.rel.plt表的大小为24字节
1
0x00000002 (PLTRELSZ) 24 (bytes)
RELENT为每个.rel.plt表项的大小为8字节
1
0x00000013 (RELENT) 8 (bytes)
这些表项为ELF32_Rel类型的数据结构。其中r_offset是该函数在got表中的位置,r_info为其类型和符号序号。
1
2
3
4
5
6
7
8
9typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;查看 .rel.plt 表中的信息,使用命令
readelf -r babystack
1
2
3
4
5
6
7
8
9
10
11root@kali:~/Desktop/ret2resolve# readelf -r babystack
Relocation section '.rel.dyn' at offset 0x2a8 contains 1 entry:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x2b0 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 alarm@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0r_info为0x107,其中1表示read在符号表SYMTAB中的偏移为1,7表示为R_386_JUMP_SLOT类型,这一项用于检查,保持为7即可。我们前面看到的函数字符串就是通过在SYMTAB中的偏移来找到的。
1
2
3
4
5
6
7
8
9
10
11
12
13gef➤ x/30i read
0x8048300 <read@plt>: jmp DWORD PTR ds:0x804a00c
0x8048306 <read@plt+6>: push 0x0
0x804830b <read@plt+11>: jmp 0x80482f0
0x8048310 <alarm@plt>: jmp DWORD PTR ds:0x804a010
0x8048316 <alarm@plt+6>: push 0x8
0x804831b <alarm@plt+11>: jmp 0x80482f0
0x8048320 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804a014
0x8048326 <__libc_start_main@plt+6>: push 0x10
0x804832b <__libc_start_main@plt+11>: jmp 0x80482f0
0x8048330 <__gmon_start__@plt>: jmp DWORD PTR ds:0x8049ffc
0x8048336 <__gmon_start__@plt+6>: xchg ax,ax
0x8048338: Cannot access memory at address 0x8048338
中级ROP
高级ROP
堆溢出利用
什么是堆:
- 堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。
对于不同的应用来说,由于内存的需求各不相同等特性,因此目前堆的实现有很多种,具体如下
- 堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。
只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。
1 | dlmalloc – General purpose allocator |
系统函数调用号:
32位
1 | #ifndef _ASM_X86_UNISTD_32_H |
64位
1 | #ifndef _ASM_X86_UNISTD_64_H |