
服务正常运行同时验证权限限制确实生效前几篇教程已经让 ARM64 服务随系统启动、在进程退出后自动恢复并为日志增加了阈值轮转。但目前 BusyBox init 直接以 root 启动debug_demo。程序只需要写自己的日志却拥有整个系统的 root 权限一旦程序出现缺陷影响范围也会随之扩大。这篇继续使用 Ubuntu、QEMU 和 Buildroot不需要开发板。我们为服务创建专用的非特权账户让程序在启动时从 root 降权再验证两件事服务仍能写自己的日志目录同时无法读取一个 root-only 测试文件。一、这次要验证什么本文承接《日志达到阈值后轮转确认程序继续写入正确文件》。沿用 Buildroot 2025.02.8、QEMU ARM64、BusyBox init 和外部软件包debug-demo。本篇只处理一个问题降低服务运行权限后服务能否继续正常运行同时确实被文件权限限制预期的权限边界如下对象所有者和权限服务账户预期能力/usr/bin/debug_demoroot 所有的可执行程序可以执行不可修改程序文件/etc/debug-demo.confroot 所有普通用户可读可以读取启动配置不可修改配置/var/log/debug-demodebug-demo:debug-demo0750可以在自己的目录中创建和写入日志/tmp/debug-demo-permission-proberoot:root0600不可读取服务仍由 BusyBox init 监督/etc/inittab不另加第二条启动路径::respawn:/usr/bin/debug_demo --serviceinit 以 root 启动程序程序完成必要的账户查询后清空附加组并切换到debug-demo用户和主组。此后打开配置和日志文件、写入服务日志都在非 root 身份下进行。若程序退出init 仍按respawn规则重启它本篇不改变监督策略。二、为什么要同时验证“能写”和“不能读”只看到进程名称或 UID 改变并不能证明服务所需的功能仍然正常。反过来只看到日志继续增长也不能证明它已经失去 root 权限。因此本篇使用两个独立证据检查/proc/PID/status中的 UID/GID确认服务进程的 real/effective/saved IDs 都是非零的专用账户。让应用在降权后尝试读取 root-only 测试文件并把EACCES结果写入服务日志同时观察它是否仍可持续追加自己的日志。root-only 文件由 root 在 QEMU 内创建权限设为0600。程序在切换身份后才尝试读取因此若日志报告拒绝访问而服务日志仍持续增长就同时验证了权限限制和服务可用性。这是一项传统 Unix 文件权限实验不等于完整安全加固。它不讨论 Linux capabilities、seccomp、SELinux、只读根文件系统或设备节点权限。三、环境与位置约定项目本文沿用的设置开发主机Ubuntu 20.04 x86_64 虚拟机Buildroot2025.02.8~/buildroot-2025.02.8输出目录~/buildroot-output-arm64-demo外部目录~/buildroot-external-demo标识ARM64_DEMO目标系统QEMU ARM64BusyBox init服务程序/usr/bin/debug_demo --service服务账户debug-demo配置文件/etc/debug-demo.conf日志目录/var/log/debug-demo活动日志/var/log/debug-demo/debug-demo.log默认命令在Ubuntu 主机执行标注QEMU 目标端的命令才在 ARM64 系统内执行。不要在 Windows PowerShell 执行本文的 Bash 命令。本篇把日志从之前的/var/log/debug-demo.log移到专属目录/var/log/debug-demo/debug-demo.log。旧日志路径不会自动迁移本次重建镜像中的实验从新路径开始。程序包的用户表使用 Buildroot 自动分配系统 UID/GID不把某个数字 UID 写死在源码里。四、关机并备份如果上一轮 QEMU 仍在运行先在QEMU 目标端执行poweroff等待系统关机。如果 QEMU 进程仍未退出再按CtrlA松开后按X。确认 QEMU 已释放可写镜像再从 Ubuntu 主机备份并编辑外部目录。在Ubuntu 主机执行exportDEMO_OUT$HOME/buildroot-output-arm64-demoexportPERM_BACKUP$HOME/debug-demo-before-permissions-$(date%Y%m%d-%H%M%S)(set-eutest-f$DEMO_OUT/.configtest-f$HOME/buildroot-external-demo/src/debug-demo/debug_demo.cmkdir$PERM_BACKUPcp-a$HOME/buildroot-external-demo$PERM_BACKUP/cp-p$DEMO_OUT/.config$PERM_BACKUP/buildroot.configprintfBackup: %s\n$PERM_BACKUP)echo$?预期返回0。保存打印出的备份路径如果检查或复制失败先停止并排查。核对当前服务仍由 BusyBox init 的一条respawn项启动且软件包、兼容脚本仍来自前文grep-nFx::respawn:/usr/bin/debug_demo --service\$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittabcat$HOME/buildroot-external-demo/package/debug-demo/S99debug-demogrep-E^BR2_(INIT_BUSYBOX|PACKAGE_DEBUG_DEMO)$DEMO_OUT/.config监督项应只有一条两个 Buildroot 选项应为y。本篇会修改应用及其包规则但不应把旧的后台启动路径重新加回来。本次检查确认overlay 中的respawn项位于第 42 行BR2_INIT_BUSYBOX和BR2_PACKAGE_DEBUG_DEMO均为y。S99debug-demo是兼容脚本start创建并设置/var/log/debug-demostop不操作服务因此没有与 init 直接启动的前台程序形成重复启动路径。五、让应用启动后主动降权当前 init 项仍以 root 启动程序。程序先确认专用账户存在清除附加组并切换 GID/UID之后才读取配置、打开日志文件和运行服务主体逻辑。在Ubuntu 主机执行下面的完整代码块替换src/debug-demo/debug_demo.ccd~/buildroot-external-demo||exit1catsrc/debug-demo/debug_demo.cEOF #define _DEFAULT_SOURCE #define _POSIX_C_SOURCE 200809L #include ctype.h #include errno.h #include grp.h #include pwd.h #include signal.h #include stdio.h #include stdlib.h #include string.h #include sys/stat.h #include sys/types.h #include time.h #include unistd.h #define CONFIG_PATH /etc/debug-demo.conf #define LOG_PATH /var/log/debug-demo/debug-demo.log #define PERMISSION_TEST_MARKER_PATH /var/log/debug-demo/permission-test.enabled #define ROOT_ONLY_TEST_PATH /tmp/debug-demo-permission-probe #define SERVICE_USER debug-demo #define DEFAULT_INTERVAL_SEC 2U #define MAX_INTERVAL_SEC 60L static volatile sig_atomic_t running 1; static void handle_signal(int signal_number) { (void)signal_number; running 0; } int compute_total(const int *values, int count) { int total 0; for (int index 0; index count; index) { total values[index]; } return total; } static unsigned int load_interval(const char **status) { char buffer[256]; const char prefix[] interval_sec; FILE *config_file fopen(CONFIG_PATH, r); if (config_file NULL) { *status errno ENOENT ? missing-default : read-error-default; return DEFAULT_INTERVAL_SEC; } size_t length fread(buffer, 1, sizeof(buffer), config_file); int read_failed ferror(config_file); int close_failed fclose(config_file); if (read_failed || close_failed ! 0) { *status read-error-default; return DEFAULT_INTERVAL_SEC; } *status invalid-default; if (length sizeof(buffer) || memchr(buffer, \0, length) ! NULL) { return DEFAULT_INTERVAL_SEC; } buffer[length] \0; char *begin buffer; while (isspace((unsigned char)*begin)) { begin; } char *end buffer length; while (end begin isspace((unsigned char)end[-1])) { --end; } *end \0; if (strncmp(begin, prefix, sizeof(prefix) - 1) ! 0) { return DEFAULT_INTERVAL_SEC; } char *number begin sizeof(prefix) - 1; if (*number \0) { return DEFAULT_INTERVAL_SEC; } for (const char *cursor number; *cursor ! \0; cursor) { if (*cursor 0 || *cursor 9) { return DEFAULT_INTERVAL_SEC; } } errno 0; char *number_end NULL; long parsed strtol(number, number_end, 10); if (errno ! 0 || *number_end ! \0 || parsed 1 || parsed MAX_INTERVAL_SEC) { return DEFAULT_INTERVAL_SEC; } *status loaded; return (unsigned int)parsed; } static int drop_privileges(uid_t *service_uid, gid_t *service_gid) { struct passwd *account getpwnam(SERVICE_USER); if (account NULL) { fprintf(stderr, Required service account %s is missing\n, SERVICE_USER); return -1; } uid_t uid account-pw_uid; gid_t gid account-pw_gid; if (uid 0 || gid 0) { fprintf(stderr, Service account must not use UID/GID 0\n); return -1; } if (geteuid() ! 0) { fprintf(stderr, Service must be started by root so it can drop privileges\n); return -1; } if (setgroups(0, NULL) ! 0 || setgid(gid) ! 0 || setuid(uid) ! 0) { perror(drop service privileges); return -1; } if (getuid() ! uid || geteuid() ! uid || getgid() ! gid || getegid() ! gid) { fprintf(stderr, Privilege drop verification failed\n); return -1; } *service_uid uid; *service_gid gid; return 0; } static int log_permission_probe(FILE *log_file) { FILE *marker fopen(PERMISSION_TEST_MARKER_PATH, r); if (marker NULL) { return errno ENOENT ? 0 : -1; } fclose(marker); errno 0; FILE *protected_file fopen(ROOT_ONLY_TEST_PATH, r); if (protected_file ! NULL) { fclose(protected_file); if (fprintf(log_file, debug-demo permissions: UNEXPECTEDLY readable path%s\n, ROOT_ONLY_TEST_PATH) 0) { return -1; } } else if (errno EACCES || errno EPERM) { if (fprintf(log_file, debug-demo permissions: denied path%s errno%d\n, ROOT_ONLY_TEST_PATH, errno) 0) { return -1; } } else if (fprintf(log_file, debug-demo permissions: probe error path%s errno%d\n, ROOT_ONLY_TEST_PATH, errno) 0) { return -1; } return fflush(log_file) 0 ? 0 : -1; } static int rotate_log(FILE **log_file) { FILE *old_log *log_file; if (fflush(old_log) ! 0) { return -1; } *log_file NULL; if (fclose(old_log) ! 0) { return -1; } if (rename(LOG_PATH, LOG_PATH .1) ! 0) { int rename_error errno; *log_file fopen(LOG_PATH, a); if (*log_file NULL) { return -1; } errno rename_error; perror(rename active log; continuing with current file); return 0; } *log_file fopen(LOG_PATH, a); if (*log_file NULL) { int open_error errno; if (rename(LOG_PATH .1, LOG_PATH) 0) { *log_file fopen(LOG_PATH, a); } if (*log_file NULL) { errno open_error; return -1; } errno open_error; perror(open new active log; restored previous path); return 0; } if (fprintf(*log_file, debug-demo log: rotation complete; active file reopened pid%ld\n, (long)getpid()) 0 || fflush(*log_file) ! 0) { return -1; } return 1; } static int rotate_if_needed(FILE **log_file) { struct stat log_status; if (fflush(*log_file) ! 0 || fstat(fileno(*log_file), log_status) ! 0) { return -1; } if (log_status.st_size 1024) { return 0; } return rotate_log(log_file) 0 ? -1 : 0; } int main(int argc, char *argv[]) { const int values[] {10, 20, 30, 40}; int result compute_total(values, 4); if (argc 1) { printf(debug-demo package v2: total %d\n, result); return 0; } if (argc ! 2 || strcmp(argv[1], --service) ! 0) { fprintf(stderr, Usage: %s [--service]\n, argv[0]); return 1; } struct sigaction action {0}; action.sa_handler handle_signal; sigemptyset(action.sa_mask); if (sigaction(SIGTERM, action, NULL) ! 0 || sigaction(SIGINT, action, NULL) ! 0) { perror(sigaction); return 1; } uid_t service_uid; gid_t service_gid; if (drop_privileges(service_uid, service_gid) ! 0) { return 1; } const char *config_status NULL; unsigned int interval_sec load_interval(config_status); umask(027); FILE *log_file fopen(LOG_PATH, a); if (log_file NULL) { perror(open log as service user); return 1; } const char *level strcmp(config_status, loaded) 0 ? INFO : WARN; if (fprintf(log_file, debug-demo config: pid%ld level%s path%s status%s interval_sec%u\n, (long)getpid(), level, CONFIG_PATH, config_status, interval_sec) 0 || fprintf(log_file, debug-demo identity: uid%ld gid%ld\n, (long)service_uid, (long)service_gid) 0 || fflush(log_file) ! 0 || log_permission_probe(log_file) ! 0) { fclose(log_file); return 1; } if (rotate_if_needed(log_file) ! 0) { perror(check or rotate log); if (log_file ! NULL) { fclose(log_file); } return 1; } unsigned long tick 0; while (running) { struct timespec timestamp; if (clock_gettime(CLOCK_MONOTONIC, timestamp) ! 0) { perror(clock_gettime); fclose(log_file); return 1; } if (fprintf(log_file, debug-demo service: pid%ld tick%lu mono%lld.%09ld interval_sec%u total%d\n, (long)getpid(), tick, (long long)timestamp.tv_sec, timestamp.tv_nsec, interval_sec, result) 0 || fflush(log_file) ! 0) { fclose(log_file); return 1; } if (rotate_if_needed(log_file) ! 0) { perror(check or rotate log); if (log_file ! NULL) { fclose(log_file); } return 1; } for (unsigned int elapsed 0; running elapsed interval_sec; elapsed) { struct timespec remaining {1, 0}; while (running nanosleep(remaining, remaining) ! 0) { if (errno ! EINTR) { perror(nanosleep); fclose(log_file); return 1; } } } } if (fprintf(log_file, debug-demo service: pid%ld stopped\n, (long)getpid()) 0) { fclose(log_file); return 1; } return fclose(log_file) 0 ? 0 : 1; } EOF代码刻意只在--service模式执行降权逻辑无参数的示例计算仍保留原有行为。getpwnam在降权前查询账户程序随后清空附加组、设置主组与 UID并检查 real/effective UID/GID 均已切换。日志文件以umask 027创建预期权限为0640。本段使用grp.h中的setgroups。若目标工具链编译时提示该接口未声明先检查特性宏和目标 libc 的声明条件不要删掉清除附加组的步骤来绕过权限收紧。六、由 Buildroot 创建专用账户并设置日志目录Buildroot 的 generic package 支持在包规则中声明目标用户和文件权限。本篇通过DEBUG_DEMO_USERS创建禁止交互登录的debug-demo账户并通过DEBUG_DEMO_PERMISSIONS创建属主为该账户的日志目录UID/GID 用-1由 Buildroot 分配避免在文章中假定固定编号。在Ubuntu 主机执行更新完整包规则cd~/buildroot-external-demo||exit1catpackage/debug-demo/debug-demo.mkEOF DEBUG_DEMO_VERSION 1.0 DEBUG_DEMO_SITE $(BR2_EXTERNAL_ARM64_DEMO_PATH)/src/debug-demo DEBUG_DEMO_SITE_METHOD local DEBUG_DEMO_USERS debug-demo -1 debug-demo -1 * - /bin/false - debug-demo-service DEBUG_DEMO_PERMISSIONS /var/log/debug-demo d 0750 debug-demo debug-demo - - - - - define DEBUG_DEMO_BUILD_CMDS $(TARGET_CC) $(TARGET_CFLAGS) -Wall -Wextra -O0 -g -gdwarf-4 \ -fno-omit-frame-pointer $(D)/debug_demo.c \ -o $(D)/debug_demo $(TARGET_LDFLAGS) endef define DEBUG_DEMO_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(D)/debug_demo \ $(TARGET_DIR)/usr/bin/debug_demo $(INSTALL) -D -m 0644 $(DEBUG_DEMO_PKGDIR)/debug-demo.conf \ $(TARGET_DIR)/etc/debug-demo.conf endef define DEBUG_DEMO_INSTALL_INIT_SYSV $(INSTALL) -D -m 0755 $(DEBUG_DEMO_PKGDIR)/S99debug-demo \ $(TARGET_DIR)/etc/init.d/S99debug-demo endef $(eval $(generic-package)) EOFMakefile 宏中的命令行使用 Tab 缩进。包规则中增加的用户表和权限表会参与最终根文件系统生成不需要手工向主机/etc/passwd添加账户。账户密码字段为*shell 为/bin/false表示该服务账户不用于交互登录。Buildroot 权限表描述日志目录的属主和0750权限但/var/log可能指向启动时会清空的临时存储所以不能只依赖镜像生成阶段创建的目录。BusyBox init 启动respawn服务前还要由 root 运行的兼容脚本在每次开机时创建并设置该目录。在Ubuntu 主机更新包内的S99debug-democd~/buildroot-external-demo||exit1catpackage/debug-demo/S99debug-demoEOF #!/bin/sh case $1 in start) mkdir -p /var/log/debug-demo || exit 1 chown debug-demo:debug-demo /var/log/debug-demo || exit 1 chmod 0750 /var/log/debug-demo || exit 1 ;; stop) exit 0 ;; *) echo debug-demo is managed by BusyBox init; inspect /etc/inittab and processes 2 exit 1 ;; esac EOFchmod0755 package/debug-demo/S99debug-demosh-npackage/debug-demo/S99debug-demoecho$?语法检查应返回0。Buildroot 的rcS以 root 调用S99debug-demo start准备好运行时日志目录后BusyBox init 再启动--service。这样既不让服务账户写整个/var/log也不依赖/var/log/debug-demo在重启后仍保留。七、关机、重建并检查镜像确认 QEMU 已退出再在Ubuntu 主机执行exportDEMO_OUT$HOME/buildroot-output-arm64-demo(set-eucd$HOME/buildroot-2025.02.8makeO${DEMO_OUT:?请先设置 DEMO_OUT}\BR2_EXTERNAL$HOME/buildroot-external-demodebug-demo-rebuildV1makeO${DEMO_OUT:?请先设置 DEMO_OUT}\BR2_EXTERNAL$HOME/buildroot-external-demo-j2)echo$?构建应返回0。debug-demo-rebuild重新编译并安装包后续完整make负责生成包含用户账户和目录权限的文件系统镜像。不要让 QEMU 同时写同一份rootfs.ext2。构建后在Ubuntu 主机核对包规则中的用户、权限声明和构建产物grep-n^DEBUG_DEMO_USERS \$HOME/buildroot-external-demo/package/debug-demo/debug-demo.mkgrep-n^DEBUG_DEMO_PERMISSIONS \$HOME/buildroot-external-demo/package/debug-demo/debug-demo.mkgrep-nFx::respawn:/usr/bin/debug_demo --service$DEMO_OUT/target/etc/inittabfile$DEMO_OUT/target/usr/bin/debug_demols-l$DEMO_OUT/images/rootfs.ext2用户表由 Buildroot 在生成根文件系统镜像时通过mkusers应用到镜像专用的临时目标树普通的$DEMO_OUT/target/etc/passwd和group不一定包含这条自定义账户记录。因此对$DEMO_OUT/target/etc/group执行grep ^debug-demo:没有输出本身不代表账户创建失败也不能作为权限验证结果。最终应在启动后的 QEMU 目标端检查账户、目录属主和服务进程身份。八、启动 QEMU 并准备 root-only 测试文件在Ubuntu 主机启动同一个 QEMU 镜像命令沿用上一篇exportDEMO_OUT$HOME/buildroot-output-arm64-demo/usr/bin/qemu-system-aarch64\-Mvirt\-cpucortex-a53\-smp2\-m1G\-nographic\-kernel${DEMO_OUT:?请先设置 DEMO_OUT}/images/Image\-appendrootwait root/dev/vda consolettyAMA0\-drivefile$DEMO_OUT/images/rootfs.ext2,ifnone,formatraw,idhd0\-devicevirtio-blk-device,drivehd0\-netdevuser,idnet0\-devicevirtio-net-device,netdevnet0以root登录后先在QEMU 目标端确认账户、日志目录及/tmp权限grep^debug-demo:/etc/passwdgrep^debug-demo:/etc/groupls-ld/var/log/debug-demols-ld/tmp预期存在debug-demo用户和同名主组日志目录属主、属组是debug-demo权限为drwxr-x---0750。/tmp应允许普通用户遍历常见权限为drwxrwxrwt否则先不要继续避免把父目录权限误判成探针文件权限。本次 QEMU 目标端检查结果如下# grep ^debug-demo: /etc/passwd debug-demo:x:100:101:debug-demo-service:/:/bin/false # grep ^debug-demo: /etc/group debug-demo:x:101: # ls -ld /var/log/debug-demo drwxr-x--- 2 debug-demo debug-demo 60 Oct 26 05:41 /var/log/debug-demo # ls -ld /tmp drwxrwxrwt 3 root root 100 Oct 26 05:41 /tmp检查确认账户 UID 为 100、主组 GID 为 101日志目录归服务账户所有且权限为0750/tmp可供普通用户遍历。此结果验证了测试前的账户和目录条件权限拒绝仍需在后续启动探针后查看服务日志确认。服务启动时若没有测试开关会跳过权限探针。现在创建一个 root 所有、模式为0600的文件再创建普通服务账户可读的测试开关。若任一路径已存在不覆盖它先查明来源(set-euprobe/tmp/debug-demo-permission-probemarker/var/log/debug-demo/permission-test.enabledif[-e$probe]||[-e$marker]||[-e$marker.new];thenechoProbe or marker already exists; inspect before continuing.2exit1fiumask077printfroot-only test data\n$probechmod0600$probeprintfrun permission test\n$marker.newchmod0644$marker.newmv$marker.new$markerls-l$probe$marker)echo$?应返回0探针属主为 root 且权限为-rw-------开关文件属主为 root、权限为-rw-r--r--并位于服务账户可访问的日志目录中。按修正后的顺序在当前运行的目标系统中重新创建后实际输出如下-rw------- 1 root root 20 Oct 26 06:06 /tmp/debug-demo-permission-probe -rw-r--r-- 1 root root 20 Oct 26 06:06 /var/log/debug-demo/permission-test.enabled # echo $? 0探针和开关文件均已创建权限符合测试设计。此处只确认测试输入准备成功服务是否因文件权限而收到拒绝仍需由服务日志验证。不要在创建测试文件后重启系统Buildroot 的/tmp和/var/log可能是临时文件系统重启会清除探针或开关。当前服务进程启动时还没有看到新开关因此在QEMU 目标端以 root 向该服务发送SIGTERM让 BusyBox init 按respawn启动一个新实例service_pid$(pidof debug_demo)test-n$service_pid||exit1kill-TERM$service_pidsleep2pidof debug_demo确认pidof返回服务 PID 后再继续通常新 PID 会与旧 PID 不同。不要手工再启动第二个--service实例。新实例启动时会读取测试开关并检查 root-only 文件。本次发送SIGTERM后pidof debug_demo返回新 PID158确认 BusyBox init 已重新拉起服务。新实例启动时会立即写入权限探针结果应马上搜索活动日志和.1不要等服务运行并多次轮转后再查。九、验证服务身份、日志写入和拒绝访问新实例启动后在QEMU 目标端立即检查身份和权限探针结果service_pid$(pidof debug_demo)grep-E^(Name|Uid|Gid|Groups):/proc/$service_pid/statusgrep-H-Fdebug-demo permissions:/var/log/debug-demo/debug-demo.log*/proc/PID/status中Uid和Gid每行的四个值分别代表 real、effective、saved-set 和 filesystem ID每项应为同一个非零服务账户Groups应为空。PID/UID/GID 由系统分配不要照抄示例数字。本次重启后pidof返回 PID158双文件搜索得到# pidof debug_demo 158 # grep -H -F debug-demo permissions: \ /var/log/debug-demo/debug-demo.log \ /var/log/debug-demo/debug-demo.log.1 /var/log/debug-demo/debug-demo.log:debug-demo permissions: denied path/tmp/debug-demo-permission-probe errno13denied ... errno13EACCES证明服务降权后无法读取 root-only 探针。若看到UNEXPECTEDLY readable、probe error或没有输出应检查身份、测试开关、探针文件权限和日志轮转时机。再等待 6 秒在QEMU 目标端确认同一 PID 仍在运行且 tick 继续增长sleep6pidof debug_demotail-n8/var/log/debug-demo/debug-demo.log本次等待后的目标端输出如下# pidof debug_demo 158 # tail -n 8 /var/log/debug-demo/debug-demo.log debug-demo service: pid158 tick188 mono2052.021982144 interval_sec2 total100 debug-demo service: pid158 tick189 mono2054.037634816 interval_sec2 total100 debug-demo service: pid158 tick190 mono2056.068956720 interval_sec2 total100 debug-demo service: pid158 tick191 mono2058.082862432 interval_sec2 total100 debug-demo service: pid158 tick192 mono2060.093289552 interval_sec2 total100 debug-demo service: pid158 tick193 mono2062.102963856 interval_sec2 total100 debug-demo service: pid158 tick194 mono2064.110705616 interval_sec2 total100 debug-demo service: pid158 tick195 mono2066.117218528 interval_sec2 total100等待后 PID 仍为158tick 从188增长到195。结合前面的EACCES记录实测确认服务受到文件权限限制后仍持续运行并写入自己的日志。这是运行时验证不只依据ls权限位推断结果。它证明的是本篇测试的 Unix UID/GID 与文件权限边界不代表完成了所有安全加固。十、常见问题与本篇边界现象检查方向服务进程仍显示 root检查程序是否重新编译、init 是否运行新版程序、账户查询和drop_privileges是否成功日志报Permission denied服务不断退出检查/var/log/debug-demo属主、组和0750权限确认 Buildroot 权限规则进入最终镜像/proc/PID/status中 UID/GID 仍为 0说明降权未成功不能只凭配置文件声称权限限制已生效探针结果是UNEXPECTEDLY readable检查程序是否在降权之后读取探针且/tmp可遍历、探针确实为root:root 0600探针结果是probe error检查测试开关是否存在、root-only 文件是否存在以及/tmp是否可遍历setgroups编译失败或运行失败检查头文件特性宏、目标 libc、是否由 root 启动不要简单删掉清空附加组的操作修改配置后服务仍使用旧值配置在启动时读取通过 BusyBox init 的受控重启让新实例重新加载本篇仍让 init 以 root 启动程序只在应用初始化阶段立刻清理附加组并切换 UID/GID。它不是独立的沙箱也不限制网络、系统调用或所有 root-owned 文件访问。本次实验只验证进程的 UID/GID 已切换以及指定文件的访问被拒绝并未审计系统中的 SUID 程序、文件能力或其他提权路径。生产部署还应评估只读文件系统、能力集、MAC 策略、日志限额、升级与账户 UID 持久性。十一、清理测试文件并总结实验结束前在QEMU 目标端以 root 删除本篇创建的探针和测试开关rm-f/tmp/debug-demo-permission-probe\/var/log/debug-demo/permission-test.enabled删除开关后后续服务启动会跳过权限探针不会把测试文件缺失记录成权限失败。本篇通过 Buildroot 包用户表创建debug-demo服务账户通过包权限表为它准备独占日志目录程序在启动时清除附加组并降为非 root再打开专属日志。实测通过条件是进程 UID/GID 非零且一致、root-only 探针访问记录为EACCES、同一服务继续在自己的日志目录写入。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。