JSリバースエンジニアリング:特定の承認局における暗号化・復号化の解析

#本記事の内容は学習と交流の目的のみに使用されるものであり、その他の目的には使用しないでください。キャプチャされたパケットデータ、データインターフェース、機密URLなどはすべて匿名化処理済みです。商業利用や違法な目的での使用は厳禁です。それによって生じるすべての結果は著者とは無関係です。著作権侵害がある場合は、著者に連絡してすぐに削除してください。

逆向目标

  • 网址:aHR0cHM6Ly9jcmVkaXQuaGQuZ292LmNuL3h5eHhncy8=
  • 目标:接口参数加密,响应数据解密

本記事では、標準的な暗号化アルゴリズムを扱う際に、様々なライブラリを使用して再現する経験を共有します。暗号学の専門家ではないため、通りすがりの皆様からのご指導をいただけると幸いです。

抓包分析

Webサイトにアクセスし、「もっと見る」ボタンをクリックすると、xzxkfrというデータパケットが送信されます。

このリクエストパラメータの中で、queryContentsignが解析の対象となります。nonceStrは単なるランダムな文字列です。

レスポンスヘッダはapplication/octet-streamであるため、レスポンスデータの復号も解析の対象となります。

逆向分析

それでは、直接にリバースエンジニアリングを始めましょう。

暗号化

Webサイトをリロードし、キーワードqueryContentを検索してみましょう。あるJSファイル内で見つかりました。

該当ファイルの該当箇所にブレークポイントを設定し、「もっと見る」をクリックすると、ブレークポイントで停止します。この時点で、パラメータqueryContentは生成済みで、iという変数に格納されています。

iがどのように生成されたかを遡ってみましょう。i && null != r.headers && "1" === r.headers["C-GATEWAY-QUERY-ENCRYPT"] && n.encryptType === P.ENCRYPT_TYPE.SM4という条件が成立していることがわかります。したがって、i = ls.sm4.encrypt(i, e.encryptKey)のロジックが実行されます。i = ls.sm4.encrypt(i, e.encryptKey)i = us(i, e.encryptKey)の両方にブレークポイントを設定し、リクエストを再送信してステップ実行し、どちらの関数が呼び出されるか確認することも可能です。

リクエストパラメータの文字列とSM4暗号化のkeyが渡され、queryContentが生成されていることがわかります。

encrypt関数の中身に直接進みましょう。ここでtは平文、ekeyです。

コードの特徴から、オンラインで検索したりAIに尋ねたりして、この方式(SM4)を実装できるライブラリがないか探してみましょう。

ここでは、すでに検索した結果のコードを提示します。

const sm4 = require('sm-crypto').sm4
const plain_text = '' // UTF-8文字列またはバイト配列
const encryption_key = '' // 16進数文字列またはバイト配列、128ビットである必要あり

let encrypted_data = sm4.encrypt(plain_text, encryption_key) // 暗号化、デフォルトで16進数文字列を出力、デフォルトでpkcs#7パディングを使用(pkcs#5を渡してもpkcs#7パディングが適用されます)
let encrypted_data = sm4.encrypt(plain_text, encryption_key, {padding: 'none'}) // 暗号化、パディングなし
let encrypted_data = sm4.encrypt(plain_text, encryption_key, {padding: 'none', output: 'array'}) // 暗号化、パディングなし、出力をバイト配列に
let encrypted_data = sm4.encrypt(plain_text, encryption_key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'}) // 暗号化、CBCモード

Node.jsでシミュレーションを試してみましょう。

const sm4 = require('sm-crypto').sm4
const plain_text = ''  // 平文
const encryption_key = ''  // 暗号鍵

console.log(sm4.encrypt(plain_text, encryption_key, {output: 'string'}))

Webサイト上の実行結果とローカルの実行結果を比較すると、一致していることがわかります。これでqueryContentの暗号化シミュレーションは完了です。

時にはそううまくはいかず、paddingmodeなどを試す必要があるかもしれません。もし独自に改変されている場合は、別途対応が必要です。

次にsignの暗号化ロジックを追ってみましょう。n.sign = oであり、signTypeSM2であるため、o = Zi(o = ls.sm2.signature(a, e.appSignPrivateKey, e.appSignPublicKey, e.appId))のロジックを辿る必要があります。

ここでaqueryContentを含む一連のパラメータから構成される文字列です。コードを直接抽出できます。

signature関数の中身に進みましょう。署名アルゴリズムであることがわかります。

いつものように、オンラインで検索します。

const sm2 = require('sm-crypto').sm2
const data_to_sign = ''
const private_key = ''
const user_id = ''

let signature_hex = sm2.doSignature(data_to_sign, private_key, {
    hash: true,
    publicKey: e.appSignPublicKey, // 公開鍵
    userId: user_id,  // デフォルトのuserIdは1234567812345678
})

SM2は、中国国家密码管理局が発表した楕円曲線公開鍵暗号アルゴリズムの標準です。各暗号化処理で新しいランダム数が生成され、暗号文の生成プロセスに参加します。そのため、ローカルでのシミュレーションでパラメータが正しいかどうかを確定することはできません。APIにリクエストを送信し、データが返ってくれば、パラメータに問題がないことを意味します。

SM2の結果が得られたので、最終的なsign値を取得し、APIをシミュレーションするには、Zi関数も追う必要があります。

Zi関数は直接コードを抽出すればよいですが、抽出過程で遭遇するXiYi関数内のBigIntegerはライブラリを導入することで実現できます。具体的な方法はオンラインで検索し、コードを提示します。

const BigInteger = require('jsbn').BigInteger;
function Xi(t) {
    return new BigInteger(t, 10).toString(16)
}
function Yi(t) {
    return new BigInteger(t, 16).toString(10)
}

パラメータのシミュレーションが完了したので、直接APIにリクエストを送信してデータが返ってくるか確認しましょう。

データが正常に返ってきたので、パラメータのシミュレーションに問題がないことが確認できました。

復号

次に、レスポンスデータの復号を確認しましょう。

インターフェースがxhrリクエストであるため、レスポンスインターセプターinterceptors.response.useを検索してみましょう。あるJSファイルで見つかりました。

ソースファイルに直接ジャンプすると、非同期処理のようですが、詳細は追いません。復号のロジックは下図で指摘されているoo関数にあります。ここでrはレスポンス、eオブジェクトには復号に必要なkeyなどの情報が含まれています。

oo関数に直接進み、条件分岐を除いたロジックコードは以下の通りです。不足している部分は随時抽出していきます。t.dataはレスポンスのarraybufferで、復号にはSM4が使用されています。

// レスポンスデータの処理
var r = t.data
  , n = new DataView(r)
  , i = new Uint8Array(r)
  , s = {}
  , a = 40;
D.forEach((function(t, e) {
    var r = n.getInt32(4 * e);
    s[t] = i.subarray(a, a + r),
    a += r
}
));
// ここでレスポンスデータの署名が正しいか検証しています
var o = ao(s, e)  
  , u = o[0]
  , h = o[1];
if (!u)
    return Promise.reject(new Error("署名検証に失敗しました"));
// データ復号 (SM4)
var c = "{}";
c = kr.sm4.decrypt(function(t) {
    for (var e = "", r = 0; r < t.length; r++) {
        var n = t[r].toString(16);
        1 === n.length && (n = "0" + n),
        e += n
    }
    return e
}(s.body), e.encryptKey, {
    output: "string"
});

コードを抽出する際、var o = ao(s, e)がレスポンスデータの署名を検証しているとはわからないため、一緒に抽出することになります。

いくつか注意点があります:

Qi関数はライブラリを導入して実現できます

// Webサイト上
function Qi(t) {
    var e = Ai.lib.WordArray.create(t);
    return Ai.MD5(e).toString(Ai.enc.Hex)
}
// ローカルシミュレーション
const CryptoJS = require('crypto-js')
function Qi(t) {
    var e = CryptoJS.lib.WordArray.create(t);
    return CryptoJS.MD5(e).toString(CryptoJS.enc.Hex)
}

ji関数はNode.jsで直接シミュレーションできます

// Webサイト上
var Pi = "function" == typeof Buffer
var ji = Pi ? function(t) {
    return Buffer.from(t).toString("base64")
}
: function(t) {
    for (var e = [], r = 0, n = t.length; r < n; r += 4096)
        e.push(Ri.apply(null, t.subarray(r, r + 4096)));
    return Hi(e.join(""))
}
// ローカルシミュレーション、Node.jsに標準搭載のBufferを使用
var ji = function(t) {
    return Buffer.from(t).toString("base64")
}

署名検証時のSM2アルゴリズム

// Webサイト上
Vr.doVerifySignature(a, Zi(i), e.platformPublicKey, {
    hash: !0,
    userId: e.appId
})
// ローカルシミュレーション
const sm2 = require('sm-crypto').sm2
sm2.doVerifySignature(a, Zi(i), e.platformPublicKey, {
    hash: true,
    userId: e.appId
})

最後にSM4復号ですが、暗号化がオンラインでライブラリを見つけられたので、復号も難しくありません。

// Webサイト上
c = kr.sm4.decrypt(function(t) {
    if (!(t instanceof Uint8Array))
        throw new Error("無効なUint8Arrayです");
    for (var e = "", r = 0; r < t.length; r++) {
        var n = t[r].toString(16);
        1 === n.length && (n = "0" + n),
        e += n
    }
    return e
}(s.body), e.encryptKey, {
    output: "string"
})
// ローカルシミュレーション
const sm4 = require('sm-crypto').sm4
sm4.decrypt(function (t) {
    for (var e = "", r = 0; r < t.length; r++) {
        var n = t[r].toString(16);
        1 === n.length && (n = "0" + n),
            e += n
    }
    return e
}(s.body), e.encryptKey, {
    output: "string"
})

最後に、様々な暗号化・復号アルゴリズムをライブラリで実装し、最終的なデータを取得するためのシミュレーションリクエストを行います。

まずレスポンスをbase64に変換し、その後JSでbase64文字列をarraybufferに変換します。

// base64文字列をarraybufferに変換
function base64ToArrayBuffer(base64_string) {
    var binary_string = atob(base64_string);
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}

シミュレーションリクエスト:

成功!!!

タグ: JSリバースエンジニアリング 暗号化 SM4 SM2 Node.js

6月30日 17:36 投稿