0x00 什么是binfmt-misc
binfmt-misc(Miscellaneous Binary Format)是Linux内核提供的一种类似Windows上文件关联的功能,但比文件关联更强大的是,它不仅可以根据文件后缀名判断,还可以根据文件内容(Magic Bytes)使用不同的程序打开。一个典型的使用场景就是:使用qemu
运行其它架构平台上的二进制文件。
本文以该场景为例,分析一下其具体的工作原理。
0x01 开启binfmt-misc
临时开启可以使用以下命令:
$ sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
这种方式重启后会失效,如果想长期生效,可以在/etc/fstab
文件中增加一行:
none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0
可以使用以下命令检查开启是否成功:
$ mount | grep binfmt_misc
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)
$ ls -l /proc/sys/fs/binfmt_misc
总用量 0
--w------- 1 root root 0 2月 5 22:55 register
-rw-r--r-- 1 root root 0 2月 5 22:55 status
0x02 在x86_64系统中运行arm64应用
先准备一个arm64架构的程序(可以使用go跨平台编译生成一个),执行后发现有报错:
bash: ./go-test:无法执行二进制文件: 可执行文件格式错误
现在,我们执行一下apt install qemu-user-binfmt
命令,然后再运行上面的arm64程序,发现能正常运行了。安装qemu-user-binfmt
后,会在/proc/sys/fs/binfmt_misc
目录下创建若干个文件,其中就有一个qemu-aarch64
。来看一下这个文件的内容:
$ cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/libexec/qemu-binfmt/aarch64-binfmt-P
flags: POC
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff
这个文件描述的是规则文件,第一行enabled
表示该规则启用;第二行interpreter /usr/libexec/qemu-binfmt/aarch64-binfmt-P
表示使用/usr/libexec/qemu-binfmt/aarch64-binfmt-P
来执行二进制文件;第三行flags: POC
表示运行的标志位,具体含义如下:
P
: 表示perserve-argv,这意味着在调用模拟器时,原始的参数(argv)将被保留。这对于某些程序在运行时需要知道它们自己的名称(即argv[0])的情况很有用O
: 表示offset,这意味着在启动模拟器之前,需要从二进制文件中读取一个偏移量。这个偏移量将作为模拟器的一个参数C
: 表示credentials,这意味着模拟器将使用与原始程序相同的用户ID和组ID运行。这有助于确保模拟器在运行时具有与原始程序相同的权限
第四行offset 0
表示从0
偏移值开始读取文件;第五行magic 7f454c460201010000000000000000000200b700
表示要匹配的魔术字节;mask ffffffffffffff00fffffffffffffffffeffffff
表示字节掩码,用来忽略掉文件中的一些不重要的字节。
可以看出,这条规则会使用/usr/libexec/qemu-binfmt/aarch64-binfmt-P
来执行arm64架构的二进制文件,而这个文件其实是一个软链,实际指向的是:/usr/bin/qemu-aarch64
。
0x03 手动创建执行规则
在上面的例子中,/proc/sys/fs/binfmt_misc/qemu-aarch64
文件是在安装qemu库的时候自动安装进去的。如果想手动创建一条规则,该怎么操作呢?
我们先将以下代码保存到文件main.go
中:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Program name:", os.Args[0])
if len(os.Args) > 1 {
fmt.Println("Arguments:")
for i, arg := range os.Args[1:] {
fmt.Printf("Arg %d: %s\n", i+1, arg)
}
} else {
fmt.Println("No arguments provided.")
}
}
使用命令:go build -o fake-runner ./main.go
进行编译,并将编译出来的fake-runner
拷贝到/usr/local/bin
目录下。
此时,我们需要向/proc/sys/fs/binfmt_misc/register
中按照:name:type:offset:magic:mask:interpreter:flags
的格式写入规则。
- name: 规则名
- type: 类型,取
E
(按扩展名匹配)或M
(按文件魔术字节匹配)之一 - offset: 当
type
为M
时生效,表示魔术字节的偏移值 - magic: 当
type
为E
时,表示要匹配的后缀名;当type
为M
时,表示16进制的魔术字节 - mask: 当
type
为M
时生效,表示魔术字节的掩码,与IP地址掩码类似 - interpreter: 解释器文件的绝对路径
- flags: 含义与上面的
flags
一致
假设我们想用fake-runner
打开以12344578
开头的文件,可以执行以下命令:
# echo ':binfmt-test:M::12345678::/usr/local/bin/fake-runner:P' > /proc/sys/fs/binfmt_misc/register
# cat /proc/sys/fs/binfmt_misc/binfmt-test enabled
interpreter /usr/local/bin/fake-runner
flags: P
offset 0
magic 3132333435363738
此命令需要在root权限下运行。
然后使用命令生成目标文件:
$ echo 12345678 > /tmp/test.txt
$ chmod 755 /tmp/test.txt
$ /tmp/test.txt hello
Program name: /usr/local/bin/fake-runner
Arguments:
Arg 1: /tmp/test.txt
Arg 2: /tmp/test.txt
Arg 3: hello
删除规则可以使用命令:echo -1 > /proc/sys/fs/binfmt_misc/binfmt-test
0x04 在x86_64系统中运行arm64架构的Docker镜像
现在我们用docker命令运行一个arm64的镜像:
$ docker run -it arm64v8/ubuntu bash
Unable to find image 'arm64v8/ubuntu:latest' locally
latest: Pulling from arm64v8/ubuntu
005e2837585d: Pull complete
Digest: sha256:ba545858745d6307f0d1064d0d25365466f78d02f866cf4efb9e1326a4c196ca
Status: Downloaded newer image for arm64v8/ubuntu:latest
standard_init_linux.go:207: exec user process caused "no such file or directory"
通过一番探索之后,发现只要执行下命令:apt install qemu-user-static
,再启动docker容器就正常了。执行这条命令会修改/usr/libexec/qemu-binfmt/aarch64-binfmt-P
文件的软链到/usr/bin/qemu-aarch64-static
。我们来看下qemu-aarch64
和qemu-aarch64-static
区别:
$ readelf -d /usr/bin/qemu-aarch64
Dynamic section at offset 0x3aee38 contains 37 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libz.so.1]
0x0000000000000001 (NEEDED) 共享库:[librt.so.1]
0x0000000000000001 (NEEDED) 共享库:[libcapstone.so.4]
0x0000000000000001 (NEEDED) 共享库:[libglib-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libgnutls.so.30]
0x0000000000000001 (NEEDED) 共享库:[libgmodule-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libstdc++.so.6]
0x0000000000000001 (NEEDED) 共享库:[libm.so.6]
0x0000000000000001 (NEEDED) 共享库:[libgcc_s.so.1]
0x0000000000000001 (NEEDED) 共享库:[libpthread.so.0]
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000c (INIT) 0xab000
0x000000000000000d (FINI) 0x2a83ec
0x0000000000000019 (INIT_ARRAY) 0x35b8e0
0x000000000000001b (INIT_ARRAYSZ) 248 (bytes)
0x000000000000001a (FINI_ARRAY) 0x35b9d8
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x340
0x0000000000000005 (STRTAB) 0x2a608
0x0000000000000006 (SYMTAB) 0xa1f0
0x000000000000000a (STRSZ) 122726 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x3b00c8
0x0000000000000002 (PLTRELSZ) 11136 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0xa7f68
0x0000000000000007 (RELA) 0x4b2e0
0x0000000000000008 (RELASZ) 380040 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) 标志: NOW PIE
0x000000006ffffffe (VERNEED) 0x4b070
0x000000006fffffff (VERNEEDNUM) 7
0x000000006ffffff0 (VERSYM) 0x4856e
0x000000006ffffff9 (RELACOUNT) 15807
0x0000000000000000 (NULL) 0x0
$ readelf -d /usr/bin/qemu-aarch64-static
There is no dynamic section in this file.
可以看出,qemu-aarch64-static是没有动态库依赖的,也就是说,docker必须使用静态编译的qemu
才能工作。通过这种方式,可以实现在x86_64机器上编译跨架构镜像的目的。
0x05 跨架构编译Docker镜像
要支持多架构,需要开启Docker的实验功能,开启方式如下:
在文件/etc/docker/daemon.json
中添加如下配置
{
"experimental": true
}
然后使用sysemcrtl restart docker
命令重启Docker服务。
$ docker info | grep -i 'experimental'
Experimental: true
当看到以上输出时,就表示实验功能已开启。
编写以下Dockerfile:
FROM ubuntu:20.04
RUN set -ex && apt update
然后使用以下命令编译arm64镜像
$ sudo docker build --platform linux/arm64 -t ubuntu .
$ sudo docker run -it ubuntu bash
root@616a3dd3a915:/# uname -a
Linux 616a3dd3a915 5.15.34-amd64-desktop #2 SMP Mon May 16 16:31:30 CST 2022 aarch64 aarch64 aarch64 GNU/Linux
因此,使用--platform linux/arm64
参数就可以编译出arm64架构的镜像。
0x06 在Linux上运行Windows可执行文件
使用binfmt-misc
机制可以支持直接在Linux上运行Windows的exe文件,这是通过wine来实现的。
$ cat /proc/sys/fs/binfmt_misc/DOSWin
enabled
interpreter /usr/bin/wine
flags:
offset 0
magic 4d5a
$ ls -l /usr/bin/wine
lrwxrwxrwx 1 root root 19 10月 8 18:09 /usr/bin/wine -> deepin-wine6-stable
deepin-wine6-stable其实是一个bash脚本:
#!/bin/bash
name=${0##*/}
bindir=/usr/lib/$name
wine32=/opt/$name/bin/wine
wine64=/opt/$name/bin/wine64
if test -x $wine32 -a "$WINEARCH" != "win64"; then
wine=$wine32
elif test -x $wine64; then
wine=$wine64
if [ "$(dpkg --print-architecture)" = "amd64" -a "$(dpkg --print-foreign-architectures | grep -cx "i386")" -ne 1 ]; then
echo "it looks like multiarch needs to be enabled. as root, please"
echo "execute \"dpkg --add-architecture i386 && apt-get update &&"
echo "apt-get install $(echo $name | sed s/wine/wine32/)\""
fi
else
echo "error: unable to find wine executable. this shouldn't happen."
exit 1
fi
if test -z "$WINEPREFIX"; then
if test "$wine" = "$wine64"; then
wineprefix=$HOME/.wine64
else
wineprefix=$HOME/.wine
fi
else
wineprefix=$WINEPREFIX
fi
if test -z "$WINELOADER"; then
wineloader=$wine
else
wineloader=$WINELOADER
fi
if test -z "$WINEDEBUG"; then
winedebug=-all
else
winedebug=$WINEDEBUG
fi
runtime_path=/opt/deepinwine/runtime-i386
export LD_LIBRARY_PATH="/opt/$name/lib:/opt/$name/lib64:$LD_LIBRARY_PATH"
export WINEDLLPATH=/opt/$name/lib:/opt/$name/lib64
# 32位wine需要指定32位runtime的路径
if [ -f "$runtime_path/init_runtime.sh" -a "$wine" = "$wine32" ];then
source "$runtime_path/init_runtime.sh"
PE_FILE="$1"
if [[ "$1" == *".exe" ]]; then
PE_FILE=${PE_FILE//\\/\/}
drive=${PE_FILE:0:2}
if [[ ${drive} == "c:"* || ${drive} == "C:"* ]]; then
PE_FILE=${wineprefix}/drive_c${PE_FILE:2}
fi
fi
init_runtime
if [ -f "$PE_FILE" ];then
#only 32 bit application need config this envs
if file "$PE_FILE" | grep -q -e "PE32 "; then
init_32bit_config
fi
fi
export WINELOADERNOEXEC=1
winepreloader=/opt/$name/bin/wine-preloader
WINEPREFIX=$wineprefix WINELOADER=$wineloader WINEDEBUG=$winedebug $winepreloader $wine "$@"
else
WINEPREFIX=$wineprefix WINELOADER=$wineloader WINEDEBUG=$winedebug $wine "$@"
fi
因此,直接在命令行中输入一个exe文件路径,例如扫雷游戏,就会看到系统打开了扫雷游戏界面。
0x07 总结
binfmt-misc提供了灵活的文件关联机制,使得部分无法直接执行的程序可以像普通Linux程序一样直接运行起来(如:跨架构程序、Windows exe等)。