fix(security): M64 P1 三条 root 命令注入 — 外部串改 execv 传 argv、电源键下线、press_ms clamp

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 请求;只改源码,未提交二进制。
This commit is contained in:
AskaEth committed 2026-11-12 08:50:10 +08:00
1 parent 952a126477
commit 7b32d775c0
12 files changed
+812 -71

No files matched your search

+93 -12
View File
@@ -51,6 +51,7 @@
#include <linux/inet_diag.h>
#include "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00EF) + m_net_*(号段 0x00E0-0x00EF) */
#include "nm_wifi_args.h" /* M64 P1-1:SSID/PSK 参数校验 + wpa 字面量转义(与 launcherd 回退共用) */
/* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */
static char g_net_dir[128] = "/sys/class/net"; /* MEOW_NET_DIR */
@@ -303,8 +304,10 @@ static int reap_bounded(pid_t pid, const char *cmd, int *st)
}
/* 带四态结果的执行器(res 可为 NULL,等价于旧 run_cmd)。返回 stdout 字节数(-1=起不来)。
* argv != NULL → execv(argv)(不经过 shell,M64 P1);argv == NULL → sh -c cmd(仅常量命令)。
* stdout 超容量时继续排空(丢弃),保证子进程不被 EPIPE/SIGPIPE 提前打断、退出码可信。 */
static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_res_t *res)
static int run_exec_ex(char *buf, int cap, int tmo_ms, char *const *argv, const char *cmd,
runcmd_res_t *res)
{
runcmd_res_t lr;
runcmd_res_t *r = res ? res : &lr;
@@ -312,6 +315,7 @@ static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_re
r->exit_code = -1;
r->status = RUNCMD_EXEC;
if (cap > 0) buf[0] = 0;
const char *label = argv ? argv[0] : cmd;
int op[2], ep[2];
if (pipe(op) < 0) return -1;
@@ -322,7 +326,11 @@ static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_re
dup2(op[1], 1);
dup2(ep[1], 2); /* M49:stderr 不再丢 /dev/null,失败时留一段进错误文案 */
close(op[0]); close(op[1]); close(ep[0]); close(ep[1]);
execl("/bin/sh", "sh", "-c", cmd, (char *)0);
if (argv) {
execv(argv[0], argv);
} else {
execl("/bin/sh", "sh", "-c", cmd, (char *)0);
}
_exit(127);
}
close(op[1]); close(ep[1]);
@@ -370,7 +378,7 @@ static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_re
if (timed_out) kill(pid, SIGKILL); /* 已经超时:不再多等 300ms */
int st = 0;
int reaped = reap_bounded(pid, cmd, &st);
int reaped = reap_bounded(pid, label, &st);
r->bytes = off;
r->truncated = trunc;
if (reaped) {
@@ -388,7 +396,19 @@ static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_re
/* 兼容外壳:返回读到的字节数(-1=起不来),语义与 P1 之前一致(调用点无需改动)。 */
static int run_cmd(char *buf, int cap, int tmo_ms, const char *cmd)
{
return run_cmd_ex(buf, cap, tmo_ms, cmd, NULL);
return run_exec_ex(buf, cap, tmo_ms, NULL, cmd, NULL);
}
/* 带四态结果的兼容外壳(res 可为 NULL)。 */
static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_res_t *res)
{
return run_exec_ex(buf, cap, tmo_ms, NULL, cmd, res);
}
/* argv 形态(M64 P1:外部可控字符串只走这里,永不拼 shell)。 */
static int run_argv(char *buf, int cap, int tmo_ms, char *const *argv)
{
return run_exec_ex(buf, cap, tmo_ms, argv, NULL, NULL);
}
/* 失败时的一句话原因(有界;stderr 有料就带上,最多 120B,避免报文过长)。 */
@@ -3147,7 +3167,27 @@ static const char *str_capsafe(const char *s, char *tmp, size_t cap)
return tmp;
}
/* wpa_cli 单命令封装:拼 PATH 前缀 + 有界收割(RUNCMD_TMO_MS)。返回 run_cmd 读到的字节数。 */
/* 在固件标准目录里解析可执行文件(等价旧 `PATH=/bin:/sbin:/usr/bin:/usr/sbin` 前缀,
* 但不再让 shell 去查 PATH)。g_* 可被 MEOW_* 环境变量覆盖为绝对路径(调试用)。 */
static int resolve_bin(const char *name, char *out, size_t cap)
{
if (!name || !name[0]) return -1;
if (strchr(name, '/')) {
if (access(name, X_OK) < 0) return -1;
snprintf(out, cap, "%s", name);
return 0;
}
static const char *dirs[] = { "/bin", "/sbin", "/usr/bin", "/usr/sbin", NULL };
for (int i = 0; dirs[i]; i++) {
snprintf(out, cap, "%s/%s", dirs[i], name);
if (access(out, X_OK) == 0) return 0;
}
return -1;
}
/* wpa_cli 单命令封装:拼 PATH 前缀 + 有界收割(RUNCMD_TMO_MS)。返回 run_cmd 读到的字节数。
* args 只允许**编译期常量**(scan/list_networks/status/...)。任何来自 IPC/用户/AP 广播的
* 字符串必须走 wpa_exec()(execv,无 shell)——M64 P1-1 之前正是这里把 SSID 拼进了 sh -c。 */
static int wpa_cmd(char *out, int cap, const char *args)
{
char cmd[512];
@@ -3155,6 +3195,34 @@ static int wpa_cmd(char *out, int cap, const char *args)
return run_cmd(out, cap, RUNCMD_TMO_MS, cmd);
}
/* 执行 wpa_cli(execv 传 argv,不经过 shell)。args 以 NULL 结尾、不含 wpa_cli 本身。
* 返回 stdout 字节数(-1=二进制找不到/起不来)。
* `' ; ` $ & | < >` 在这里只是 argv 里的普通字节——这条路径上不存在 shell 解析。 */
static int wpa_exec(char *out, int cap, char *const *args)
{
char bin[256];
if (resolve_bin(g_wpa_cli, bin, sizeof bin) < 0) { if (cap > 0) out[0] = 0; return -1; }
char *argv[12];
int n = 0;
argv[n++] = bin;
for (int i = 0; args[i] && n < (int)(sizeof argv / sizeof argv[0]) - 1; i++) argv[n++] = args[i];
argv[n] = NULL;
return run_argv(out, cap, RUNCMD_TMO_MS, argv);
}
/* set_network <id> <field> <值>:字符串字段按 wpa_supplicant 语法加双引号(纯正确性转义,
* 与 shell 无关),整条命令以 argv 传入。val 必须先过 nm_wifi_ssid_ok/nm_wifi_psk_ok。 */
static int wpa_set_string(char *out, int cap, int id, const char *field, const char *val)
{
char idbuf[16], quoted[160];
char *args[5];
if (nm_wpa_set_argv(idbuf, sizeof idbuf, quoted, sizeof quoted, id, field, val, args) < 0) {
if (cap > 0) out[0] = 0;
return -1;
}
return wpa_exec(out, cap, args);
}
/* WiFi 设置页 bug4-②(2026-09-15):list_networks/scan_results 对非 ASCII 字节输出 \xNN 转义
* (中文 SSID)——还原为原始字节串再与输入 SSID 做同表示比较;不还原则中文 SSID 永不命中
* 既有网络 → 新建同名 open 记录顶影原记录。net.c wpa_decode_ssid / netmgr_ipc.c nm_decode_ssid
@@ -3265,6 +3333,16 @@ static void wifi_connect_ex(const char *arg, ctl_result_t *r, int hidden)
str_capsafe(ssid, tmp_ssid, sizeof tmp_ssid);
str_capsafe(psk, tmp_psk, sizeof tmp_psk);
if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; }
/* M64 P1-1:SSID/PSK 是外部可控串(附近 AP 广播 / 用户输入)。长度与控制字符在这里
* 收口;执行侧已改 execv 传 argv(wpa_set_string),shell 元字符不再有语义。 */
if (!nm_wifi_ssid_ok(tmp_ssid)) {
ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 非法(长度 1..32 字节,禁控制字符)");
return;
}
if (!nm_wifi_psk_ok(tmp_psk)) {
ctl_sets(r, MEOW_ERR_INVAL, "connect: psk 非法(≤63 字符口令或 64 位 hex,禁控制字符)");
return;
}
/* 找既有网络:list_networks → 匹配 ssid(剥引号 + \xNN 还原后同表示比较);命中则复用其 id。 */
char cmd[256];
@@ -3304,16 +3382,19 @@ static void wifi_connect_ex(const char *arg, ctl_result_t *r, int hidden)
id = atoi(out);
if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; }
created = 1;
snprintf(cmd, sizeof cmd, "set_network %d ssid '\"%s\"'", id, tmp_ssid);
wpa_cmd(out, sizeof out, cmd);
if (tmp_psk[0]) snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
else snprintf(cmd, sizeof cmd, "set_network %d key_mgmt NONE", id);
wpa_cmd(out, sizeof out, cmd);
/* M64 P1-1:ssid/psk 走 execv 传 argv(wpa_set_string),不再拼 `sh -c`。
* 修复前是 `set_network %d ssid '\"%s\"'` —— 单引号里套双引号,SSID 带 `'` 即逸出。 */
wpa_set_string(out, sizeof out, id, "ssid", tmp_ssid);
if (tmp_psk[0]) {
wpa_set_string(out, sizeof out, id, "psk", tmp_psk);
} else {
snprintf(cmd, sizeof cmd, "set_network %d key_mgmt NONE", id);
wpa_cmd(out, sizeof out, cmd);
}
} else if (tmp_psk[0]) {
/* bug4-①/bug1-④:命中既有网络且调用方带了新 psk → 先更新凭据再连
* (旧记录密码错误可经扫描路径重输修复;psk 为空则不动既有凭据)。 */
snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
wpa_cmd(out, sizeof out, cmd);
wpa_set_string(out, sizeof out, id, "psk", tmp_psk);
}
if (hidden) {
/* bug2:隐藏 AP 必须 scan_ssid=1,supplicant 才会主动发 Probe Request(命中既有也要补)。 */