/* test_nm_wifi_args.c — M64 P1-1 宿主回归:SSID/PSK 参数安全 + 反向控制。 * * 纯宿主测试:不连设备、不启服务、不发 /api/key/,只用本进程 + sh + execv。 * * 编译运行(在 patch_v2/meow_netmgrd 下): * cc -O1 -Wall -Wextra -I. test_nm_wifi_args.c -o ~/tmp/test_nm_wifi_args * ~/tmp/test_nm_wifi_args * * 反向控制:同一套用例分别喂给 * ① 「修复前实现」复刻(legacy_build:`set_network %d ssid '"%s"'` → sh -c)→ 必须注入成功; * ② 修复后实现(nm_wifi_ssid_ok/nm_wpa_set_argv + execv 传 argv)→ 必须不执行任何 shell。 * 只有 ①成立、②不成立,才证明这套改动真的拦住了东西(而不是「本来就不会中」)。 */ #include #include #include #include #include #include "nm_wifi_args.h" static char g_self[512]; static int g_fail; #define CHECK(cond, ...) do { \ if (!(cond)) { printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); g_fail = 1; } \ else { printf(" PASS: "); printf(__VA_ARGS__); printf("\n"); } \ } while (0) /* 复刻修复前 meow_netmgrd.c:3154 + :3307 的拼法:SSID 塞进单引号内的双引号,整行交给 sh -c。 * (值是 `a' ; id ; '` 时,第一个 `'` 就把单引号闭合了。) */ static void legacy_build(const char *field, const char *val, char *out, size_t cap) { char args[256]; snprintf(args, sizeof args, "set_network 0 %s '\"%s\"'", field, val); snprintf(out, cap, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli %s 2>/dev/null", args); } static int run_shell(const char *cmd, char *out, int cap) { out[0] = 0; FILE *f = popen(cmd, "r"); if (!f) return -1; size_t off = fread(out, 1, (size_t)cap - 1, f); out[off] = 0; return pclose(f); } /* 用 execv 跑一条 argv,子进程是本程序的 --stub-argv 形态(把收到的 argv 原样打印)。 * 这就是修复后的下发方式:wpa_cli 收到的是 argv,不经 shell。 */ static int run_argv_stub(char *const *args, char *out, int cap) { out[0] = 0; int pfd[2]; if (pipe(pfd) < 0) return -1; pid_t pid = fork(); if (pid < 0) { close(pfd[0]); close(pfd[1]); return -1; } if (pid == 0) { dup2(pfd[1], 1); dup2(pfd[1], 2); close(pfd[0]); close(pfd[1]); char *av[10]; int n = 0; av[n++] = (char *)"test_nm_wifi_args"; av[n++] = (char *)"--stub-argv"; for (int i = 0; args[i] && n < 9; i++) av[n++] = args[i]; av[n] = NULL; execv(g_self, av); _exit(127); } close(pfd[1]); size_t off = 0; for (;;) { size_t room = (off + 1 < (size_t)cap) ? (size_t)cap - 1 - off : 0; if (!room) break; ssize_t rd = read(pfd[0], out + off, room); if (rd <= 0) break; off += (size_t)rd; } out[off] = 0; close(pfd[0]); int st = 0; waitpid(pid, &st, 0); return st; } /* 旧实现「接受」判定:str_capsafe(meow_netmgrd.c:3140)只 trim 空白,不校验字符。 */ static int legacy_ssid_accepted(const char *s) { return s && s[0]; } /* 一条用例的双跑:旧实现必须中招,新实现必须只把值当字节。 */ static void case_attack(const char *label, const char *field, const char *val) { char cmd[512], out[4096]; printf("== %s ==\n value=%s\n", label, val); /* ① 反向控制:修复前 */ CHECK(legacy_ssid_accepted(val), "修复前判定放行该值(无字符校验)"); legacy_build(field, val, cmd, sizeof cmd); printf(" 修复前命令:%s\n", cmd); run_shell(cmd, out, sizeof out); CHECK(strstr(out, "uid=") != NULL, "修复前被注入:值里的 `id` 在 root 语境执行(stdout 含 uid=)"); /* ② 修复后 */ char idbuf[16], quoted[160]; char *args[5]; int rc = nm_wpa_set_argv(idbuf, sizeof idbuf, quoted, sizeof quoted, 0, field, val, args); CHECK(rc == 0, "修复后可构造 argv(execv 传参,值里的元字符无 shell 语义)"); char *full[7] = { (char *)"wpa_cli", args[0], args[1], args[2], args[3], NULL }; run_argv_stub(full, out, sizeof out); printf(" 修复后子进程收到的 argv:\n%s", out); CHECK(strstr(out, "uid=") == NULL, "修复后没有 shell:值里的 `id` 不执行"); CHECK(strstr(out, quoted) != NULL, "值原样作为**单个** argv 元素到达(未断词)"); } int main(int argc, char **argv) { if (argc > 1 && !strcmp(argv[1], "--stub-argv")) { /* execv 桩:打印收到的 argv */ for (int i = 2; i < argc; i++) printf("ARG[%d]=%s\n", i - 2, argv[i]); return 0; } ssize_t l = readlink("/proc/self/exe", g_self, sizeof g_self - 1); if (l <= 0) snprintf(g_self, sizeof g_self, "%s", argv[0]); else g_self[l] = 0; printf("== M64 P1-1 反向控制:修复前注入 / 修复后不注入 ==\n"); /* 审计给出的真实恶意串:附近 AP 广播的 SSID 与用户输入的 PSK。 */ case_attack("SSID 注入(审计原串)", "ssid", "a' ; id ; '"); case_attack("PSK 注入(审计原串)", "psk", "x';id;'"); printf("== 约束层:长度 / 控制字符 ==\n"); CHECK(nm_wifi_ssid_ok("a' ; id ; '") == 1, "`a' ; id ; '` 通过校验(11B<32B,是合法 SSID;靠 execv 中立化)"); CHECK(nm_wifi_ssid_ok("Home2G") == 1, "普通 SSID 通过"); CHECK(nm_wifi_ssid_ok("中文网络") == 1, "UTF-8 中文 SSID 通过(12B)"); CHECK(nm_wifi_ssid_ok("") == 0, "空 SSID 拒绝"); CHECK(nm_wifi_ssid_ok("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") == 0, "33 字节 SSID 拒绝(802.11 上限 32)"); CHECK(nm_wifi_ssid_ok("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") == 1, "32 字节 SSID 通过"); CHECK(nm_wifi_ssid_ok("a\nset_network 0 psk \"x\"") == 0, "含 \\n 的 SSID 拒绝(wpa ctrl_iface 行协议)"); CHECK(nm_wifi_ssid_ok("a\x7f" "b") == 0, "含 DEL 的 SSID 拒绝"); CHECK(nm_wifi_ssid_ok("a\tb") == 0, "含 TAB 的 SSID 拒绝"); CHECK(nm_wifi_psk_ok("") == 1, "空 PSK(开放网络)通过"); CHECK(nm_wifi_psk_ok("pass1234") == 1, "普通口令通过"); CHECK(nm_wifi_psk_ok("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") == 1, "64 位 hex PSK 通过"); CHECK(nm_wifi_psk_ok("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz") == 0, "64 字符非 hex 拒绝(≠ 合法 PSK)"); CHECK(nm_wifi_psk_ok("x';id;'") == 1, "`x';id;'` 通过(=合法口令字节,靠 execv 中立化)"); CHECK(nm_wifi_psk_ok("a\nb") == 0, "含 \\n 的 PSK 拒绝"); printf("== wpa_supplicant 字面量:只转义 \" 与 \\ ==\n"); { char q[64]; CHECK(nm_wpa_quote("a\"b\\c", q, sizeof q) == 0 && !strcmp(q, "\"a\\\"b\\\\c\""), "`a\"b\\c` → %s", q); CHECK(nm_wpa_quote("a' ; id ; '", q, sizeof q) == 0 && !strcmp(q, "\"a' ; id ; '\""), "shell 元字符原样保留 → %s", q); CHECK(nm_wpa_quote("a\nb", q, sizeof q) == -1, "控制字符拒绝(不落盘、不进 ctrl_iface)"); CHECK(nm_wpa_quote("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", q, 8) == -1, "缓冲不足返回 -1"); } printf("== 两处实现同步(只修一处等于没修)==\n"); { static const char *files[] = { "meow_netmgrd.c", "../meow_launcherd/netmgr_ipc.c" }; for (int i = 0; i < 2; i++) { FILE *f = fopen(files[i], "r"); if (!f) { printf(" SKIP: 读不到 %s(从 patch_v2/meow_netmgrd 下运行才检查)\n", files[i]); continue; } char buf[400000]; size_t n = fread(buf, 1, sizeof buf - 1, f); buf[n] = 0; fclose(f); CHECK(strstr(buf, "nm_wpa_set_argv") != NULL, "%s 使用共享的 nm_wpa_set_argv", files[i]); CHECK(strstr(buf, "ssid '\"") == NULL, "%s 不再有 `ssid '\\\"` 拼法", files[i]); } } printf(g_fail ? "\nRESULT: FAIL\n" : "\nRESULT: ALL PASS\n"); return g_fail ? 1 : 0; }