這裡不再解釋vDSO的概念,而直接談其意義:
- vDSO類似一個資訊公告板,使用者可以直取所需,而無需為此辦理任何手續。
- vDSO相當於核心直接暴露出來的一個C庫,作為GLIBC的補充。
- …
類似gettimeofday之類的呼叫,每次都陷入核心去拿一個時間戳,顯得有點昂貴了,不如核心把時間戳放在一個公共的可以暴露給任何使用者的地方,使用者自己去看就行了,這是vDSO的典型用例。
為了簡單化描述,我們關閉ASLR:
[root@localhost ~]# sysctl -w kernel.randomize_va_space=0
隨便開啟一個ping程式,獲取其/proc/pid/smap中vdso的map區間:
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
Size: 8 kB
...
我們將其dd出來:
[root@localhost ~]# dd if=/proc/3688/mem of=./vsdo.dd obs=1 bs=1 skip=140737354113024 count=8192
隨後我們看看它是什麼:
[root@localhost ~]# file ./vdso.dd
./vdso.dd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=09be88363f7ca8b05e2cb54a82d16bec2e840186, stripped
那麼,接下來可以objdump了,就像對待普通的動態連結庫一樣:
[root@localhost ~]# objdump -T vdso.dd
vdso.dd: 檔案格式 elf64-x86-64
DYNAMIC SYMBOL TABLE:
ffffffffff700354 l d .eh_frame_hdr 0000000000000000 .eh_frame_hdr
ffffffffff700700 w DF .text 000000000000059d LINUX_2.6 clock_gettime
0000000000000000 g DO *ABS* 0000000000000000 LINUX_2.6 LINUX_2.6
ffffffffff700ca0 g DF .text 00000000000002d5 LINUX_2.6 __vdso_gettimeofday
ffffffffff700fa0 g DF .text 000000000000003d LINUX_2.6 __vdso_getcpu
ffffffffff700ca0 w DF .text 00000000000002d5 LINUX_2.6 gettimeofday
ffffffffff700f80 w DF .text 0000000000000016 LINUX_2.6 time
ffffffffff700fa0 w DF .text 000000000000003d LINUX_2.6 getcpu
ffffffffff700700 g DF .text 000000000000059d LINUX_2.6 __vdso_clock_gettime
ffffffffff700f80 g DF .text 0000000000000016 LINUX_2.6 __vdso_time
看看,看看,裡面竟都是些什麼東西,竟是一些時間公告函數啊,這意味著如果你想獲取時間,調這裡的函數就好了,我們看看最簡單的time系統呼叫是如何來獲取時間的,下面是對待vdso.dd檔案的objdump -D的結果:
ffffffffff700f80 <__vdso_time@@LINUX_2.6>:
ffffffffff700f80: 55 push %rbp
ffffffffff700f81: 48 85 ff test %rdi,%rdi
ffffffffff700f84: 48 8b 04 25 a8 f0 5f mov 0xffffffffff5ff0a8,%rax
ffffffffff700f8b: ff
ffffffffff700f8c: 48 89 e5 mov %rsp,%rbp
ffffffffff700f8f: 74 03 je ffffffffff700f94 <__vdso_time@@LINUX_2.6+0x14>
ffffffffff700f91: 48 89 07 mov %rax,(%rdi)
ffffffffff700f94: 5d pop %rbp
ffffffffff700f95: c3 retq
很顯然,並沒有呼叫任何系統呼叫,而是直接從地址0xffffffffff5ff0a8處拿到了時間,那麼地址0xffffffffff5ff0a8一定就是核心對映到使用者態的時間公告板的位置了。
記住地址0xffffffffff5ff0a8,使用者態的分析到此告一段落,我們進入核心去看一看。
首先從/proc/kallsyms中查到vdso的位置:
ffffffff81941000 D vdso_start
ffffffff819424b0 D vdso_end
其次我們找到核心時間公告板vsyscall_gtod_data的位置:
ffffffff81a75080 D vsyscall_gtod_data
我們看一下該公告板的值:
crash> struct vsyscall_gtod_data.wall_time_sec ffffffff81a75080
wall_time_sec = 1600912854
crash> struct vsyscall_gtod_data.wall_time_sec ffffffff81a75080
wall_time_sec = 1600912856
crash> struct vsyscall_gtod_data.wall_time_sec ffffffff81a75080
wall_time_sec = 1600912857
顯然,公告板的wall_time_sec欄位就是返回給time的值了。下面我們找到它的地址:
crash> struct vsyscall_gtod_data ffffffff81a75080 -o
struct vsyscall_gtod_data {
[ffffffff81a75080] seqcount_t seq;
struct {
int vclock_mode;
cycle_t cycle_last;
cycle_t mask;
u32 mult;
u32 shift;
[ffffffff81a75088] } clock;
[ffffffff81a750a8] time_t wall_time_sec;
[ffffffff81a750b0] u64 wall_time_snsec;
[ffffffff81a750b8] u64 monotonic_time_snsec;
[ffffffff81a750c0] time_t monotonic_time_sec;
[ffffffff81a750c8] struct timezone sys_tz;
[ffffffff81a750d0] struct timespec wall_time_coarse;
[ffffffff81a750e0] struct timespec monotonic_time_coarse;
}
嗯,就是0xffffffff81a750a8了。它就是對映到0xffffffffff5ff0a8暴露給使用者態的那個地址了。
我們接下來證實這一點:
我們再看公告板:
crash> struct vsyscall_gtod_data ffffffff81a75080
...
sys_tz = {
tz_minuteswest = 0,
tz_dsttime = 0
},
我們把sys_tz對映出去怎樣,這個值是一直為0的,我們期望的就是time返回0.
為此,我們首先拿到sys_tz和wall_time_sec之間的偏移:
crash> eval ffffffff81a750c8-ffffffff81a750a8
hexadecimal: 20
decimal: 32
octal: 40
因此,我們只要把vdso的time函數程式碼改掉即可:
ffffffffff700f84: 48 8b 04 25 a8 f0 5f mov 0xffffffffff5ff0a8,%rax
改為:
ffffffffff700f84: 48 8b 04 25 c8 f0 5f mov 0xffffffffff5ff0c8,%rax
即將time函數的第8個位元組,0xa8改成0xc8即可:
通過模式匹配,可以拿到time函數在vdso頁面的偏移:
f80: 55 push rbp
f81: 48 85 ff test rdi,rdi
f84: 48 8b 04 25 a8 f0 5f mov rax,QWORD PTR ds:0xffffffffff5ff0a8
f8b: ff
f8c: 48 89 e5 mov rbp,rsp
f8f: 74 03 je 0xf94
f91: 48 89 07 mov QWORD PTR [rdi],rax
f94: 5d pop rbp
f95: c3 ret
即0xf80.
那麼0xffffffff81941f80便是time函數其地址了:
unsigned char *addr = (unsigned char *)0xffffffff81941f80;
addr[8] = 0xc8;
在修改之前,我們先程式設計驗證:
#include <time.h>
#include <stdio.h>
typedef time_t (*time_func)(time_t *);
int main(int argc, char *argv[])
{
time_t tloc;
// 直接從地址拿值
unsigned long *p = (unsigned long *)0xffffffffff5ff0a8;
// 通過函數拿值
time_func func = (time_func)0x7ffff7ffaf80;
func(&tloc);
printf("%ld\n", tloc);
printf("%lu\n", *p);
}
預期的結果應該是兩種方式獲取的是同一個值:
[root@localhost ~]# ./a.out
1600923922
1600923922
[root@localhost ~]# ./a.out
1600923923
1600923923
[root@localhost ~]#
下面將核心頁面對應的指令修改之:
[root@localhost ~]# cat modtime.stp
#!/usr/local/bin/stap -g
function modtime(val:long)
%{
unsigned char *addr = (unsigned char *)0xffffffff81941f80;
unsigned char c = (unsigned char)STAP_ARG_val;
addr[8] = c;
%}
probe begin
{
modtime($1)
exit()
}
執行之:
[root@localhost ~]# ./modtime.stp 0xc8
[root@localhost ~]# ./a.out
0
1600924228
[root@localhost ~]# ./a.out
0
1600924229
[root@localhost ~]# ./modtime.stp 0xa8
[root@localhost ~]# ./a.out
1600924238
1600924238
[root@localhost ~]#
當修改了vdso頁面的指令後,所有呼叫time的程序都將異常,這是很顯然的:
top - 08:00:00 up 42 min, 3 users, load average: 0.00, 0.00, 0.00
Tasks: 114 total, 1 running, 113 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 0 total, 0 free, 0 used, 0 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 51696 3808 2492 S 0.0 inf 0:01.29 systemd
2 root 20 0 0 0 0 S 0.0 -nan 0:00.00 kthreadd
3 root 20 0 0 0 0 S 0.0 -nan 0:00.00 ksoftirqd/0
7 root rt 0 0 0 0 S 0.0 -nan 0:00.01 migration/0
8 root 20 0 0 0 0 S 0.0 -nan 0:00.00 rcu_bh
9 root 20 0 0 0 0 S 0.0 -nan 0:00.00 rcuob/0
10 root 20 0 0 0 0 S 0.0 -nan 0:00.00 rcuob/1
值得一提的是,在vdso之前,vsyscall機制也是類似,只是說它僅僅提供了一種map,而沒有抽象出動態連結的含義,因此也就無法享受ASLR帶來的安全保護了。
浙江溫州皮鞋溼,下雨進水不會胖。