AI 逆向分析国航 AirChina FECU 参数来源并实现离线生成

AI 逆向分析国航 AirChina FECU 参数来源并实现离线生成

1. 背景与目标

本次分析的目标是定位网页请求中 FECU 参数的来源,并将其从浏览器环境中剥离出来,最终通过 Python 的 execjs 调用 JS 运行时离线生成。最初直接运行 catch.py 时虽然可以发起请求,但服务端返回 body: error;同时本地生成的 FECU 长度和网页实时生成结果不一致,因此需要继续追踪 FECU 的真实生成链路。

用户在浏览器中验证的调用方式如下:

js 复制代码
a0_0x411f3a('/g/invoke.json222')

返回形式类似:

text 复制代码
/g/invoke.json222?FECU=3logsbxN7vnp5HFjIqafz0N%2Fu%2BQt...

这说明 FECU 并不是接口固定参数,而是由页面加载的混淆 JS 动态生成并追加到 URL 中。

2. 定位核心函数:a0_0x411f3aa0_0x56ab93

通过分析提取出的 JS 文件 hxk_fec_bc29daa3.raw.js 和格式化后的 test.js,可以确认两个关键函数:

js 复制代码
a0_0x411f3a(url)   // 包装 URL,追加 FECU 参数
a0_0x56ab93()      // 生成 FECU 原始值

在可读化后的逻辑中,a0_0x411f3a 对应类似 ca(c),内部会调用 _() 生成 FECU,再通过 da(c, e) 拼接到 URL:

js 复制代码
function ca(c) {
  var f = _();
  return da(c, f);
}

function da(c, e) {
  var b = c.split("?").length > 1 ? "&FECU=" : "?FECU=";
  return c.replace(/^\s+|\s+$/g, "") + b + encodeURIComponent(e);
}

这里要注意:最终 URL 中看到的 FECU 是经过 encodeURIComponent 编码后的值。

3. FECU 的真实生成结构

核心生成函数 _() 的逻辑大致如下:

js 复制代码
function _() {
  O = u();                  // 当前时间戳
  a = $();                  // 读取服务 cookie
  aa();                     // 初始化 fingerprint

  T["0"] = c(a);            // cookie md5
  T["8"] = sessionStorage.getItem("fi") || c(JSON.stringify("none"));
  T["9"] = c(navigator.userAgent);
  T["10"] = m();            // 浏览器类型检测
  T["2"] = w();             // webdriver 检测
  T["3"] = d;               // debugger 检测
  T["4"] = I();             // 调试检测
  T["11"] = O - P + Q * 1000;
  T["12"] = p();            // 5 位随机字符串

  return Z(o(T));           // 序列化并加密
}

可以看到 FECU 不是单纯哈希,而是多个环境字段拼接后再加密,字段包括:

text 复制代码
FECW / FECL / FECN cookie
sessionStorage.fi
navigator.userAgent
浏览器检测结果
webdriver 检测结果
debugger 检测结果
服务端时间差
随机字符串
页面配置派生出的 key

其中 Z(o(T)) 是最终加密出口,o(T) 负责序列化,Z() 使用页面配置中的 R.key 派生密钥进行加密。

4. 关键问题:离线环境缺少 #wsyzwdbq

最初离线运行时生成的是 173 位左右的错误值,例如:

text 复制代码
dU+iHqmfHJ/cElrxC0tD3S...

而网页生成的值解码后是 194 位。通过 Node 直接运行 fecu_runtime.js 发现报错:

text 复制代码
getConfig error

继续追踪 r() 函数后发现,JS 会从页面隐藏配置中解析出 R.keyserver_timesecure 等字段:

js 复制代码
function r() {
  var a = window.fecBaseConfig_wsyzwdbq ||
          document.getElementById("wsyzwdbq").innerHTML.replace(/[\r\n]/g, "");

  var b = a.split(",");
  // 经过固定索引重排,派生 key、server_time、is_debugger、secure
  return {
    key: r,
    server_time: j,
    is_debugger: k,
    secure: l
  };
}

离线补环境中没有 window.fecBaseConfig_wsyzwdbq,也没有 document.getElementById("wsyzwdbq"),导致 R.key 为空,生成结果自然走错分支。

5. 使用 MCP 从真实浏览器采集配置

通过启动带远程调试端口的 Chrome:

powershell 复制代码
chrome.exe --remote-debugging-port=9222 `
  --user-data-dir=D:\Desktop\codex_api\GH\chrome-debug-profile `
  https://m.airchina.com.cn/c/invoke/booking/showFlights@pg

然后用 js-reverse-mcp 在页面上下文中执行:

js 复制代码
(() => {
  return {
    wrapped: a0_0x411f3a('/g/invoke.json222'),
    cookie: document.cookie,
    fecBaseConfig:
      window.fecBaseConfig_wsyzwdbq ||
      document.getElementById('wsyzwdbq')?.innerHTML.replace(/[\r\n]/g, ''),
    fi: sessionStorage.getItem('fi'),
    ua: navigator.userAgent
  };
})()

得到真实页面中的关键配置:

text 复制代码
#wsyzwdbq:
3dc6cadce1502814c1a8516993f11e2c,
1ec374c607689d420fb3dc7d87169dbf,
...
6c39eb66402fff30b91bb61f08db4c64

cookie:
FECW=54b6ab449c43b66b9c9a9ff0c8762dd...

这一步确认了:离线生成缺的不是算法,而是页面运行时配置和 cookie 环境。

6. 补环境实现离线生成

fecu_runtime.js 中补上必要浏览器环境:

js 复制代码
var DEFAULT_FEC_BASE_CONFIG = '3dc6cadce1502814c1a8516993f11e2c,...';

global.fecBaseConfig_wsyzwdbq = DEFAULT_FEC_BASE_CONFIG;

document.getElementById = function (id) {
  if (String(id) === 'wsyzwdbq') {
    return {
      innerHTML: global.fecBaseConfig_wsyzwdbq || DEFAULT_FEC_BASE_CONFIG
    };
  }
  return null;
};

document.forms = [];
window.chrome = { runtime: {} };
global.chrome = window.chrome;

同时增加统一参数入口:

js 复制代码
function applyOptions(options) {
  options = options || {};

  if (options.fecBaseConfig) {
    global.fecBaseConfig_wsyzwdbq =
      String(options.fecBaseConfig).replace(/[\r\n]/g, '');
    window.fecBaseConfig_wsyzwdbq = global.fecBaseConfig_wsyzwdbq;
  }

  if (options.cookies) setCookies(options.cookies);

  if (options.localStorage) {
    Object.keys(options.localStorage).forEach(function (key) {
      localStorage.setItem(key, options.localStorage[key]);
    });
  }

  if (options.sessionStorage) {
    Object.keys(options.sessionStorage).forEach(function (key) {
      sessionStorage.setItem(key, options.sessionStorage[key]);
    });
  }
}

这样 execjs 调用时就可以把真实网页参数传进去。

7. Python 侧通过 execjs 调用

catch.py 中保留 JS 编译缓存,并通过 payload 传入 cookie、页面配置和 storage:

python 复制代码
@lru_cache(maxsize=1)
def _load_fecu_ctx():
    return execjs.compile(FECU_RUNTIME_PATH.read_text(encoding='utf-8'))


def generate_fecu_offline(url: str = '/g/invoke.json') -> str:
    payload = {
        'cookies': cookies,
        'fecBaseConfig': os.getenv('AIRCHINA_FEC_BASE_CONFIG', DEFAULT_FEC_BASE_CONFIG),
        'localStorage': {
            'H5_KEY': os.getenv('AIRCHINA_H5_KEY', '2CB6E10B73D101E7'),
            'tnum': os.getenv('AIRCHINA_TNUM', '"hk6pLR0O2iwLwY0Yl/9Tzw=="'),
            'fVFlag': os.getenv('AIRCHINA_FVFLAG', '"W6muJhRLQKHltvIqWvW2HA=="'),
        },
        'sessionStorage': {
            'fi': os.getenv('AIRCHINA_FI', 'c56a66b07a8623afbfe503ef428f5fe3'),
        },
    }
    return _load_fecu_ctx().call('get_fecu', url, payload)

验证结果:

bash 复制代码
python -m py_compile catch.py
python catch.py --fecu-only

此时本地可以稳定生成 194 位解码态 FECU,说明已经进入正确生成分支。

8. 为什么网页上看到 202 位以上?

提出疑问:网页实时测试很多组,最少都是 202 位,为什么离线只有 194 位?

原因是统计口径不同。网页 URL 中的 FECU 是编码态:

js 复制代码
encodeURIComponent(fecu)

原始 FECU 中如果出现 /+,会被编码成 3 个字符:

text 复制代码
/  -> %2F
+  -> %2B

所以长度关系是:

text 复制代码
编码后长度 = 原始长度 194 + 2 * (原始 FECU 中 / 和 + 的数量)

例如用户样本:

text 复制代码
encoded length: 204
decoded length: 194
+ 数量: 3
/ 数量: 2

计算:

text 复制代码
194 + 2 * (3 + 2) = 204

因此网页看到的 202、204、206、208 都是编码态长度波动;原始 FECU 解码后仍然是 194 位。

9. 固定前缀由什么决定?

继续分析发现,FECU 的固定前缀主要由 FECW 这个 cookie 决定,当前页面 R.secure === "2",因此 $() 函数读取的是:

js 复制代码
function $() {
  if (R.secure === "2") {
    return s("FECW") || "";
  } else if (R.secure === "3") {
    return s("FECN") || "";
  } else {
    return s("FECL") || "";
  }
}

而生成时第一项就是:

js 复制代码
T["0"] = c(FECW);

实验结果:

text 复制代码
原始 FECW       -> 8BznAJJV3izo...
删除 FECW       -> dZ3gV/tvDz61...
FECW 改成 abc   -> 1+eXFlA2QX8n...
fi 改变         -> 前缀不变

所以前缀主因是:

text 复制代码
FECW + 页面配置派生出的 R.key

随机串、时间戳、fi 等更多影响中后段,不决定最前面的固定前缀。

10. 总结

本次分析的关键不是单纯"抠算法",而是还原完整浏览器运行环境。最初离线生成错误,是因为缺少网页隐藏配置 #wsyzwdbq,导致 R.key 为空;补齐页面配置、cookie、storage、UA、Chrome 环境后,execjs 能够脱离浏览器生成正确结构的 FECU。最终确认:

text 复制代码
a0_0x411f3a(url) 负责 URL 包装
a0_0x56ab93 / _() 负责生成 FECU
#wsyzwdbq 决定 R.key、server_time、secure
FECW 决定 FECU 前缀
FECU 原始长度为 194
URL 编码后通常显示为 202+

至于 python catch.py 返回 body: error,这不是 FECU 函数本身的异常,而是服务端还会校验 live session、cookie、checkToken、业务参数等上下文。FECU 只是接口校验链路中的一环。

相关推荐
LJianK12 小时前
进程、线程、多线程、异步
java·开发语言·jvm
黄林晴2 小时前
Compose 原生 FlexBox 正式上线,告别布局妥协
android
lKWO OMET2 小时前
图文详述:MySQL的下载、安装、配置、使用
android·mysql·adb
ch.ju2 小时前
Java程序设计(第3版)第二章——循环结构1
java
大黄烽2 小时前
IDEA中集成AI 工具CodeBuddy和Trae区别和选型
java·人工智能·intellij-idea
Z_Wonderful2 小时前
实现图片拖动、鼠标中心点缩放、文字层跟随功能
前端·javascript·计算机外设
HalvmånEver2 小时前
MySQL表的约束(二)
java·数据库·mysql
|晴 天|2 小时前
前端项目多平台部署:GitHub Pages + Vercel + Cloudflare Pages 实战教程
前端·javascript·vue.js
hhkSUC8PD2 小时前
Laravel AI SDK 正式发布
android·人工智能·laravel