跳到正文
Hacker News· scottjg·· 20 小时前AI 评分36

逆向工程 Coway 空气净化器:用 LLM 搭建 MitM 环境摆脱云端依赖

Reverse Engineering My Coway Air Purifier to Ditch the Cloud

AI 导读

一位用户为让已停产的 Coway Airmega IconS 空气净化器接入 Apple Home,尝试逆向其云端通信协议以摆脱 Coway 云服务。他利用 2026 年 10 月 eMudhra 弱私钥事件可能带来的根证书漏洞,并借助 LLM 在数分钟内自动搭建 MitM 环境,从而抓取设备与云端之间的通信流量。

正文 · AI 翻译

时间是2017年。一连串年度野火席卷了西海岸。白天的天空看起来是这样的:

A city skyline with an orange sky

我的家,和湾区的许多家庭一样,没有隔热层,也没有真正密封。为了避免吸入烟尘,很多人开始购买这样的空气净化器:

A Coway ad showing a black and a white Airmega purifier, each a square box with a large round front panel, under the headline Wirecutter top pick for 10 years

经典的Coway Airmega

它本质上就是一台价格虚高的箱式风扇,配有HEPA滤网和空气质量传感器。它有一些控制风速的按钮,如果空气质量变差,它还能自动加大风量。它很难看,但在这附近实在太常见了,我觉得大家甚至都不再注意到它们了。另外,顺便说一句,这设备确实管用。它真的能改善空气质量。

当我的妻子后来开始在晚上把风扇开到最大档来制造白噪音(一举两得!)时,这个设备就成了我们家的必备品。久而久之,我也习惯了它,现在睡觉也离不开它了。

“晚安”按钮

我们家还有智能照明,全部接入了Apple Home。晚上,我总是按下“晚安”按钮,它会关掉所有的灯。我非常想把风扇也接入这个流程。我按下“晚安”,灯熄灭,风扇加大到我妻子喜欢的白噪音音量。

Apple Home app showing the Good Night scene button

旧款Airmega没有Wi-Fi,所以没有简单的方法把它接入Apple Home。当它最终坏掉后,我买了现已停产的Coway Airmega IconS(对于未停产的Wi-Fi型号,可以看看Airmega 250S或Airmega MightyS)。它仍然只是一台带HEPA滤网的箱式风扇,但价格贵了不少,外观设计得像一件高档的中世纪现代家具,最重要的是,它可以通过Wi-Fi控制(算是吧)。

A beige, fabric-fronted Coway purifier on tapered wooden legs, next to a phone showing Coway's app

Coway Airmega IconS

现有的Home Assistant集成

Coway的iPhone应用是典型的IoT产品。加载要花很长时间,而且几乎每次打开都要求我改密码。幸运的是,一位勇敢的人对这款应用进行了逆向工程,并编写了一个Home Assistant集成。这个集成或多或少地伪装成手机应用。它轮询Coway云服务以获取状态更新,并通过它发送命令来控制风扇。Home Assistant再把它桥接到Apple Home,然后……瞧!

Coway IoCare云

Coway的服务器,在互联网的某个地方

命令 ↑
状态 ↓

状态 ↑
命令 ↓

我的家

Home Assistant

IoCare集成

空气净化器

Airmega IconS,通过Wi-Fi连接

Apple Home

“晚安”按钮

这种方法的缺点是你仍然受制于(或者说受制于其缺失)Coway的云服务。似乎每年至少会坏一两次。

以下是我见过的一些故障原因:

  1. Coway不喜欢这个集成频繁请求他们的服务器,于是将其封禁。在与社区进行了旷日持久的争论后,他们最终同意允许其使用。
  2. Coway重写了他们的手机应用,并将其从IoCare更名为IoCare+,因此集成也不得不重写。
  3. Coway对集成所抓取的HTML布局做了细微改动,导致集成失效。
  4. Coway的服务器定期离线,导致无法对设备进行任何远程控制。
  5. Coway曾一度封禁了我的账户,因为我运行的是旧版本的集成。在等待他们的支持团队解除封禁的几周里,我无法远程控制风扇。

除此之外,Coway 似乎不太可能永远维持这项服务。我怀疑最终它会像所有物联网厂商一样,要么对这项服务收费,要么直接停掉。我这款风扇型号已经停产了(不过还有其他支持 Wi-Fi 的型号,所以……谁知道呢)。

突破口

我承认,我一直很懒。多年来,我一直拖着没去想办法摆脱 Coway 云服务,因为如果设备真的被锁死,那会相当棘手。据推测,风扇启动时会连接到 coway.com(或类似地址),并检查服务器 SSL 证书是否有效。很可能我无法伪造出能通过该检查的证书,所以风扇不会连接我的服务器。

而且,搭建中间人(MitM)环境本身就是件麻烦事。风扇必须连接到一个隔离的 Wi-Fi 网络,该网络在出网时将所有流量经由我自己的服务器路由。要做到这一点,你得配置一堆路由和防火墙规则来重定向流量。这些并非不可能(我以前做过),但搭建起来很麻烦,而且总是很难调对。而且一旦搭好,那也只是第一步。

话虽如此,有两件大事发生了变化。

首先,最近发生了一起安全事件(2026 年 10 月),有人发现受信任的互联网证书颁发机构之一 eMudhra 为其部分私钥使用了非常弱的参数。

在接下来的几天或几周内,任何人现在都能凭借 eMudhra 获得一个根 CA。是时候对不情愿的设备搞点 MitM 乐子了!

A table of eMudhra intermediate CAs and their private key d values, all tiny numbers like 156, 230, 115, 56, 155 and 4

2026 年 10 月 20 日上午 10:15

所以,如果设备未打补丁,并且硬编码了根证书,而且没有对云服务的证书做固定(pinning),那么……也许这就是一个突破口。诚然,这有很多个“如果”,但假设设备设计上没有重大安全失误,这可能就是控制它所需的立足点。

第二件事就是 AI。LLM 可以搭建 MitM 环境所需的一切,这样你就能直接开始探查风扇的工作原理。它可以在无人值守的情况下,几分钟内完成。

一旦你能置身于风扇和互联网之间,你就能看到它如何通信,这应该有助于我们理解如何让风扇与我们通信,而不是与云通信。

我们的中间人

最初,我尝试这样搭建:

空气净化器

Airmega IconS

Wi-Fi

树莓派

Wi-Fi 接入点

树莓派处于 AP 模式,使用其自己的隔离网络

所有流量被重定向

mitmproxy

解密并记录流量

我的家用路由器

常规家庭网络

Coway IoCare 云

在互联网上

这实际上没有成功,因为树莓派使用的是一根较小的 PCB 走线天线,信号强度不太够。当树莓派和我的路由器在同一个房间时,信号太弱,无法到达风扇。我想我本可以把风扇搬到路由器的房间,但既然我可以用一个复杂的技术方案解决,为什么要那么做呢?

My laptop plugged into a USB-C Ethernet adapter, plugged into a Raspberry Pi, plugged into a USB-C battery

树莓派使用其 Wi-Fi 无线电运行自己的接入点,供风扇连接。它很难在同时通过 Wi-Fi 连接家庭网络的情况下做到这一点。所以,我把树莓派搬到风扇所在的房间,并将其以太网口插入我的笔记本电脑,笔记本电脑再通过 Wi-Fi 将其桥接到家庭网络:

空气净化器

Airmega IconS

Wi-Fi,同一房间

树莓派

Wi-Fi 接入点

树莓派处于 AP 模式,使用其自己的隔离网络

所有流量被重定向

mitmproxy

解密并记录流量

以太网

我的笔记本电脑

互联网共享,将 Wi-Fi 桥接到以太网

Wi-Fi

我的家用路由器

常规家庭网络

Coway IoCare 云

在互联网上

接下来,我们需要配置树莓派。

Claude chat where my request to set up the Pi as a mitmproxy access point is blocked under Category: cyber

我在这里并不是要评判拦截这个请求是否合理,但 Claude 不会答应这个请求,即使是为了……呃……测试目的也不行。

幸运的是,ChatGPT 没有这种顾虑。

ChatGPT is fine with category Cyber

经过几轮来回,它最终得到了以下 nftables 规则:

scottjg@pi:~ $ sudo nft -a list ruleset
table inet mitmproxy_ap { # handle 8
	set client_interfaces { # handle 4
		type ifname
		elements = { "wlan0" }
	}

	chain prerouting { # handle 1
		type nat hook prerouting priority dstnat; policy accept;
		meta nfproto ipv4 iifname @client_interfaces tcp dport { 80, 443 } counter packets 2 bytes 120 redirect to :8080 # handle 6
	}

	chain input { # handle 2
		type filter hook input priority filter - 10; policy accept;
		iifname != "lo" iifname != @client_interfaces tcp dport 8080 counter packets 0 bytes 0 reject with tcp reset # handle 7
		iifname @client_interfaces meta nfproto ipv6 counter packets 9 bytes 2397 drop # handle 8
		iifname @client_interfaces ct state established,related accept # handle 9
		iifname @client_interfaces udp dport { 53, 67 } accept # handle 11
		iifname @client_interfaces tcp dport { 53, 8080 } accept # handle 13
		iifname @client_interfaces ip protocol icmp accept # handle 14
		iifname @client_interfaces counter packets 23 bytes 4697 reject with icmpx admin-prohibited # handle 15
	}

	chain forward { # handle 3
		type filter hook forward priority filter - 10; policy accept;
		iifname @client_interfaces meta nfproto ipv6 counter packets 0 bytes 0 drop # handle 16
		oifname @client_interfaces meta nfproto ipv6 counter packets 0 bytes 0 drop # handle 17
		iifname @client_interfaces udp dport 443 counter packets 1 bytes 57 reject # handle 18
		iifname @client_interfaces oifname != "eth0" counter packets 0 bytes 0 reject with icmpx admin-prohibited # handle 19
	}
}
table ip nm-shared-wlan0 { # handle 12
	chain nat_postrouting { # handle 1
		type nat hook postrouting priority srcnat; policy accept;
		ip saddr 10.42.0.0/24 ip daddr != 10.42.0.0/24 masquerade # handle 2
	}

	chain filter_forward { # handle 3
		type filter hook forward priority filter; policy accept;
		ip daddr 10.42.0.0/24 oifname "wlan0" ct state { established, related } accept # handle 5
		ip saddr 10.42.0.0/24 iifname "wlan0" accept # handle 6
		iifname "wlan0" oifname "wlan0" accept # handle 7
		iifname "wlan0" reject # handle 8
		oifname "wlan0" reject # handle 9
	}
}

基本上,masquerade 规则将来自隔离 Wi-Fi 网络的流量通过我们的家庭互联网进行转换,而 redirect 规则将 HTTP/HTTPS 转发到正在运行的 mitmproxy 实例。

同样在树莓派上,mitmproxy 与其 Web 界面 mitmweb 一起运行,使用如下命令行:

mitmweb \
  --mode transparent --showhost \
  --listen-host 0.0.0.0 --listen-port 8080 \
  --web-host 127.0.0.1 --web-port 8081 \
  --set web_open_browser=false \
  --emudhra-old

是的,开箱即用,mitmproxy 有一个内置选项(--emudhra-old)可以自动使用 eMudhra 根 CA 来拦截流量。

窥探风扇

接下来,我使用 iPhone 应用在我们的 Pi-MITM-Test Wi-Fi 网络上设置风扇。中途,应用报告设置失败。让我们看一下 mitmproxy Web 界面:

mitmweb showing a single request, a plain HTTP GET for AmazonRootCA1.pem

所以,我们知道存在某种连接问题,因为应用说风扇连接失败。但它显然连上了我们的隔离 Wi-Fi,因为它发出了一个请求。

这个请求非常可疑。看起来风扇正在通过明文 HTTP 下载 Amazon 证书颁发机构的根证书。此界面仅显示足够深入以发出实际 HTTP 请求的连接。例如,如果风扇不接受我们用来冒充真实服务器的 eMudhra 证书,它的 HTTPS 连接将在到达该点之前失败,并且永远不会出现在列表中。

假设:设备通过 HTTP 下载此公共证书,以便它有一些东西来验证其后续(目前不可见)的 HTTPS 连接。它根本没有内置任何根证书。

mitmproxy 有一个插件模式,你可以修改特定请求。我们可以用它来将 mitmproxy 自己生成的 CA 证书替换到这个公共证书的位置。如果我们的假设正确,风扇将信任该 CA,我们就可以拦截其剩余的流量。

"""Serve this lab's public RSA CA only to the authorized Coway test device."""
from pathlib import Path
from mitmproxy import ctx, http

def response(flow: http.HTTPFlow) -> None:
    if (
        flow.client_conn.peername[0] == "10.42.0.93" # device IP
        and flow.request.scheme == "http"
        and flow.request.method == "GET"
        and flow.request.pretty_host == "rootca-fs.iocareus.com.s3.ap-northeast-2.amazonaws.com"
        and flow.request.path.split("?", 1)[0] == "/AmazonRootCA1.pem"
        and flow.response is not None
        and flow.response.status_code == 200
    ):
        certificate = Path("/var/lib/mitmproxy-ap/mitmproxy-ca-cert.pem").read_bytes()
        flow.response = http.Response.make(
            200,
            certificate,
            {
                "Content-Type": "application/x-pem-file",
                "Cache-Control": "no-store",
            },
        )
        flow.metadata["coway_test_ca_served"] = True
        ctx.log.info("Served lab RSA CA to the Coway's HTTP CA bootstrap request")

果然,设备现在完全连接上了,我们可以看到一些新的请求:

mitmweb now showing HTTPS requests to Coway’s cert server, including downloads of a device certificate and private key

假设得到证实

我们可以拦截风扇和服务器之间的流量,这意味着我们可能可以构建自己的服务器来替代 Coway IoCare 云。而且事实证明,我们根本不需要 eMudhra 破解。风扇通过明文 HTTP 下载其根 CA 证书。它一直都可以被中间人攻击。

唯一缺少的是,这些请求都与更改风扇速度或你在应用中能做的任何其他事情无关。不过,这很容易弄清楚。如果你在树莓派上运行 tcpdump 同时在应用中点击按钮,你可以看到端口 8883 上的加密流量,这是 MQTT over TLS 的标准端口。MQTT 是一种常用于 IoT 设备的消息总线协议。

回到上面请求列表中的 POST,以下是该 API 调用返回的内容:

{
    "header": {
        "success": true,
        "result": "S1000",
        "message": "SUCCESS"
    },
    "payload": {
        "macAddr": "<redacted>",
        "certToken": "<redacted>",
        "host": "a29vouc2qruduj-ats.iot.ap-northeast-2.amazonaws.com",
        "port": 8883,
        "serial": "<redacted>",
        "certSvrUrl": "https://cert.iotp.coway.com/iot/v1/cert/r0",
        "caUrl": "http://rootca-fs.iocareus.com.s3.ap-northeast-2.amazonaws.com/AmazonRootCA1.pem",
        "privateUrl": "https://icert-fs.iocareus.com/<redacted>/55d66923e9-private.pem.key",
        "certUrl": "https://icert-fs.iocareus.com/<redacted>/55d66923e9-certificate.pem.crt",
        "svcCode": "002AIR007",
        "countryCd": "US"
    }
}

所以,如果我们控制对此调用的响应,我们就控制了风扇用于连接 MQTT 服务器的密钥,以及该 MQTT 服务器的位置。

目前,我们可以编写另一个 mitmproxy 插件,将我们自己的密钥注入此响应,然后使用它来代理 MQTT 流量。插件代码变得有点长,所以我没有在这里包含它,但 AI 生成它相当快。mitmproxy 不解码 MQTT,但它仍然可以向我们显示原始流:

mitmweb showing the raw MQTT stream between the fan and AWS IoT, with topic names like IoCare/MControl visible

这些数据看起来像是二进制和 ASCII 乱码的混合体,但它其实是一个标准的 MQTT 协议流。当我在 App 中调节风扇转速时,你可以看到一条新消息从 Coway 服务器发往设备。通常,我会把它加载到流量解析器中,并制作一张表格,记录我在 App 中按下每个按钮时传入的乱码,但现在……

My message to the AI listing every button I pressed in the app, in order (mode, speed, timer, sensitivity, lighting, pre-filter cycle), asking it to map them to the MQTT commands

几分钟后……

The AI’s reply with a few notes on the decoded commands: timer values are in minutes, filter-cycle values are weeks times 56, and the mappings are saved to coway-command-map.json

现在这里的信息已经足够我们构建自己的 Web 服务器,在设置过程中向风扇下发证书和配置,再加上我们自己的 MQTT 服务器来向它发送命令。这样在正常运行时就能完全把 Coway 云排除在外,但我们仍然使用 Coway App 来设置设备。我们仍然需要一个相当麻烦的中间人(MitM)环境,才能让风扇连接到我们而不是 Coway。

蓝牙设置过程

当你使用 Coway App 设置风扇时,它会通过蓝牙搜索设备。我希望我们能找到一种自己配置设备的方法,而不需要 App。另外,如果运气好的话,我们可能还会发现其他有用的东西。

我以前逆向工程过蓝牙设备。我以前的策略是获取 Android 版 App,反编译它,然后仔细研读代码。这并不总是像听起来那么简单。开发者经常对 App 进行混淆,这使得反编译后的代码更难阅读。有些字节码也无法干净地反编译回源代码,这意味着你最终不得不阅读原始字节码。

不过,在我看来,这通常比处理 iPhone App 更容易。Android App 通常用 Java(或 Kotlin)编写,而逆向 Java App 就是比逆向用 Swift 或 Objective-C 编译的 App 更容易。

这一次,我尝试了一种新策略,即使用 Android 内置的蓝牙调试日志。

Photo of an Android phone’s developer options menu with Enable Bluetooth HCI snoop log turned on

然后,我通过 USB 连接手机,让 AI 通过 adb 驱动手机,查看蓝牙日志,并重建协议。AI 花了一段时间来处理这个任务。最终,它识别出携带 Wi-Fi 配置的消息是加密的。由于我们最终需要弄清楚使用了什么加密算法,我们可能不得不查看反编译后的 App。果然,它接下来做的就是从手机上提取 Coway App 并反编译它。然后,它开始将源代码与蓝牙日志进行匹配。

经过所有这些,我们可以看到配网过程是这样的:

  1. 通过蓝牙从特征值 FFA0 读取风扇的身份信息(“特征值”是一个可以读取或写入的蓝牙端点)。这会给你一些关于设备的基本信息,包括它的 MAC 地址。
  2. Write an encryption key (20 random lowercase ASCII letters) to characteristic FFA2:
    "P001" + protobuf {
      command: 1,
      key: Base64(random_letters)
    }
    
  3. 将第 2 步中的密钥与 MAC 地址的 12 个大写 ASCII 字符拼接,构造 AES 密钥。
  4. Build the configuration payload, which looks like this:
    {
      "cmd": 2,
      "ssid": "Example Wi-Fi",
      "pwd": "example-wifi-password",
      "bssid": "AABBCCDDEEFF",
      "type": 1,
      "macAddr": "112233445566",
      "caUrl": "http://192.0.2.1:8080/ca.pem",
      "certSvrUrl": "https://192.0.2.1:9443/iot/v1/cert/r0",
      "certToken": "example-enrollment-token",
      "serial": "EXAMPLE000000000001"
    }
    
    That gets wrapped in protobuf, padded with PKCS#7, and encrypted using the AES key from step 3. Then it gets prefixed with the string “P002” and sent to FFA2 as well.

不用说,如果手动从反编译的 Android App 中端到端地弄清楚这一点,会非常耗时。至少需要几个小时的工作(很可能是几天)。AI 在这里真的帮了大忙。AI 花了大约 20 分钟。

不过,好的一面是,解密该载荷所需的所有数据都在我们从手机捕获的蓝牙日志中,因此我们可以仅凭这些日志来检查我们的重新实现是否正确。在此基础上,AI 很快就用一个小型 Python 脚本重新实现了整个设置流程。

在整个过程中,我一直相当仔细地关注着进展。AI 实际上从未向我展示完整的 payload,但我能从它正在编写的脚本的中间输出中看到。就在那时,我注意到了某些东西。我们捕获的两个 URL 就设置在这个 payload 中(caUrl 和 certSvrUrl)。第一个告诉风扇通过明文 HTTP 下载根 CA 证书的位置,第二个告诉它通过 HTTPS POST 到哪里以获取其 MQTT 配置。

既然现在是我们发送这个 payload,我们就可以把那些 URL 指向任何地方。最重要的是,这意味着我们不再需要进行任何中间人攻击了。我们只需要运行自己的服务器,并在设置风扇时填入它的 URL。

Home Assistant 集成

基于本文中的发现,我能够为风扇构建一个 Home Assistant 自定义集成。我的 Home Assistant 服务器没有蓝牙,而且离我的风扇相当远,所以我选择使用 Web Bluetooth 进行配网。这样,你就可以把笔记本电脑带到风扇旁边,直接将其配网以与 Home Assistant 通信。

The coway-local setup page in Chrome, with Chrome’s Bluetooth picker showing the fan as paired

之后,设备直接通过 Wi-Fi 与 Home Assistant 通信。

Home Assistant’s Air Purifier control showing the fan at 67%, with High, Medium, Low, and Off speed buttons

而且,更重要的是,它从 Home Assistant 桥接到我的 Apple Home。

Apple Home's Master Bedroom section with an Air Purifier tile next to the Soffit Light Apple Home's Air Purifier control, a tall blue slider set to Manual 67%

一天的工作就完成了。现在,我终于可以点击“晚安”了。

来源:Hacker News · scottjg.com