一、binfmt_misc介绍
binfmt_misc是Linux内核的一项功能,其使得内核可识别任意类型的可执行文件格式并传递至特定的用户空间应用程序,如模拟器和虚拟机。例如在装有qemu的x86-64的Linux中可以直接执行ARM64程序不需要指明使用qemu。
二、binfmt_misc机制分析
要使用binfmt_misc机制,首先需要挂载binfmt_misc
1
| mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
在/proc/sys/fs/binfmt_misc文件夹下会有一些已经注册的配置文件,分别用来处理各种个样的二进制程序
配置文件中的内容一般有interpreter 文件交给哪个处理程序
、magic 匹配文件头
、mask 匹配文件头时使用的掩码
type应为E或M类型
- 若类型为E,则可执行文件格式由其文件扩展名进行识别:magic是与二进制格式相关联的文件扩展名;此时忽略offset和mask参数。
- 若类型为M,则可执行文件格式通过文件中的offset的(默认为0)magic数字识别;mask是全默认为 0xFF的bitmask,其用于指示数字中存在意义的二进制位。
flags: 这些标志控制解析器的行为和操作方式。下面是一些常见的 flags 标志及其作用:
- O:覆盖(Override)标志。当多个解析器的匹配规则冲突时,使用具有此标志的解析器进行匹配和执行。
- E:可执行文件标志。指定解析器用于执行可执行文件的功能。
- F:开启自动刷新标志。当启用此标志时,每次访问 register 文件时都会重新加载解析器配置。
- C:关闭自动刷新标志。当关闭此标志时,解析器配置只在系统启动时加载一次,之后不会自动刷新。
- B:启用解析器的特权执行。这将允许使用具有特权的解析器执行文件。
- M:启用魔数验证标志。指定解析器在匹配时必须验证魔数。
注意事项:offset+size(magic) 必须小于 128,解释器字符串不得超过 127 个字符
三、注册一个新的配置文件
使用echo ":name:type:offset:magic:mask:interpreter:flags" > /proc/sys/fs/binfmt_misc/register
的格式来注册配置文件,例如我们可以注册一个这样的配置文件
1
| echo ":hello:M:10:123456:\xff\xff\xff\xff\xff\xff:/bin/cat:O" > /proc/sys/fs/binfmt_misc/register
|
并使用这样的文件内容来触发
要取消注册一个配置文件,可以执行echo -1 > /proc/sys/fs/binfmt_misc/xxx
我们还可以使用扩展名匹配的方式
echo ":hello:E::log::/bin/cat:O" > /proc/sys/fs/binfmt_misc/register
四、binfmt_misc的利用
CGI程序调试
在调试CGI程序时,一般需要先用gdb附加到类似于httpd的主进程上,然后慢慢的跟踪子进程的创建,最后一步一步的才能调试到CGI程序;又或者是对CGI程序打补丁,加入sleep函数给gdb附加创造一个时机。有了binfmt_misc机制,CGI调试可以变得非常简单,对于带有.cgi
后缀的,我们可以直接使用扩展名匹配的方式,将.cgi
程序交给gdb来启动
为了过滤出想要调试的具体cgi程序,我们需要写一个中间代理程序。
1
| echo ":cgi_test:E::cgi::/mnt/hgfs/works/share/debug:O" > /proc/sys/fs/binfmt_misc/register
|
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
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>
int main(int argc, char *argv[]) { char buf[100]; char tmp_exec[200]; char *gdb_path = "/usr/bin/gdb"; char *target_config = "test2.cgi";
char **new_argv = calloc(argc + 10,sizeof(char *)); if (!new_argv) { perror("calloc failed"); exit(EXIT_FAILURE); } char *name = strrchr(argv[1],'/'); if (name) { name = name + 1; } else { name = argv[1]; } snprintf(tmp_exec,200,"/tmp/%s.tmp",name); snprintf(buf,100,"cp %s %s",argv[1],tmp_exec); system(buf); if (argc > 1 && strstr(argv[1], target_config)) { new_argv[0] = "/usr/bin/gdb"; new_argv[1] = "-x"; new_argv[2] = "/mnt/hgfs/works/share/script.gdb"; new_argv[3] = "--args"; new_argv[4] = tmp_exec; for (int i = 2; i < argc; i++) { new_argv[i + 3] = argv[i]; } execv(gdb_path, new_argv); } else { new_argv[0] = tmp_exec; for (int i = 2; i < argc; i++) { new_argv[i-1] = argv[i]; } execv(tmp_exec, new_argv); }
perror("execv failed"); free(new_argv); return EXIT_FAILURE; }
|
中间代理程序会检查要执行的程序,如果是test2.cgi
,那么就会使用/usr/bin/gdb -x /mnt/hgfs/works/share/script.gdb --args /tmp/test2.cgi.tmp xxxx
,这里我们需要拷贝一份CGI程序,同时后缀要加上.tmp防止gdb启动CGI时再次匹配上.cgi
后缀进入一个死循环。
在实际调试时,我们应该使用gdbserver而不是gdb来启动CGI,因为CGI的标准输入输出不在终端,因此稍微修改一下
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
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>
int main(int argc, char *argv[]) { char buf[100]; char tmp_exec[200]; char *gdb_path = "/usr/bin/gdbserver"; char *target_config = "test2.cgi";
char **new_argv = calloc(argc + 10,sizeof(char *)); if (!new_argv) { perror("calloc failed"); exit(EXIT_FAILURE); } char *name = strrchr(argv[1],'/'); if (name) { name = name + 1; } else { name = argv[1]; } snprintf(tmp_exec,200,"/tmp/%s.tmp",name); snprintf(buf,100,"cp %s %s",argv[1],tmp_exec); system(buf); if (argc > 1 && strstr(argv[1], target_config)) { new_argv[0] = "/usr/bin/gdbserver"; new_argv[1] = "0.0.0.0:1234"; new_argv[2] = tmp_exec; for (int i = 2; i < argc; i++) { new_argv[i + 1] = argv[i]; } execv(gdb_path, new_argv); } else { new_argv[0] = tmp_exec; for (int i = 2; i < argc; i++) { new_argv[i-1] = argv[i]; } execv(tmp_exec, new_argv); }
perror("execv failed"); free(new_argv); return EXIT_FAILURE; }
|
CGI被httpd等程序启动时,会自动进入gdbserver监听1234端口等待调试。
qemu-user模拟下的CGI调试
以模拟调试Cisco RV340的CGI为例,使用chroot + qemu-arm-static基本可以将根文件系统模拟起来,虽然有报错但是http服务可以正常启动。
对qemu-arm的binfmt_misc配置文件首先进行取消注册echo -1 > /proc/sys/fs/binfmt_misc/qemu-arm
,然后注册新的配置,将ARM程序交给中间代理程序debug进行处理
1 2 3 4
| echo ':qemu-arm:M:0:\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff \xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff: /debug:OC' > /proc/sys/fs/binfmt_misc/register
|
中间代理程序debug如下
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
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>
int main(int argc, char *argv[]) { char *qemu_path = "/qemu-arm-static"; char *target_config = "/www/cgi-bin/upload.cgi"; char **new_argv = malloc((argc + 1) * sizeof(char *)); if (!new_argv) { perror("malloc failed"); exit(EXIT_FAILURE); } new_argv[0] = qemu_path; if (argc > 1 && strcmp(argv[1], target_config) == 0) { new_argv[1] = "-g"; new_argv[2] = "1234"; for (int i = 1; i < argc; i++) { new_argv[i + 2] = argv[i]; } new_argv[argc + 2] = NULL; execv(qemu_path, new_argv); } else { for (int i = 1; i < argc; i++) { new_argv[i] = argv[i]; } new_argv[argc] = NULL; execv(qemu_path, new_argv); } perror("execv failed"); free(new_argv); return EXIT_FAILURE; }
|
代理程序过滤了/www/cgi-bin/upload.cgi
,如果匹配到这个CGI,就会执行/qemu-arm-static -g 1234 /www/cgi-bin/upload.cgi
进入调试模式。
此时可以直接使用gdb-multiarch
进行远程附加,可以看到环境变量传递这些一个不漏,比手动执行CGI然后构造REQUEST_METHOD=POST QUERY_STRING
的方式要更加的方便快捷,是服务器真实传递给CGI的参数。
五、小结
binfmt-misc机制可以匹配任意格式的文件并将文件传递给注册的处理程序进行处理。我们可以借助binfmt-misc机制来对一些特定的程序进行处理,比如加入调试、监听程序的打开等操作。
六、参考
binfmt_misc wiki
binfmt_misc CSDN