P1-1 Wi-Fi SSID/PSK 注入(netmgrd + launcherd 回退两处同步):
- 不再拼 `set_network %d ssid '"%s"'` 交给 sh -c;新增 wpa_exec()/nm_direct_wpa_exec()
以 fork+execv 传 argv(含 resolve_bin 解析绝对路径,替代 `PATH=...` 前缀)。
- 共用 nm_wifi_args.h:nm_wifi_ssid_ok/n_wifi_psk_ok/nm_wpa_quote/nm_wpa_set_argv,
两份实现同一套规则(只修一处等于没修)。
- 约束层:SSID 1..32B、禁控制字符;PSK ≤63 或 64 hex、禁控制字符;`"`/`\` 按
wpa_supplicant 语法转义。shell 元字符不再需要拒绝——execv 下没有 shell 解析。
P1-2 MeowPanel App 安装/导出/卸载注入:
- install/export/remove 改 run_argv()(fork+execv,PATH 只在子进程 setenv)。
- 包名统一 mp_pkg_ok()(仅 [a-z0-9._-],拒 `..`,与 nyai.c valid_pkg 同规则)→ 修 P2-7。
- `{path}` 追加 mp_path_unsafe()(与 mp_fonts/nyai 同一拒绝集)作第二道。
P1-3 /api/key/* 电源键:
- keycode_to_device 删除 KEY_POWER;116 进红线(403),前端遥控板电源/长按电源按钮移除。
- press_ms 全局 clamp 0..1000(单键与批量),`long` 缺省取上限。
- 按下→等待→抬起改独立线程(inject_key_async),不再阻塞单线程 8080 服务。
测试(宿主,反向控制):test_nm_wifi_args.c / test_mp_argsafe.c —— 同一套审计恶意串
(`a' ; id ; '`、`x';id;'`、`/tmp/a';id>/tmp/pwn;'.mpk`、`MeowAPPs/..`)在修复前实现下
注入成功(stdout 含 uid=)/放行,在修复后实现下不执行/拒绝。另含「两处实现同步」检查。
未刷机、未连设备、未发 /api/key 请求;只改源码,未提交二进制。
182 lines
8.0 KiB
C
182 lines
8.0 KiB
C
/* test_nm_wifi_args.c — M64 P1-1 宿主回归:SSID/PSK 参数安全 + 反向控制。
|
||
*
|
||
* 纯宿主测试:不连设备、不启服务、不发 /api/key/<code>,只用本进程 + 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <sys/wait.h>
|
||
|
||
#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;
|
||
}
|