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:
1 parent
952a126477
commit
7b32d775c0
12 files changed
+812
-71
No files matched your search
@@ -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(命中既有也要补)。 */
|
||||
|
||||
Reference in new issue
Block a user