李恒道 发表于 4 天前

蓝鸟 x-client-transaction-id追溯

关键词找到`return e.isTrue("rweb_client_transaction_id_enabled") && (i.headers["x-client-transaction-id"] = await a(t.host, t.path, t.method)),`

然后找到了https://abs.twimg.com/responsive-web/client-web/ondemand.s.cb13130a.js

经过ast解析得到了

```
!function () {
try {
    var W = "u" > typeof window ? window : "u" > typeof global ? global : "u" > typeof globalThis ? globalThis : "u" > typeof self ? self : {};
    W.SENTRY_RELEASE = {
      id: "bd5de347106a02d9c8611420fdc1de31bf160b21"
    };
    var n = new W.Error().stack;
    n && (W._sentryDebugIds = W._sentryDebugIds || {}, W._sentryDebugIds = "569088b2-348c-4662-9029-d728fdea8e0b", W._sentryDebugIdIdentifier = "sentry-dbid-569088b2-348c-4662-9029-d728fdea8e0b");
} catch {}
}();
"use strict";
(globalThis.webpackChunk_twitter_responsive_web = globalThis.webpackChunk_twitter_responsive_web || []).push([, {
208932(W, n, t) {
    t.d(n, {
      default: () => e
    }), t(28792), t(506533);
    let e = () => {
      let W;
      const = ,
       = ,
      s = W => new O(atob(W).split("").map(W => W.charCodeAt(0))),
      y = W => btoa(Q(W).map(W => String.fromCharCode(W)).join("")).replace(/=/g, ""),
      b = () => s(L(C(""), "content")),
      K = (n, t) => W = W || L(D(C(n)) % 4].childNodes.childNodes, "d").substring(9).split("C").map(W => W.replace(/[^\d]+/g, " ").trim().split(" ").map(a)),
      L = (W, n) => W && W.getAttribute(n) || "",
      J = W => typeof W == "string" ? new l().encode(W) : W,
      h = W => S.digest("sha-256", J(W)),
      N = W => (W < 16 ? "0" : "") + W.toString(16),
      D = W => Q(W).map(W => (W.parentElement?.["removeChild"](W), W)),
      H = () => {
          {
            let n = i.createElement("div");
            return i.body.append(n), )];
          }
      },
       = ,
       = ,
      U = (W, n, t) => n ? W ^ t : W,
      z = (W, n, t) => {
          {
            if (!W.animate) return;
            let e = W.animate(v(n), E);
            e.pause(), e.currentTime = j(t / 10) * 10;
          }
      },
      T = (W, n, t, e) => {
          {
            let r = W * (t - n) / 255 + n;
            return e ? g(r) : r.toFixed(2);
          }
      },
      v = W => ({
          color: ["#" + N(W) + N(W) + N(W), "#" + N(W) + N(W) + N(W)],
          transform: ["rotate(0deg)", "rotate(" + T(W, 60, 360, true) + "deg)"],
          easing: "cubic-bezier(" + Q(W.slice(7)).map((W, n) => T(W, n % 2 ? -1 : 0, 1)).join() + ")"
      });
      let F,
      X = [],
      A = n => {
          if (!F) {
            let = % 16, n % 16 * (n % 16) * (n % 16)],
            C = K(".r-1nskx", n);
            new R(() => {
            {
                let t = new q(),
                  e = V().toString(36);
                t.createDataChannel(e), t.createOffer().then(o => {
                  try {
                  {
                      let W = o.sdp || e;
                      X = Q(J( % 8] || "4", W % 8]])), t.close();
                  }
                  } catch {}
                }).catch(x);
            }
            }).catch(x);
            let = H();
            z(p, C, l);
            let m = G(p);
            F = Q(("" + m.color + m.transform).matchAll(/([\d.-]+)/g)).map(W => a(a(W).toFixed(2)).toString(16)).join("").replace(/[.-]/g, ""), S();
          }
          return F;
      };
      return async (W, n) => {
      let u = g((P.now() - NaN) / 1e3),
          o = new O(new p().buffer),
          c = b(),
          d = A(c);
      return y(new O(.concat(Q(c), Q(o), M(Q(new O(await h(.join("!") + "obfiowerehiring" + d))).concat(X)), )).map(U));
      };
    };
    (Object.getOwnPropertyDescriptor(e, "name") || {}).writable || Object.defineProperty(e, "name", {
      value: "default",
      configurable: true
    });
}
}]);

```

第零个`V() * 256`是随机掩码

第一个是c `s(L(querySelectorAll(""), "content"))`,属于标识符,找到了`<meta name="twitter-site-verification" content="0VcdiIIH5QjcFDSUuGZ34fedDV+db1TZLPS6udWM1tZF/xxxxxxxx+x2ok/TC7">`

第二个 let u = g((P.now() - 1682924400000) / 1e3),时间戳,

第三个

```
      J = W => typeof W == "string" ? new l().encode(W) : W,
      h = W => S.digest("sha-256", J(W)),
```

把传入的两个参数和时间戳进行sha-256然后.join("!") + "obfiowerehiring" + d,d参数是

```
       K = (n, t) => W = W || L(D(querySelectorAll(n)) % 4].childNodes.childNodes, "d").substring(9).split("C").map(W => W.replace(/[^\d]+/g, " ").trim().split(" ").map(a)),
      
      A = n => {
          if (!F) {
            let = % 16, n % 16 * (n % 16) * (n % 16)],
            C = K(".r-1nskx", n);
            new R(() => {
            {
                let t = new q(),
                  e = V().toString(36);
                t.createDataChannel(e), t.createOffer().then(o => {
                  try {
                  {
                      let W = o.sdp || e;
                      X = Q(J( % 8] || "4", W % 8]])), t.close();
                  }
                  } catch {}
                }).catch(x);
            }
            }).catch(x);
            let = H();
            z(p, C, l);
            let m = G(p);
            F = Q(("" + m.color + m.transform).matchAll(/([\d.-]+)/g)).map(W => a(a(W).toFixed(2)).toString(16)).join("").replace(/[.-]/g, ""), S();
          }
          return F;
      };
```

首先取之前的meta特定位置,求一些基础数据

```
let = % 16, n % 16 * (n % 16) * (n % 16)],
```

然后`C = K(".r-1nskx", n);`

`C(".r-1nskx")` 找出页面上所有 `.r-1nskx` 元素。

`t % 4` 用种子字节的第 5 位决定选第几个元素。

取它的 `childNodes.childNodes`,读 `d` 属性。

`substring(9)` 去掉前 9 个字符。

按 `"C"` 切分。

每段里的数字提取出来,用 `Number` 转成数值数组。

```
K = (n, t) =>
W = W ||
L(
    D(C(n)) % 4]
      .childNodes.childNodes,
    "d"
)
.substring(9)
.split("C")
.map(...)
```

用RTCPeerConnection创建一个连接,得到指纹信息,然后按种子信息取值

```
                let t = new q(),
                  e = V().toString(36);
                t.createDataChannel(e), t.createOffer().then(o => {
                  try {
                  {
                      let W = o.sdp || e;
                      X = Q(J( % 8] || "4", W % 8]])), t.close();
                  }
                  } catch {}
                }).catch(x);
```

创建一个dom元素

```
            let = H();
            z(p, C, l);
            
            
            z = (W, n, t) => {
if (!W.animate) return;
let e = W.animate(v(n), E);
e.pause();
e.currentTime = j(t / 10) * 10;
}
```

其中v是

```
v = W => ({
color: [
    "#" + N(W) + N(W) + N(W),
    "#" + N(W) + N(W) + N(W)
],
transform: [
    "rotate(0deg)",
    "rotate(" + T(W, 60, 360, true) + "deg)"
],
easing: "cubic-bezier(" +
    Q(W.slice(7)).map((W, n) => T(W, n % 2 ? -1 : 0, 1)).join() +
    ")"
})
```

就是让 `div` 按 `v(n)` 的关键帧播放 x 秒,但随后立即暂停并设置 `currentTime`,让浏览器停留在某个由种子数据决定的动画进度上,再读取计算后的样式作为指纹。

其中数值来源

```
let = % 16, n % 16 * (n % 16) * (n % 16)],
```

k是取SVG的数据,判断取哪行的index,l是frameTime

```
    let m = G(p);
            F = Q(("" + m.color + m.transform).matchAll(/([\d.-]+)/g)).map(W => a(a(W).toFixed(2)).toString(16)).join("").replace(/[.-]/g, ""), S();
```

获取到属性元素,提取数字,保留两位小数,再转数字,最后 `String(16)` 转十六进制,然后.join("").replace(/[.-]/g, "")

所以整体顺序是

```
[随机掩码]
[页面种子 c]
[时间戳 4 字节]


[版本 3]
```

然后xor再base64,所以随机掩码可以伪造,页面种子不可以,时间戳随便算,WEBRTC无法确定,版本3固定,那SHA-256 前 16 字节很难被检测,我们可以跳过很多计算细节,直接得到简易算法

这里推荐测试d用搜索的SearchTimeline接口,因为有cf盾,最好用fetch直接在浏览器测试



那么问题来了

1.为什么CSS在不同浏览器上精度不同,有细微差异,依然可以通过?

我们可以看到` e.currentTime = j(t / 10) * 10`,` r.toFixed(2)`,`F = Q(("" + m.color + m.transform).matchAll(/([\d.-]+)/g)).map(W => a(a(W).toFixed(2)).toString(16)).join("").replace(/[.-]/g, ""), S();`

等代码,在处理之前都会调用toFixed来进行抹除精度,保证推特的后端跟本地计算一致来通过,如果通不过就算用户该死了

2.为什么用CSS不用canvas?

canvas在用户浏览器实现多种多样,可能出现巨大差异,被浏览器/插件进行扰动或直接屏蔽放追踪,相比之下CSS兼容性更好,同时实现算法更加困难



目前市面上求动画无非两类

1.直接丢浏览器里执行

2.每一个都直接硬生生通过算出来



这两种方案github开源都有,第一种只能相对较为容易,所以我们要采用第二种方案实现



让我们再读一下结构

```
[随机掩码]
[页面种子 c]
[时间戳 4 字节]


[版本 3]
```

随机掩码 = Math.random() * 256

页面种子 = new Uint8Array(atob(content).split("").map(ch => ch.charCodeAt(0)));

时间戳 4 字节

```
const timeSeconds = Math.floor((Date.now() - 1682924400000) / 1000);
const timeBytes = Buffer.alloc(4);
timeBytes.writeUInt32LE(timeSeconds, 0)
```

WebRTC = randomBytes(2)

版本 = 3

接下来着重分析SHA-256 前 16 字节

```
hashInput = `${method}!${path}!${timeSeconds}obfiowerehiring${d}`
digest = SHA-256(hashInput)
前16字节 = digest
```

就可以得到例如`GET!/i/api/graphql/PusO6nN_nUSAsfJktZJd9w/SearchTimeline!103251094obfiowerehiring`然后接d,所以着重在d值计算上

首先第一个问题,k和l的来源是怎么取的?

!(data/attachment/forum/202608/08/172930nf5iauyipaufnrf9.png)

也是具有两条路线

1.正则匹配,直接根据正则`/\(\w\[(\d{1,2})\],\s*16\)/g`找出来

2.ast解析,之前处理of用的该种方式

我们就采用第一种了,然后一路补计算就好了

比较重要的就是关于CSS的计算规则,我们多关心这里,参考https://blog.nest.moe/posts/twitter-header-part-4#%E7%B2%BE%E5%BA%A6%E9%97%AE%E9%A2%98,这里尽量多靠AI辅导去读浏览器源码

1.cubic_bezier 缓动函数

给输入时间x,解出来时间t,然后得出进度y

(https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/UnitBezier.h)

```
      double solve(double x, double epsilon)
      {
            if (x < 0.0)
                return 0.0 + startGradient * x;
            if (x > 1.0)
                return 1.0 + endGradient * (x - 1.0);
            return sampleCurveY(solveCurveX(x, epsilon));
      }
                // Given an x value, find a parametric value it came from.
      double solveCurveX(double x, double epsilon)
      {
            double t0 = 0.0;
            double t1 = 0.0;
            double t2 = x;
            double x2 = 0.0;
            double d2 = 0.0;
            int i = 0;

            // Linear interpolation of spline curve for initial guess.
            double deltaT = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
            for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) {
                if (x <= splineSamples) {
                  t1 = deltaT * i;
                  t0 = t1 - deltaT;
                  t2 = t0 + (t1 - t0) * (x - splineSamples) / (splineSamples - splineSamples);
                  break;
                }
            }

            // Perform a few iterations of Newton's method -- normally very fast.
            // See https://en.wikipedia.org/wiki/Newton%27s_method.
            double newtonEpsilon = std::min(kBezierEpsilon, epsilon);
            for (i = 0; i < kMaxNewtonIterations; i++) {
                x2 = sampleCurveX(t2) - x;
                if (std::abs(x2) < newtonEpsilon)
                  return t2;
                d2 = sampleCurveDerivativeX(t2);
                if (std::abs(d2) < kBezierEpsilon)
                  break;
                t2 = t2 - x2 / d2;
            }
            if (std::abs(x2) < epsilon)
                return t2;

            // Fall back to the bisection method for reliability.
            while (t0 < t1) {
                x2 = sampleCurveX(t2);
                if (std::abs(x2 - x) < epsilon)
                  return t2;
                if (x > x2)
                  t0 = t2;
                else
                  t1 = t2;
                t2 = std::midpoint(t0, t1);
            }

            // Failure.
            return t2;
      }
       double sampleCurveY(double t)
      {
            return ((ay * t + by) * t + cy) * t;
      }
```

这里简单来说先进行采样,然后通过斜率/误差逼近,失败再回退二分,这里市面实现只有二分,我们也只实现二分即可,但是需要注意精度遵守1e-7

```
function solve(value, minVal, maxVal, rounding) {
const result = (value * (maxVal - minVal)) / 255 + minVal;
return rounding ? Math.floor(result) : Number(result.toFixed(2));
}

function sampleCurveX(t, x1, x2) {
const cx = 3 * x1;
const bx = 3 * (x2 - x1) - cx;
const ax = 1 - cx - bx;
return ((ax * t + bx) * t + cx) * t;
}

function sampleCurveY(t, y1, y2) {
const cy = 3 * y1;
const by = 3 * (y2 - y1) - cy;
const ay = 1 - cy - by;
return ((ay * t + by) * t + cy) * t;
}

function cubicBezierYByBisection(xTarget, curves, epsilon = 1e-7) {
const = curves;
let lo = 0;
let hi = 1;

while (hi - lo > epsilon) {
    const mid = (lo + hi) / 2;
    const xMid = sampleCurveX(mid, x1, x2);
    if (Math.abs(xMid - xTarget) < epsilon) {
      return sampleCurveY(mid, y1, y2);
    }
    if (xMid < xTarget) lo = mid;
    else hi = mid;
}

return sampleCurveY((lo + hi) / 2, y1, y2);
}
const curves = frameRow.slice(7).map((value, index) =>
    solve(value, index % 2 ? -1 : 0, 1, false),
);
const val = cubicBezierYByBisection(targetTime, curves);
```

同理

```
inline float interpolateComponentWithoutAccountingForNaN(float componentFromColor1, double color1Multiplier, float componentFromColor2, double color2Multiplier)
{
    return (componentFromColor1 * color1Multiplier) + (componentFromColor2 * color2Multiplier);
}
```

按进度取开始颜色到结束颜色,得`color = from * color1Multiplier + to * color2Multiplier`

```
TransformationMatrix& TransformationMatrix::rotate(double angle, RotationSnapping snapping)
{
    if (!std::fmod(angle, 360))
      return *this;

    return rotateRadians(deg2rad(angle), snapping);
}

TransformationMatrix& TransformationMatrix::rotateRadians(double angle, RotationSnapping snapping)
{
    double sinZ = snapping == RotationSnapping::Snap90degRotations ? roundEpsilonToZero(sin(angle)) : sin(angle);
    double cosZ = snapping == RotationSnapping::Snap90degRotations ? roundEpsilonToZero(cos(angle)) : cos(angle);
    multiply({ cosZ, sinZ, -sinZ, cosZ, 0, 0 });
    return *this;
}

```

这里是将开始角度和结束角度归一求出角度,然后算出矩阵,最后根据矩阵求d,可以得出

```
function generateD(frames, keyBytes) {
if (!frames || !keyBytes) {
    console.log({ frames, keyBytes });
    return "";
}

const frameD = frames % 4];
const frameRows = parseFrameRows(frameD);

const rowIndex = keyBytes] % 16;
const frameRow = frameRows;

let frameTime = 1;
for (const idx of ONDEMAND_INDICES.slice(1)) {
    frameTime *= keyBytes % 16;
}
frameTime = Math.round(frameTime / 10) * 10;
const targetTime = frameTime / TOTAL_TIME;
const curves = frameRow.slice(7).map((value, index) =>
    solve(value, index % 2 ? -1 : 0, 1, false),
);
const val = cubicBezierYByBisection(targetTime, curves);

// Color: interpolate from RGB to RGB, clamp, round, hex
const fromColor = frameRow.slice(0, 3).concat(1).map(Number);
const toColor = frameRow.slice(3, 6).concat(1).map(Number);
const color = interpolate(fromColor, toColor, val).map((value) =>
    Math.min(255, Math.max(0, value)),
);

// Rotation: 0deg -> target angle at progress val, then matrix
const toRotation = solve(frameRow, 60, 360, true);
const rotation = toRotation * val;
const rad = (rotation * Math.PI) / 180;
const matrix = ;

const strArr = color
    .slice(0, -1)
    .map((value) => Math.round(value).toString(16));

for (const value of matrix) {
    let rounded = Number(value.toFixed(2));
    if (rounded < 0) rounded = -rounded;
    const hexValue = floatToHex(rounded);
    strArr.push(
      hexValue.startsWith(".") ? `0${hexValue}`.toLowerCase() : hexValue || "0",
    );
}

strArr.push("0", "0");
const d = strArr.join("").replace(/[.-]/g, "");

console.log({
    rowIndex,
    frameTime,
    targetTime,
    curves,
    val,
    frameRow,
    d,
});

return d;
}
```

得到结果,bypass

!(data/attachment/forum/202608/08/172913iymjz88msmgora1z.png)

cyfung1031 发表于 昨天 10:27

> 2.为什么用CSS不用canvas?

> canvas在用户浏览器实现多种多样,可能出现巨大差异,被浏览器/插件进行扰动或直接屏蔽放追踪,相比之下CSS兼容性更好,同时实现算法更加困难


Canvas 和 CSS 各有好处。
效能方面,多元素的话Canvas较好。这在各弹幕插件证实了
国内网站,看文章的什么,多用Canvas把文章内容显示重新封装,避免text select and copy - CSS 有反解锁风险

不过整体来说还是CSS简单一点。GPUI ,JavaFX 那些都用上了CSS风格来做Styling
页: [1]
查看完整版本: 蓝鸟 x-client-transaction-id追溯