Linux中的binfmt-misc原理分析

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
COPY

这种方式重启后会失效,如果想长期生效,可以在/etc/fstab文件中增加一行:

  1. none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0
COPY

可以使用以下命令检查开启是否成功:

  1. $ mount | grep binfmt_misc
  2. binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)
  3. $ ls -l /proc/sys/fs/binfmt_misc
  4. 总用量 0
  5. --w------- 1 root root 0 2 5 22:55 register
  6. -rw-r--r-- 1 root root 0 2 5 22:55 status
COPY

0x02 在x86_64系统中运行arm64应用

先准备一个arm64架构的程序(可以使用go跨平台编译生成一个),执行后发现有报错:

  1. bash: ./go-test:无法执行二进制文件: 可执行文件格式错误
COPY

现在,我们执行一下apt install qemu-user-binfmt命令,然后再运行上面的arm64程序,发现能正常运行了。安装qemu-user-binfmt后,会在/proc/sys/fs/binfmt_misc目录下创建若干个文件,其中就有一个qemu-aarch64。来看一下这个文件的内容:

  1. $ cat /proc/sys/fs/binfmt_misc/qemu-aarch64
  2. enabled
  3. interpreter /usr/libexec/qemu-binfmt/aarch64-binfmt-P
  4. flags: POC
  5. offset 0
  6. magic 7f454c460201010000000000000000000200b700
  7. mask ffffffffffffff00fffffffffffffffffeffffff
COPY

这个文件描述的是规则文件,第一行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. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func main() {
  7. fmt.Println("Program name:", os.Args[0])
  8. if len(os.Args) > 1 {
  9. fmt.Println("Arguments:")
  10. for i, arg := range os.Args[1:] {
  11. fmt.Printf("Arg %d: %s\n", i+1, arg)
  12. }
  13. } else {
  14. fmt.Println("No arguments provided.")
  15. }
  16. }
COPY

使用命令: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开头的文件,可以执行以下命令:

  1. # echo ':binfmt-test:M::12345678::/usr/local/bin/fake-runner:P' > /proc/sys/fs/binfmt_misc/register
  2. # cat /proc/sys/fs/binfmt_misc/binfmt-test enabled
  3. interpreter /usr/local/bin/fake-runner
  4. flags: P
  5. offset 0
  6. magic 3132333435363738
COPY

此命令需要在root权限下运行。

然后使用命令生成目标文件:

  1. $ echo 12345678 > /tmp/test.txt
  2. $ chmod 755 /tmp/test.txt
  3. $ /tmp/test.txt hello
  4. Program name: /usr/local/bin/fake-runner
  5. Arguments:
  6. Arg 1: /tmp/test.txt
  7. Arg 2: /tmp/test.txt
  8. Arg 3: hello
COPY

删除规则可以使用命令:echo -1 > /proc/sys/fs/binfmt_misc/binfmt-test

0x04 在x86_64系统中运行arm64架构的Docker镜像

现在我们用docker命令运行一个arm64的镜像:

  1. $ docker run -it arm64v8/ubuntu bash
  2. Unable to find image 'arm64v8/ubuntu:latest' locally
  3. latest: Pulling from arm64v8/ubuntu
  4. 005e2837585d: Pull complete
  5. Digest: sha256:ba545858745d6307f0d1064d0d25365466f78d02f866cf4efb9e1326a4c196ca
  6. Status: Downloaded newer image for arm64v8/ubuntu:latest
  7. standard_init_linux.go:207: exec user process caused "no such file or directory"
COPY

通过一番探索之后,发现只要执行下命令:apt install qemu-user-static,再启动docker容器就正常了。执行这条命令会修改/usr/libexec/qemu-binfmt/aarch64-binfmt-P文件的软链到/usr/bin/qemu-aarch64-static。我们来看下qemu-aarch64qemu-aarch64-static区别:

  1. $ readelf -d /usr/bin/qemu-aarch64
  2. Dynamic section at offset 0x3aee38 contains 37 entries:
  3. 标记 类型 名称/值
  4. 0x0000000000000001 (NEEDED) 共享库:[libz.so.1]
  5. 0x0000000000000001 (NEEDED) 共享库:[librt.so.1]
  6. 0x0000000000000001 (NEEDED) 共享库:[libcapstone.so.4]
  7. 0x0000000000000001 (NEEDED) 共享库:[libglib-2.0.so.0]
  8. 0x0000000000000001 (NEEDED) 共享库:[libgnutls.so.30]
  9. 0x0000000000000001 (NEEDED) 共享库:[libgmodule-2.0.so.0]
  10. 0x0000000000000001 (NEEDED) 共享库:[libstdc++.so.6]
  11. 0x0000000000000001 (NEEDED) 共享库:[libm.so.6]
  12. 0x0000000000000001 (NEEDED) 共享库:[libgcc_s.so.1]
  13. 0x0000000000000001 (NEEDED) 共享库:[libpthread.so.0]
  14. 0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
  15. 0x000000000000000c (INIT) 0xab000
  16. 0x000000000000000d (FINI) 0x2a83ec
  17. 0x0000000000000019 (INIT_ARRAY) 0x35b8e0
  18. 0x000000000000001b (INIT_ARRAYSZ) 248 (bytes)
  19. 0x000000000000001a (FINI_ARRAY) 0x35b9d8
  20. 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
  21. 0x000000006ffffef5 (GNU_HASH) 0x340
  22. 0x0000000000000005 (STRTAB) 0x2a608
  23. 0x0000000000000006 (SYMTAB) 0xa1f0
  24. 0x000000000000000a (STRSZ) 122726 (bytes)
  25. 0x000000000000000b (SYMENT) 24 (bytes)
  26. 0x0000000000000015 (DEBUG) 0x0
  27. 0x0000000000000003 (PLTGOT) 0x3b00c8
  28. 0x0000000000000002 (PLTRELSZ) 11136 (bytes)
  29. 0x0000000000000014 (PLTREL) RELA
  30. 0x0000000000000017 (JMPREL) 0xa7f68
  31. 0x0000000000000007 (RELA) 0x4b2e0
  32. 0x0000000000000008 (RELASZ) 380040 (bytes)
  33. 0x0000000000000009 (RELAENT) 24 (bytes)
  34. 0x000000000000001e (FLAGS) BIND_NOW
  35. 0x000000006ffffffb (FLAGS_1) 标志: NOW PIE
  36. 0x000000006ffffffe (VERNEED) 0x4b070
  37. 0x000000006fffffff (VERNEEDNUM) 7
  38. 0x000000006ffffff0 (VERSYM) 0x4856e
  39. 0x000000006ffffff9 (RELACOUNT) 15807
  40. 0x0000000000000000 (NULL) 0x0
  41. $ readelf -d /usr/bin/qemu-aarch64-static
  42. There is no dynamic section in this file.
COPY

可以看出,qemu-aarch64-static是没有动态库依赖的,也就是说,docker必须使用静态编译的qemu才能工作。通过这种方式,可以实现在x86_64机器上编译跨架构镜像的目的。

0x05 跨架构编译Docker镜像

要支持多架构,需要开启Docker的实验功能,开启方式如下:

在文件/etc/docker/daemon.json中添加如下配置

  1. {
  2. "experimental": true
  3. }
COPY

然后使用sysemcrtl restart docker命令重启Docker服务。

  1. $ docker info | grep -i 'experimental'
  2. Experimental: true
COPY

当看到以上输出时,就表示实验功能已开启。

编写以下Dockerfile:

  1. FROM ubuntu:20.04
  2. RUN set -ex && apt update
COPY

然后使用以下命令编译arm64镜像

  1. $ sudo docker build --platform linux/arm64 -t ubuntu .
  2. $ sudo docker run -it ubuntu bash
  3. root@616a3dd3a915:/# uname -a
  4. Linux 616a3dd3a915 5.15.34-amd64-desktop #2 SMP Mon May 16 16:31:30 CST 2022 aarch64 aarch64 aarch64 GNU/Linux
COPY

因此,使用--platform linux/arm64参数就可以编译出arm64架构的镜像。

0x06 在Linux上运行Windows可执行文件

使用binfmt-misc机制可以支持直接在Linux上运行Windows的exe文件,这是通过wine来实现的。

  1. $ cat /proc/sys/fs/binfmt_misc/DOSWin
  2. enabled
  3. interpreter /usr/bin/wine
  4. flags:
  5. offset 0
  6. magic 4d5a
  7. $ ls -l /usr/bin/wine
  8. lrwxrwxrwx 1 root root 19 10 8 18:09 /usr/bin/wine -> deepin-wine6-stable
COPY

deepin-wine6-stable其实是一个bash脚本:

  1. #!/bin/bash
  2. name=${0##*/}
  3. bindir=/usr/lib/$name
  4. wine32=/opt/$name/bin/wine
  5. wine64=/opt/$name/bin/wine64
  6. if test -x $wine32 -a "$WINEARCH" != "win64"; then
  7. wine=$wine32
  8. elif test -x $wine64; then
  9. wine=$wine64
  10. if [ "$(dpkg --print-architecture)" = "amd64" -a "$(dpkg --print-foreign-architectures | grep -cx "i386")" -ne 1 ]; then
  11. echo "it looks like multiarch needs to be enabled. as root, please"
  12. echo "execute \"dpkg --add-architecture i386 && apt-get update &&"
  13. echo "apt-get install $(echo $name | sed s/wine/wine32/)\""
  14. fi
  15. else
  16. echo "error: unable to find wine executable. this shouldn't happen."
  17. exit 1
  18. fi
  19. if test -z "$WINEPREFIX"; then
  20. if test "$wine" = "$wine64"; then
  21. wineprefix=$HOME/.wine64
  22. else
  23. wineprefix=$HOME/.wine
  24. fi
  25. else
  26. wineprefix=$WINEPREFIX
  27. fi
  28. if test -z "$WINELOADER"; then
  29. wineloader=$wine
  30. else
  31. wineloader=$WINELOADER
  32. fi
  33. if test -z "$WINEDEBUG"; then
  34. winedebug=-all
  35. else
  36. winedebug=$WINEDEBUG
  37. fi
  38. runtime_path=/opt/deepinwine/runtime-i386
  39. export LD_LIBRARY_PATH="/opt/$name/lib:/opt/$name/lib64:$LD_LIBRARY_PATH"
  40. export WINEDLLPATH=/opt/$name/lib:/opt/$name/lib64
  41. # 32位wine需要指定32位runtime的路径
  42. if [ -f "$runtime_path/init_runtime.sh" -a "$wine" = "$wine32" ];then
  43. source "$runtime_path/init_runtime.sh"
  44. PE_FILE="$1"
  45. if [[ "$1" == *".exe" ]]; then
  46. PE_FILE=${PE_FILE//\\/\/}
  47. drive=${PE_FILE:0:2}
  48. if [[ ${drive} == "c:"* || ${drive} == "C:"* ]]; then
  49. PE_FILE=${wineprefix}/drive_c${PE_FILE:2}
  50. fi
  51. fi
  52. init_runtime
  53. if [ -f "$PE_FILE" ];then
  54. #only 32 bit application need config this envs
  55. if file "$PE_FILE" | grep -q -e "PE32 "; then
  56. init_32bit_config
  57. fi
  58. fi
  59. export WINELOADERNOEXEC=1
  60. winepreloader=/opt/$name/bin/wine-preloader
  61. WINEPREFIX=$wineprefix WINELOADER=$wineloader WINEDEBUG=$winedebug $winepreloader $wine "$@"
  62. else
  63. WINEPREFIX=$wineprefix WINELOADER=$wineloader WINEDEBUG=$winedebug $wine "$@"
  64. fi
COPY

因此,直接在命令行中输入一个exe文件路径,例如扫雷游戏,就会看到系统打开了扫雷游戏界面。

0x07 总结

binfmt-misc提供了灵活的文件关联机制,使得部分无法直接执行的程序可以像普通Linux程序一样直接运行起来(如:跨架构程序、Windows exe等)。

分享

Gitalking ...