前言

起因是我原来用了一款代理软件,本来可以导入配置到 clash 中还算比较方便。结果因为一些总所周知的原因,他们选择了使用了不知道什么协议,并放弃了 clash 导入配置的选项,只能使用他们提供的第三方桌面程序启动。但是他们写的第三方桌面程序只能说是灾难(Electron),不仅启动慢、控件响应问题也很严重,对窗口的支持也不好。终于我忍不了了,选择分析一下他们的流程。

需要了解的一些框架知识

大致的架构

electron 是前后端分离的一个程序,由 Chromium 和 Node.js 组成,采用多进程的方式运行。大致的结构如下:

image.png

主进程大致可以看作后端,渲染大致可以看作后端。两者独立,通过 ipc 进行通信。

Elecltron IPC 通信机制

这里的核心是通过 ipcMain 和 ipcRender 按照 channel 收发。

分析流程

想要上手分析程序,首先大概得先知道它的框架是什么吧,于是打开 die 看看是什么情况。可以看出来很明显是 electron 程序。

image.png

可是对于分析 electron 程序我并没有什么经验,于是借助 ai 和浏览器大致了解一下 electron 打包后的结构。

<AppName>/
  <AppName>.exe        ### 主程序
  *.dll *.pak *.bin    ### 运行时依赖库
  locales/  swiftshader/
  resources/
    app.asar           ### js 代码打包
    app.asar.unpacked/ ### 不能进 asar 的原生文件(.node/.exe/.dll)
    app-update.yml     ### electron-updater 的更新源

根据这里的信息,我们可以知道最重要的似乎就是 resoureces 中的这个 app.asar 了,根据我搜索到的信息,electron 本质上是将 js 代码打包在我的环境中用 node 启动 webview 界面运行。

那么接下来应该做什么呢?根据我以往分析程序的经验,想要分析控件最好是附加调试一个进程,通过动态的方式定位逻辑,于是我打开了 x64dbg 尝试分析。

image.png

不过马上遇到了一个棘手的问题,有很多同名的进程,我应该选择附加哪一个呢?虽然大致根据窗口名称能判断出是最后一个,但是抱着问问看的想法询问了一下 ai。

image.png

通过无 —type 参数来看确定是最后一个进程无疑了。于是我选择附加进程调试,但是马上遇到了第二个问题:以往我们定位逻辑都是通过 windows api 按钮控件定位代码,那 electron 这种 webview 程序应该如何定位。事情到这里似乎有点卡住了,想要分析功能,我们就必须找到控件对应的代码,这个和安卓逆向的思路有点类似。

那有什么方法能获取控件对应的逻辑呢?我们想到了这个前端是否能够开启 devtools 帮助我们分析,于是在尝试 F12 和 Ctrl+Shift+I 没用后,搜索到了似乎可以通过附加参数 —remote-debugging-port=<端口号> 启动的方式启动 Devtools。

electron 默认启动调试后会通过这几个地址显示信息:

/json 或 /json/list            ### 列出可调试 target
/json/version                  ### 浏览器信息 + 浏览器级 ws
/json/new?url                  ### 新建标签
/json/activate|close/<id>

image.png

给了我们 devtoolsFrontendUrl 的地址,根据这个地址能在浏览器中打开 devtools 界面。到了这里,我们终于能有方便的工具来帮助我们分析了。

接下来就需要找到按钮对应的控件函数了,在侧边的 event listener breakpoint 中找到 Mouse -> click 打下断点。

image.png

在打下断点后,点击我们要分析的程序按钮,追踪它的流程。

image.png

询问 ai 得知这里是 vue 包装的一层 handler 函数,所以我们单步跟踪进入。

image.png

在这里找到了 invoker,查看 invoker.fns 就能找到我们想要的东西了。点击跟到对应的代码部分。

image.png

startProxy() {
 
    let self = this
    const storage = window.localStorage;
 
    self.animateCSS('.Ellipse0', 'scaleAnimate')
    self.animateCSS('.Ellipse1', 'scaleAnimate1')
    self.animateCSS('.Ellipse2', 'scaleAnimate2')
    self.animateCSS('.Ellipse3', 'scaleAnimate3')
 
    if (self.nodes.length < 1) {
        self.Toast(self.$t('lang.No_line_available'), "warn")
        return
    }
 
    if (self.isAccountExhausted) {
        self.isShowAccountTrafficExhausted = true
        if (self.isStart) {
            self.sendMess("stopProxy", { "name": "stopProxy" })
            self.isStart = false
            self.Reset()
        }
        return
    }
 
    if (self.accountExpire != "") {
        if (self.isExpired(self.accountExpire)) {
            self.isShowAccountExp = true
            self.isAccountExp = true
            if (self.isStart) {
                self.sendMess("stopProxy", { "name": "stopProxy" })
                self.isStart = false
                self.Reset()
            }
            return;
        } else {
            self.isShowAccountExp = false
            self.isAccountExp = false
        }
    }
 
    if (self.isStarting) {
        return
    }
 
    if (self.isStart) {
        self.statusText = "disconnecting"
        onClickControl('Stop', self.getTunState())
    } else {
        self.statusText = "connecting"
        onClickControl('Connect', [self.getTunState(), self.isStart])
    }
 
    self.statusText = "connecting"
    self.isStarting = true
},
if (self.isStart) {
    self.statusText = "disconnecting"
    onClickControl('Stop', self.getTunState())
} else {
    self.statusText = "connecting"
    onClickControl('Connect', [self.getTunState(), self.isStart])
}

关键看这个地方,程序检测状态,然后根据状态使用 onClickControl 来与主进程通信。那么意味着我们接下来就要去在主进程对应的代码中搜寻对应的代码。也就是我们需要解包分析 app.asar 里的代码。

使用 npx @electron/asar extract app.asar out 解包得到 out 目录

 .
├── main.node
├── package.json
└── 󰣞 src
    └── main
        ├── decrypt-main.js
        ├── embedded-require-patch.js
        └── main.js

很遗憾,main.js 遭到了很严重的混淆。但是,我们转念一想这种小规模的开发者并不可能有什么比较强的反逆向技巧,所以我们尝试寻找有没有现成的工具能帮助我们解混淆。

image.png

在询问 ai 得知这个混淆主要是平坦化加虚拟机混淆,借助 ai 找到一个叫 webcrack 的项目。经过一顿操作后,发现解混淆出来的代码基本可读的。

image.png

根据原来我们 onClickControl 函数搜索到这个内核启动代码。

(async () => {
  try {
    const R = await checkCoreRunning();
    if (!R) {
      logger.info(F.rUHBI);
      await F.XkbYP(startCoreProcess);
    } else {
      logger.info(F.CpYQX);
    }
    if (q === 0) {
      logger.info(F.jeAIH);
      F.oVrEa(setSystemProxy, true);
      webContentsSend("statusJS", F.dHfXt);
    } else {
      setSystemProxy(false);
      logger.info(F.rwrat);
      try {
        await setTun(true);
        await waitMs(1000);
        const o = await F.ZRZcE(checkTunAdapter);
        if (o) {
          logger.info("[Connect] ✓ TUN 网卡已就绪");
        } else {
          logger.warn(F.JKpnu);
        }
        F.AfFDr(webContentsSend, "statusJS", "true");
      } catch (B) {
        logger.error("[Connect] 启用 TUN 失败: " + B.message);
        F.znBXt(webContentsSend, "statusJS", "false");
        sendConnectError(F.KkeVk);
      }
    }
  } catch (i) {
    logger.error("[Connect] 处理失败: " + (i.message || i));
    webContentsSend("statusJS", F.dAgle);
    sendConnectError(classifyConnectError(i.message, "core_error"));
  } finally {
    isConnecting = false;
  }
})();

重点关注下面这段内容,看到比较重要的内核怎么启动的部分。

const R = await checkCoreRunning();
if (!R) {
  logger.info(F.rUHBI);
  await F.XkbYP(startCoreProcess);
} else {
  logger.info(F.CpYQX);
}

追踪找到这个内核启动命令。解析后可以得到大概就是 "...\extra\mihomo-windows-386.exe" -d "...\resources\extra" 这条命令。

coreServer = spawn(k, ["-d", BIN_PATH], {
  cwd: BIN_PATH,
  stdio: [c.rzIVR, "pipe", c.SoyHt],
  windowsHide: true
});

我们还能找到配置文件对应的代码 const CONFIG_PATH = path.join(BIN_PATH, "config.yaml"); 但是这个配置文件依旧是加密的。但是可以找到这个保存这个配置文件的函数。

image.png

还原里面的逻辑大概是这样的流程,同时我们还能在里面找到硬编码的 aes 密钥和 iv。

2083:X = Buffer.from(fileData,'utf8').toString('base64')
2089:U = AESencrypt(X, KEY, IV)
 
const CLASH_CONFIG_KEY = "4422a60e08c97f30";   // 16B → AES-128
const CLASH_CONFIG_IV  = "8c97f304422a60e0";   // 16B

逆向操作就能得到复原出来的 config.yaml。里面有一些节点信息和个人凭证,这个东西别人拿到就随便刷你账号流量(可能也没什么用 大雾) 哦还可以修改端口了,原来版本甚至不支持修改端口。就先分析到这里吧,这个配置文件是服务器下发的,暂时想不到还有什么用处。

知道了启动流程让 ai 写了个启动 cli,避免了他原来 electron 的神秘启动流程速度。中间还遇到了死锁问题,这个以后再说吧。

image.png