通过快捷方式.lnk获得文件真实路径
前提
最近开发资源管理,需要预先上传大量资源,负责整理资源的同学因为空间不足,直接用快捷键方式整理视频资源OTZ,所以只能想办法通过.lnk文件获得文件的真实地址。
以下所有内容都来自网络,博主仅做了参考与总结。
.lnk文件格式解析
此处对lnk文件组成做一个大概介绍主旨是帮助了解如何从link文件中提取需要的信息
一个lnk文件包括一下几个模块:
模块 | 备注 |
---|---|
文件头(lnk file header) | 一些重要的文件信息。 |
Shell Item Id List 段 | 可选结构,由文件头中offset 0x14位置处的值来决定,0 bit值为1时,表示该lnk文件包含该结构。 |
文件位置信息段(File location info) | 可选结构,由文件头中offset 0x14位置处的值来决定,1 bit值为1时,表示该lnk文件包含该结构。 |
描述字符段(Description string) | 可选结构,由文件头中offset 0x14位置处的值来决定,2 bit值为1时,表示该lnk文件包含该结构。 |
相对路径段(Relative path string) | 可选结构,由文件头中offset 0x14位置处的值来决定,3 bit值为1时,表示该lnk文件包含该结构。 |
工作目录段(Working directory string) | 可选结构,由文件头中offset 0x14位置处的值来决定,4 bit值为1时,表示该lnk文件包含该结构。 |
命令行段(Command line string) | 可选结构,由文件头中offset 0x14位置处的值来决定,5 bit值为1时,表示该lnk文件包含该结构。 |
图标文件段(Icon filename string) | 可选结构,由文件头中offset 0x14位置处的值来决定,6 bit值为1时,表示该lnk文件包含该结构。 |
附加信息段(Extra stuff) | 里面的数据具体含义未知,有些里面会包含一些本机机器名称。 |
注意:
不是所有的模块都必须包含在内,但如果存在就要按上述的顺序排列。
以下我们详细了解需要用到的两个模块:
1. 文件头(lnk file header)
偏移 | 长度 | 类型 | 备注 |
---|---|---|---|
0x00 | 1 | dword | 总是为0000004CH,相当于字符"L",用于标识是否是个有效的.lnk文件。 |
0x04 | 16 | bytes | GUID,标识.lnk的唯一标识符,不排除以后MS对该字段有所修改。 |
0x14 | 1 | dword | flags标志,用来标识.lnk文件中有哪些可选属性,也就是哪些节是可选的。 |
0x18 | 1 | dword | 目标文件属性(是否只读、隐藏、系统文件、加密、临时…) |
0x1c | 1 | qword | 文件创建时间 |
0x24 | 1 | qword | 文件修改时间 |
0x2c | 1 | qword | 文件最后一次访问时间 |
0x34 | 1 | dword | 目标文件长度 |
0x38 | 1 | dword | 自定义图标个数 |
0x3c | 1 | dword | 目标文件执行时窗口显示方式(1-正常显示,2-最小化,3-最大化) |
0x40 | 1 | dword | 热键 |
0x44 | 2 | dword | 该字段未知,常为0 |
0x14
处16进制数的含义:
Bit | 所在位为1时表示 |
---|---|
0 | 包含shell item id list节,通过修改文件过滤掉该节,不影响.lnk执行目标文件,但影响其它功能。 |
1 | 指向文件或文件夹,如果此位为0表示指向其他。 |
2 | 存在描述字符串 |
3 | 存在相对路径 |
4 | 存在工作路径 |
5 | 存在命令行参数 |
6 | 存在自定义图标 |
0x18
处16进制数的含义:
Bit | 所在位为1时表示 |
---|---|
0 | 快捷方式所指目标文件有只读属性 |
1 | 快捷方式所指目标文件有隐藏属性 |
2 | 快捷方式所指目标文件是系统文件 |
3 | 快捷方式所指目标是卷标 |
4 | 快捷方式所指目标是文件夹 |
5 | 快捷方式所指目标文件上次存档后被改变过 |
6 | 快捷方式所指目标文件被加密 |
7 | 快捷方式所指目标文件属性为一般 |
8 | 快捷方式所指目标文件为临时 |
9 | 快捷方式所指目标文件为稀疏文件(sparse file) |
10 | 快捷方式所指目标文件有重分析点数据(reparse point) |
11 | 快捷方式所指目标文件被压缩 |
12 | 快捷方式所指目标文件脱机 |
2. 文件位置信息段(File location info)
偏移 | 长度 | 类型 | 备注 |
---|---|---|---|
0x00 | 1 | dword | 该节总长度,该值的修改,会影响.lnk文件执行或命令参数混乱失效。 |
0x04 | 1 | dword | 固定为0x1c,指明为该节长度,该值可任意修改。 |
0x08 | 1 | dword | flags标志,指示文件在哪些卷有效,例如是本地卷标还是网络卷标。 |
0x0c | 1 | dword | 固定为0x1c,本地卷信息表偏移。 |
0x10 | 1 | dword | 指明目标文件路径相对本节头部的偏移,但.lnk文件执行目标程序并不依赖于该值指向的路径是否正确。 |
0x14 | 1 | dword | 网络卷信息表偏移,如果flags标记含有网络卷的话,否则为0。 |
0x18 | 1 | dword | 剩余偏移路径,一般都指向本节的末尾,也就是节总长度减1的值,当该值指向的位置为0时(大多数情况下都是0),该值可任意修改。 |
0x08
偏移flags 具体含义:
- 如果目标文件是本地文件,那么文件名称 = 本地路径信息+剩余偏移路径
- 如果目标文件是网络文件,那么文件名称 = 网络卷中共享名称+剩余偏移路径
所以,File location info节之后的数据是,本地卷信息表,及网络卷信息表。
1. 本地卷信息表结构
偏移 | 长度 | 类型 | 备注 |
---|---|---|---|
0x00 | 1 | dword | 本地卷信息表的长度,该值可任意修改。 |
0x04 | 1 | dword | 卷类型,该值可以任意修改。 |
0x08 | 1 | dword | 标识卷序列号,2byte一组,该值可任意修改。 |
0x0c | 1 | dword | 卷名称的偏移,固定为0x10,该值可以任意修改。 |
0x10 | 可变长度 | 卷名称,本地路径信息,其大小由该表总长度决定。 |
2. 网络卷信息表结构
偏移 | 长度 | 类型 | 备注 |
---|---|---|---|
0x00 | 1 | dword | 网络卷信息表的长度,该值可任意修改。 |
0x04 | 1 | dword | 固定为0x2 |
0x08 | 1 | dword | 固定为0x14 |
0x0c | 1 | dword | 固定为0 |
0x10 | 可变长度 | 固定为0x20000 | |
0x10 | 可变长度 | 网络共享名 |
注意:
八个比特(Bit)称为一个字节(Byte),两个字节称为一个字(Word),两个字称为一个双字(Dword),两个双字称为一个四字(Qword)。
代码
private void parseLink(File f) throws FileNotFoundException, IOException {
FileInputStream fin = new FileInputStream(f);
byte[] link = new byte[(int)f.length()];
//读取文件中的内容到link[]数组
fin.read(link);
fin.close();
// 判断当前文件是否为快捷方式
if(!isLnkFile(link)){
return;
}
// 获得flags信息
byte flags = link[0x14];
int shell_len = 0;
// 0000 0000 xxxx xxxx & 0000 0000 0000 0001(判断是否包含shell item id list段)
if((flags & 0x1) > 0) {
// 如果存在,则获取shell item id list段的总长度,加2是为了将link[0x4c]本身的长度计算在内
shell_len = bytes2short(link,0x4c) + 2;
}
// 获得文件位置信息段的开始位置=shell item id list段的开始位置+shell item id list段的总长度
int file_start = 0x4c + shell_len;
// 获取本地路径信息的偏移
int local_sys_off = link[file_start + 0x10] + file_start;
String real_file = getNullDelimitedString(link, local_sys_off);
System.out.println(real_file);
}
private boolean isLnkFile(byte[] link) {
if (link[0x00]== 0x4c) {
// 76,L,0x4c代表lnk文件格式
return true;
}
return false;
}
/** * 将两个字节转换为short<br> * 注意,因为仅限英特尔操作系统,所以这是小端字节<br> */
private int bytes2short(byte[] bytes, int off) {
return bytes[off] | (bytes[off + 1] << 8);
}
/** * 获得从偏移位置off到以‘0’为结尾分割字符串 * @param bytes 源数组 * @param off 偏移位置 * @return 字符串 */
private String getNullDelimitedString(byte[] bytes, int off) {
int len = 0;
// 计算字符串占用数组的真实长度
while (true) {
if (bytes[off + len] == 0) {
break;
}
len++;
}
byte[] results = new byte[len];
for (int i = off, j = 0; i < off + len; i++, j++) {
results[j] = bytes[i];
}
try {
// 因为我是中文系统,所以设置了字符集GBK,否则中文路径会出现乱码
return new String(bytes, off, len, "GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
后话
GIT上也有一些项目实现了解析.lnk文件的功能,比如:
1. mslinks: 将.lnk封装对象可以通过get方法拿到想要的属性,但中文路径有乱码,如果愿意可以尝试自行修改源码;
2. jshortcut:用到了JNI,需要编译jshortcut.dll文件,没有尝试过,貌似同样存在中文乱码问题;