Linux中的binfmt-misc原理分析

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: 当typeM时生效,表示魔术字节的偏移值
  • magic: 当typeE时,表示要匹配的后缀名;当typeM时,表示16进制的魔术字节
  • mask: 当typeM时生效,表示魔术字节的掩码,与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-aarch64qemu-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等)。

分享