0x00 什么是binfmt-misc binfmt-misc(Miscellaneous Binary Format)是Linux内核提供的一种类似Windows上文件关联的功能,但比文件关联更强大的是,它不仅可以根据文件后缀名判断,还可以根据文件内容(Magic Bytes)使用不同的程序打开。一个典型的使用场景就是:使用qemu运行其它架构平台上的二进制文件。
本文以该场景为例,分析一下其具体的工作原理。
0x01 开启binfmt-misc 临时开启可以使用以下命令:
1 $ sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
这种方式重启后会失效,如果想长期生效,可以在/etc/fstab文件中增加一行:
1 none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0
可以使用以下命令检查开启是否成功:
1 2 3 4 5 6 $ 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跨平台编译生成一个),执行后发现有报错:
1 bash: ./go-test:无法执行二进制文件: 可执行文件格式错误
现在,我们执行一下apt install qemu-user-binfmt命令,然后再运行上面的arm64程序,发现能正常运行了。安装qemu-user-binfmt后,会在/proc/sys/fs/binfmt_misc目录下创建若干个文件,其中就有一个qemu-aarch64。来看一下这个文件的内容:
1 2 3 4 5 6 7 $ 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中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "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开头的文件,可以执行以下命令:
1 2 3 4 5 6 interpreter /usr/local/bin/fake-runner flags: P offset 0 magic 3132333435363738
此命令需要在root权限下运行。
然后使用命令生成目标文件:
1 2 3 4 5 6 7 8 $ 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的镜像:
1 2 3 4 5 6 7 $ 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区别:
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 $ 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中添加如下配置
1 2 3 { "experimental" : true }
然后使用sysemcrtl restart docker命令重启Docker服务。
1 2 $ docker info | grep -i 'experimental' Experimental: true
当看到以上输出时,就表示实验功能已开启。
编写以下Dockerfile:
1 2 3 FROM ubuntu:20.04 RUN set -ex && apt update
然后使用以下命令编译arm64镜像
1 2 3 4 $ 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
因此,使用--platform linux/arm64参数就可以编译出arm64架构的镜像。
0x06 在Linux上运行Windows可执行文件 使用binfmt-misc机制可以支持直接在Linux上运行Windows的exe文件,这是通过wine来实现的。
1 2 3 4 5 6 7 8 $ 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脚本:
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 70 71 72 73 74 75 #!/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 /lib64if [ -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 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等)。