avatar

linux-pwn技能树

最近发现对过往的知识掌握还不够,所以打算建一个自己的技能树,以此来记录自己掌握的技术,也便于发现不足。

参考资料:ctf-wiki-pwn

二进制漏洞利用的过程,就是一步步扩大可以控制的内存的范围

保护机制

需要重点关注的是canary,其他的保护如下:

  • FORTIFY保护:限制格式化字串漏洞
  • NX保护:堆栈不可执行
  • PIE保护:地址随机化
  • RELRO保护:GOT表不可写
  • 去除符号表(stripped):增加逆向难度

canary保护

用于防止栈溢出被利用的一种方法,原理是在栈的ebp下面放一个随机数,在函数返回之前会检查这个数有没有被修改,就可以检测是否发生栈溢出了。

泄漏栈中的canary

Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。

需要存在合适的输入函数例如read(),不会在后面加截断符

绕过canary

不修改canary而完成栈溢出,例如scanf把某些输入(+、-)认定为无效而不是非法,从而不修改canary而完成栈溢出

pwnable-tw-dubblesort-保护全开栈溢出/#利用

one-by-one 爆破 Canary

对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。

canary的最低位为0x00,之后逐次爆破,如果canary爆破不成功,则程序崩溃;爆破成功则程序进行下面的逻辑。由此可判断爆破是否成功。

我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。

劫持__stack_chk_fail 函数

已知 Canary 失败的处理逻辑会进入到 __stack_chk_fail 函数,__stack_chk_fail 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。

覆盖 TLS 中储存的 Canary 值

已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。

参考:Bypassing Canary——Override Canary in TLS

栈溢出

初级ROP

ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

ret2text

ret2text 即控制程序执行程序本身已有的的代码 (.text),例如存在后门函数等情况。

ret2shellcode

ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

我的WP:pwnable-tw-death-note-可见字符shellcode

ret2syscall

ret2syscall,即控制程序执行系统调用,获取 shell。

例如我们利用如下系统调用来获取 shell

1
execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

我们使用各种gadgets即可

ret2libc

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

再就是one_gadget,应该也可以算做这类利用

中级ROP

中级 ROP 主要是使用了一些比较巧妙的 Gadgets。

ret2csu

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp

这里我们可以利用以下几点

  • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
  • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。

ret2reg

还没写过题目,仅在此记录原理

  1. 查看溢出函返回时哪个寄存值指向溢出缓冲区空间
  2. 然后反编译二进制,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址
  3. reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的)

BROP

Blind ROP,简而言之就是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。

也还没写过这类题目

参考ctf-wiki-BROP

高级ROP

ret2_dl_runtime_resolve

在 linux 中是利用_dl_runtime_resolve(link_map_obj, reloc_index) 来对动态链接的函数进行重定位的。如果我们可以控制相应的参数以及其对应地址的内容就可以控制解析的函数了。

还没写过这类题目

参考ctf-wiki-ret2_dl_runtime_resolve

SROP

简而言之就是通过sigreturn这个系统调用来控制各寄存器的值

还没写过这类题目

ctf-wiki-SROP

Stack smash

在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。

如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail 函数中就会输出我们想要的信息。

栈上的 partial overwrite

我们知道, 在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。

栈迁移

栈迁移一般是ROP利用的前序步骤,目标是劫持esp寄存器到我们控制的内存上去,然后在ret的时候就可以进入我们布置好的ROP链

例如:pwnable-tw-3x17-任意地址写构造ROP

格式化字符串

printf

最核心的利用:

  1. 利用%p进行参数打印,用于实现任意内存泄漏
  2. 利用%n进行内存修改,利用%$n/hn/hhn可以将内存值修改为之前打印的字符数目

64位格式化字符串和栈溢出

sprintf

函数作用是字符串复制,我们可以利用字符串可变进行越界,类似于栈溢出

pwnable.tw-spirited_away-read与sprintf的栈溢出

strcpy

利用字符串复制的截断造成复制意料之外的数据

pwnable-tw-babystack-strcpy形成栈溢出

堆利用

据说,堆利用的本质就是UAF

off-by-one

off-by-one 是指单字节缓冲区溢出,利用:

  1. 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
  2. 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。

chunk extend

简而言之,就是通过控制chunk header 中的数据,完成对其他堆内容的控制

在ptmalloc中,获取后一堆块的地址是使用当前块指针加上当前块大小,获取前一堆块的地址是通过 malloc_chunk->prev_size 获取前一块大小,然后使用本 chunk 地址减去所得大小,而判断当前chunk是否是use状态的操作是查看下一 chunk 的 prev_inuse 域。

在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果。

类似unlink的利用:pwnable-tw-applestore-利用栈平衡控制内存/#delete一次有约束的地址写

UAF

use after free 顾名思义,就是一个内存块被释放之后我们还是可以使用它

pwnable-tw-re-alloc-利用realloc实现uaf更改got表

fastbin-attack

house-of-spirit

house of spirit 技术的核心原理是在目标位置处伪造一个chunk,并将其释放,从而达到分配指定地址的chunk的目的,成功的关键是要能够修改指定地址的前后的内容使其可以绕过对应的检测

pwnable-tw-TcacheTear-tcache-dupANDhouse-of-spiritAND-free-hook

arbitrary-alloc

通过寻找合适的值完成任意地址分配

pwnable-tw-secret-garden-fastbin任意地址分配修改hook

unsortedbin-attack

main-arena泄漏libc基址

pwnable-tw-hacknote-用main-arena泄漏libc/#unsortbin泄露libc基址

tcache-attack

tcache-dup

类似于简易版double free

1
2
3
4
5
6
7
8
a = malloc(0x20);

free(a);
free(a);

malloc(0x20,addr)
malloc(0x20)
malloc(0x20,data)

pwnable-tw-TcacheTear-tcache-dupANDhouse-of-spiritAND-free-hook/#tcache-dup构造任意地址写

glibc2.29-key指针

glibc2.29引入的key指针,它指向堆开头的tcache_perthread_struct结构的地址,我们将他泄漏就可以得到堆基址。

googlectf_echo

IO FILE

libc实现的文件流机制中存在可以被改写的函数指针

glibc2.23下伪造vtable

伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。

pwnable-tw-seethefile-栈溢出构造fake-FILE/#前置知识之-IO-FILE

逻辑漏洞

简而言之就是程序逻辑不完善造成的意外情况

pwnable-tw-calc-逻辑漏洞实现任意地址读写

整数溢出

主要是类型转换,上下界溢出等情况

10种整数溢出和浮点精度导致的漏洞

沙盒逃逸

我的理解就是在各种限制下完成shell的获取

plaidctf2020_sandybox

文章作者: 0bs3rver
文章链接: http://yoursite.com/2020/12/03/linux-pwn%E6%8A%80%E8%83%BD%E6%A0%91/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 0bs3rver的小屋