自分だけのクイズ作成 - Quipha公開中

【天鳳】牌譜の乱数種からハッシュ値の生成

C++
スポンサーリンク

はじめに

以前、天鳳の牌山の検証を行いました。

仕組みとしては、事前に公開されているハッシュ値と、対局後の牌譜から生成したハッシュ値を比較するものです。
この値は、乱数種をハッシュ化したものであり、また、乱数種から牌山を再現することができます。

つまり、牌山は対局の前に既に決まっており、対局中に牌操作が行われていないことが証明できます。

ただし現状、牌譜からハッシュ値を生成する方法は、天鳳のサイトの「牌譜URL to SHA512」を使用するしかありません。

オンライン対戦麻雀 天鳳 / 牌山乱数

現時点で、色々とググってみましたが、牌譜からハッシュ値の生成に成功している人はいませんでした。

今回は、牌譜の乱数種からハッシュ値の生成に成功しましたのでまとめました。

ハッシュについて

ざっくりハッシュそのものの特徴として、

  • あるデータをもとに、ハッシュ値を作れる。
  • 元データが同じであれば、同じハッシュ値が作れる。
  • ハッシュ値から元のデータに戻すことができない。(一方向性)

天鳳の場合、以下の情報をSHA512でハッシュ化しているとのことです。

SHA512のソースは4(座席並べ替え情報) + 624*8(乱数種) = 4996bytes
SHA512は牌譜と座席から再計算可能です

https://tenhou.net/stat/rand/

これしかヒントがなく、全く分からん・・

ハッシュ値とは、以下のような値です。

3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4

上記と同じハッシュ値を生成するには、ソースが必要になります。(SHA512を行う元の文字列)

ハッシュ値からは、ソースを取得できないため、色々と試してみます。
以降、結論を書いていますが、ここに辿り着くのにかなりの時間を費やしました・・😖

使用した対局

私の以下の対局を使用しました。

天鳳 / Web版

対局前に、ハッシュ値の一覧を取得しました。

天鳳のサイトの「牌譜URL to SHA512」を使用して、上記のオレンジ色のハッシュ値が取得できました。
以下のハッシュ値です。

3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4

対局後の牌譜の中に記載されている乱数種は以下でした。
この乱数種から、上記のハッシュ値を取得したいと思います。

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

乱数種のソース

牌譜の取得

まずは対局の牌譜を取得します。
有料会員であれば対局の牌譜のダウンロードは可能です。

オンライン対戦麻雀 天鳳 / HTML5+JS版牌譜ビューアβ

自分のIDを入力し、ダウンロードリンクから、mjlogファイルを取得することができます。

ダウンロードしたファイルはgzip圧縮されていますので、拡張子を変更して解凍します。

ファイルをテキストエディタで開くと、XML形式のデータを確認できます。
先頭部分に、対局で使用した乱数種が記載されています。

<mjloggm ver="2.3"><SHUFFLE seed="mt19937ar-sha512-n288-base64,乱数種" ref=""/>

ソースの取得

以降、C++でプログラミングしていきます。
設定の方法などは、以下の記事を参考にしてください。

乱数種のソースは以下とのこと。

624*8(乱数種) = 4992bytes

XMLに記載されている乱数種は、Base64でエンコードされているため、デコードを行います。
ソースの全ては最後にも記載しますが、乱数種と事前に公開されているハッシュ値を定義しています。

#include <iostream>
#include <windows.h>

#include <openssl/sha.h>
#include "base64.h"

#define MTRAND_N 624

// 乱数種
char MTseed_b64[] = "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";
// 事前に公開されているハッシュ値
char beforeHash[] = "3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4";

int main() {
    int i;

    // 2496byte+1(終了コード追加用の+1)
    unsigned char MTseed[MTRAND_N * 4 + 1]; 

    // base64でデコード
    base64Decode(MTseed_b64, (char*)MTseed);
}

デコードされた情報は、2496byte+1のデータが取れました。
このデータを16進の文字列で表示します。

// 乱数種のバイナリデータを16進の文字列で表示
// 2496byteを2文字の16進で表示→4992文字
char seedStr16[4992 + 1] = "";
// 終了コードは除く
for (int i = 0; i < sizeof(MTseed) - 1; ++i) {

    // 16進の文字列で代入
    char buf[3] = "";
    sprintf_s(buf, sizeof(buf), "%02x", MTseed[i]);

    // 文字列を格納
    strcat_s(seedStr16, buf);
}

// 16進の乱数種を表示
printf("seed:\r\n%s\r\n", seedStr16);

実行を行うと、乱数種の16進の文字列が取得できました。
以下をbytesと呼ぶとややこしいので「文字」と呼びますが、4992文字取得できました。

3833a3078d60fd1b823409d277626820cbe68e5f69ac98cf3c57b62aa3fe68e9bcafc2797261534755bae1912db00beb53689bc8953b6a77a376dc677d1bb289cc7bfe98bfd00f3373de70d564b996b3b08a35cb343184bebf56b9d7d5c3cbaa34e80c1e3956ce2c7b3518e421ed6fc1f38004cc7274f8936b34ebaf76e0d5fb32a68a5c656d7cf9d2615287e72c6d2d9e480780b80958f38fedaf8023e1aebd1be203c599c014f93402189719c5aa6731e507ccd849fb4a775029ce227da00cf741115c09f07d1d6d19c9760c8a8e0467c3ada841c68847d9337ca11cd53622bb5ff1cd87e16bd2539108f094f88e56547f7dcdd35aec7b994edf0cc9fc3073d63069d9494ae80624bda424e4d57a8870a42b604fcd7adf6e73e0da51d2043e770103886a0c04c92c7b8c87432ce1998ed5fa1619e2ebdfc96b16210b10931ee3471828c51abf775911b41508417abffae654893c72332eacf8d5bfe7f79ce659a92f9d43a45cb20f969c5b254e8c8f88453e5031af380420be692df434deffa4e50ea761d968c3b22195e616786560728483f81710e03e93b53fc02963eff9379f4dfb993b9967011cdc7c4682b41f5231b0d6d50aa151a615e3b3e23797308b0b742654a3b5481d06e95e491bc25ad467d0ee8e3addb6377b039e87507f200697f53525fbc0039f604c975e0895f00d77bb8582b5e265bd85d2e6e49209d299885e64b1e91bb1ae5bebf6fb8259eb7c7807e7068f7796cafae202494495168293d57f7ad846b54770cb2644f094853418365530bf3888694959fd06aec321b1e0f0defbb633116aef106309f053f7b8e5f15fa789a3179272d43519e47c56ee10d2c5a7aa5e8ecbd609302bf68a2f5c1cad962d3a72e3fce9791d03aa7055a1a086d44a5478c9faaa5875f8afe42a37413e447364fcac4b8c68afbe5c321735e90838eb22d64dd18fb5c4e355f331279ddfaf7cfaa72d308c441d1349e09a29a63178f872935b45f07d018f1855c25e4de6c0c8065cecd84e7c071920b583c3145de7a0c4f2b3955117d5901bb92405e1c0c632fcf9e4625e243cae07f9222269cd2e5551834b9b9db1ce2137373a05155cd44484adf57f576523f61e0d6fa4130f7a499ea8cc73036075a645effe578996ed22e4a7f708508e98dfffa12c96857d5801d3455fb33cb4de3f1dea4ae96676a19690212e4b1ed30a5f4e4d9451f2836581b4206227853d6be50cfb7c48b3f915e2dedbe066ff0e20c2cf47c401f28988ac1adc2cddca047c1a5e809bbdf9dda45b088efbd3e4d108600bd53f23db42e410a1b22fe33258b527343217b92e6a6ddddf3436958714f843eef0ae6bc5d03c13fe7dbbc2bd4f8acba512bb60a6ede45e17eae08e06ddc2c6d6fb4f0d0ba77bc5bd2a3e119c56f3fb37b9c6cca91aed6a6132c83315cd49e470e6d1fa681819fc6ea390c09a0853d0855d0f3ce0435c1cde3adae329bda9e874018b5de27f978bb0c965b1d26b67b7f7ffe5882184eb91847f2bb31f9b6f46ce3dd7ee23821a500e172eb6e29326d2aeb5d35a9a5b7ed4bfdfa38b32f64f23e366f243b8de98247086a1c8da943f218da376b337e282caae5499f8e1ebe57572236a8197d2903e3257321997f1a246bcf8818452dd6a97c0a361bef54a139c32639e6cbaf5d7517c2cfd1b8d702cb64174950885f40d2d107870c006e26ada3dd1b7dfa81b450c112cad3c45698e67f3221fb50e70e6c3b4cf8598c2287bad741e46ad9b69dafb461b1d90139b21636e66d14e19b7d9b4f8364b295ab35d2295642fa80f9a01ce4af29565460da4f54468e986df3d2a4a7925dcd4e7d29ab06ba72aa74e6be4d98e829afa30ac9b6f5f12ea4430e014e414d4322d7b35f269f33f4df7941238f7b53392fdce9e58dfb662fc12592fc668f72c8041b067ffc23323d652a59010754fd1fd38a5b3d888144f76c90fbb6b302e8d3c5e4116c6514dcb0d68378e8b462defef84f9ee4cd61a1490412a20b28004cb62708dfe352168e51abf98999b4dfba3961eb5c14e71167485526436dce6eb32053b05c27c82211de3c0c5ff18ca9c51f989d67ecc5bd669271e429591940f65f35f9db9df4c6dea4a389ca55036e16a44d2be5d26c379d711e156bd458c060c645629db486cd418b66afa62002c449de2f883865503c9aa971d3ca49e73dce8abe8e423091b27ec6e9d22ad1db2f09697617f45a2cba3915cd24bf0ff8f1cebd0b6224d35bbc7b07041c48e02fa1a440eef544c8a41b6eb837ffb2ab5b318325584eb46a21920020c73f7453c947202520e93a1d14e0fd87240089cf6c5804704c5449809c1a719256b015b05873f231833e3eb818f369d82b399192ac83a5dd4b223d6e74980306ac9c4855893687c113820d4ad1de4eb666c9be5edeceec50fd04b98484f283af1b2d016601f2355c986b938890f59f7e8d9ee1a35d4c1bcbca687d916e299d181dc8892e3b6675774a7fcb726c7e266ef261cdc3353473df27c46b8e268e3a1aacd0d9946fff7cb17e65b24d2a59a2853611ddf8a4b5c6373f77593da177b32b977efb34712b8a7df98589dd0b6d10fb780b16b7e597a7753b10350fb3fb3c13a868c5e9d62f25497f7381e221812a1dc5a6e23825111c9066e99938fdceab5648d38a59c31ddd6f1ccbb282f9b9ca6e3a762063e52fdc99740150131ab7f66a5283cb41324725f8088f5bfb1259553e53a526dbe0453ba153e6ffc75c5cc694f08d3fd602f9edc97b8ef09ec599791bc71f7bb13ca0c707e1e39a2f578fe7c8b67985d5d07279162b77d06133f827da767af46acd03de56ec17d04df5ab37f0bb83d6d08eb26f234e8699e576aee8f91547249c124e786049c73885eb7ada8e9be8a4d120b6a48365c377dc5fa760cee83e9aeafb137eb871a1f655e4da32fd513b8be42013175dc77d1d0d3669de6a46887afbdf6781e24baf0982a4b99ef62113f4cf2dc15b75d1abf8457eefa9a1323f6eae67150338dee3f59f6bde23d2cba2d5912812dffce4f9674fe2c19841a4a10487bace50ca7d1a39d9971e50fd3ff079fc2be006f66b4117ce46ae3e705b0a9c65afc60a7681c6673522eaacdf94fb2dc3aec230093133977824571695bd7b9a1de29ca12207d7bfb202dcd4ed024592736f5ee20afe191a6516204da670c43e9156cd11918a54c56fedc90631707a872130f8000766e50b415b614594a60caceef37d8c89aae09dfbfd360f16018ae8895234623a66308efca060ba1163f75ade6300506dd1e69ff498db71653dc3c48f017024d0e2f224f010f24f20068df48a4e2a78e98777f6784f10efb952f20ac95e815fbc370637ee0edabcf45f44c2916956d725038e8467ad7fd7fab0ee92ec164f28d74c1d542423d88bfa897926a8838e439b9908958e436199a6765fc49b57ec8a03169d5dcb3b0e494590bf4082f063a84ee8fff7d5c20bc56819ab86560f46d4fe7f869b40fe60e9ed8afeaa72ef332d783133e9f9d3234b1e2

ポイントとしては、大文字ではなく小文字の16進で取得します。
(この文字列をソースにSHA512でハッシュ化するので、大文字の場合、結果が変わるため)

この文字列が、乱数種のソースになります。

スポンサーリンク

座席並べ替え情報のソース

座席並べ替え情報のソースは以下とのこと。

4(座席並べ替え情報)= 4bytes

こちらについては、どうやって取得するのか分かりませんでした🤔
ただし乱数種と同じであれば、2bytesの4文字の16進文字列が取得できれば良いと推測しました。

2bytesは256*256で、65,536通りしか無いので、総当たりで取得したいと思います。

ハッシュ値の取得

実装

乱数種のソースは取得できたとして、座席並べ替え情報のソースは総当たりで、ハッシュ値を計算したいと思います。

????(座席並べ替え情報) + 624*8(乱数種) = SHA512のソース

これで、事前に取得したハッシュ値を生成することができれば、検証はOKです。
(対局後の牌譜から、事前に公開されているハッシュ値が生成できたことになるので)

ソースコードは以下です。

// 座席並べ替え情報は総当たりで計算
// 2byte: 256*256 = 65536
// 0000 ~ ffff
for (int i = 0; i < 65536; ++i) {

    // 座席並べ替え情報
    char buf[5];
    sprintf_s(buf, sizeof(buf), "%04x", i);

    // SHA512のソース(座席並べ替え情報 + 乱数種)
    // 4996文字
    char source[4996 + 1] = "";
    strcat_s(source, buf);
    strcat_s(source, seedStr16);

    // SHA512でハッシュ化
    unsigned char bufHash[SHA512_DIGEST_LENGTH];
    SHA512((BYTE*)source, sizeof(source) - 1, (BYTE*)bufHash);

    // ハッシュを16進文字列で取り出し(2文字ずつ)
    char hashStr16[SHA512_DIGEST_LENGTH * 2 + 1] = "";
    for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {

        char buf[3];
        sprintf_s(buf, sizeof(buf), "%02x", (unsigned char)bufHash[i]);
        strcat_s(hashStr16, buf);

    }

    // ハッシュの表示
    //printf("hash:%s\r\n", hashStr16);

    // ハッシュの比較
    if (!strcmp(beforeHash, hashStr16)) {
        // ハッシュを発見!
        printf("\r\n========\r\nHash Found!\r\nsource:%s", source);
        break;
    }
}

SHA512について補足

SHA512とはSHA-2の一種であり、出力は512ビット(64bytes)です。
1byteを16進の2文字で出力し、128文字のハッシュ値になります。

実行

ソースコードを実行してみましょう。
総当りと言ってもパターンが多くないので、直ぐにハッシュ値は見つかります。

========
Hash Found!
source:20133833a3078d60fd1b823409d277626820cbe68e5f69ac98cf3c57b62aa3fe68e9bcafc2797261534755bae1912db00beb53689bc8953b6a77a376dc677d1bb289cc7bfe98bfd00f3373de70d564b996b3b08a35cb343184bebf56b9d7d5c3cbaa34e80c1e3956ce2c7b3518e421ed6fc1f38004cc7274f8936b34ebaf76e0d5fb32a68a5c656d7cf9d2615287e72c6d2d9e480780b80958f38fedaf8023e1aebd1be203c599c014f93402189719c5aa6731e507ccd849fb4a775029ce227da00cf741115c09f07d1d6d19c9760c8a8e0467c3ada841c68847d9337ca11cd53622bb5ff1cd87e16bd2539108f094f88e56547f7dcdd35aec7b994edf0cc9fc3073d63069d9494ae80624bda424e4d57a8870a42b604fcd7adf6e73e0da51d2043e770103886a0c04c92c7b8c87432ce1998ed5fa1619e2ebdfc96b16210b10931ee3471828c51abf775911b41508417abffae654893c72332eacf8d5bfe7f79ce659a92f9d43a45cb20f969c5b254e8c8f88453e5031af380420be692df434deffa4e50ea761d968c3b22195e616786560728483f81710e03e93b53fc02963eff9379f4dfb993b9967011cdc7c4682b41f5231b0d6d50aa151a615e3b3e23797308b0b742654a3b5481d06e95e491bc25ad467d0ee8e3addb6377b039e87507f200697f53525fbc0039f604c975e0895f00d77bb8582b5e265bd85d2e6e49209d299885e64b1e91bb1ae5bebf6fb8259eb7c7807e7068f7796cafae202494495168293d57f7ad846b54770cb2644f094853418365530bf3888694959fd06aec321b1e0f0defbb633116aef106309f053f7b8e5f15fa789a3179272d43519e47c56ee10d2c5a7aa5e8ecbd609302bf68a2f5c1cad962d3a72e3fce9791d03aa7055a1a086d44a5478c9faaa5875f8afe42a37413e447364fcac4b8c68afbe5c321735e90838eb22d64dd18fb5c4e355f331279ddfaf7cfaa72d308c441d1349e09a29a63178f872935b45f07d018f1855c25e4de6c0c8065cecd84e7c071920b583c3145de7a0c4f2b3955117d5901bb92405e1c0c632fcf9e4625e243cae07f9222269cd2e5551834b9b9db1ce2137373a05155cd44484adf57f576523f61e0d6fa4130f7a499ea8cc73036075a645effe578996ed22e4a7f708508e98dfffa12c96857d5801d3455fb33cb4de3f1dea4ae96676a19690212e4b1ed30a5f4e4d9451f2836581b4206227853d6be50cfb7c48b3f915e2dedbe066ff0e20c2cf47c401f28988ac1adc2cddca047c1a5e809bbdf9dda45b088efbd3e4d108600bd53f23db42e410a1b22fe33258b527343217b92e6a6ddddf3436958714f843eef0ae6bc5d03c13fe7dbbc2bd4f8acba512bb60a6ede45e17eae08e06ddc2c6d6fb4f0d0ba77bc5bd2a3e119c56f3fb37b9c6cca91aed6a6132c83315cd49e470e6d1fa681819fc6ea390c09a0853d0855d0f3ce0435c1cde3adae329bda9e874018b5de27f978bb0c965b1d26b67b7f7ffe5882184eb91847f2bb31f9b6f46ce3dd7ee23821a500e172eb6e29326d2aeb5d35a9a5b7ed4bfdfa38b32f64f23e366f243b8de98247086a1c8da943f218da376b337e282caae5499f8e1ebe57572236a8197d2903e3257321997f1a246bcf8818452dd6a97c0a361bef54a139c32639e6cbaf5d7517c2cfd1b8d702cb64174950885f40d2d107870c006e26ada3dd1b7dfa81b450c112cad3c45698e67f3221fb50e70e6c3b4cf8598c2287bad741e46ad9b69dafb461b1d90139b21636e66d14e19b7d9b4f8364b295ab35d2295642fa80f9a01ce4af29565460da4f54468e986df3d2a4a7925dcd4e7d29ab06ba72aa74e6be4d98e829afa30ac9b6f5f12ea4430e014e414d4322d7b35f269f33f4df7941238f7b53392fdce9e58dfb662fc12592fc668f72c8041b067ffc23323d652a59010754fd1fd38a5b3d888144f76c90fbb6b302e8d3c5e4116c6514dcb0d68378e8b462defef84f9ee4cd61a1490412a20b28004cb62708dfe352168e51abf98999b4dfba3961eb5c14e71167485526436dce6eb32053b05c27c82211de3c0c5ff18ca9c51f989d67ecc5bd669271e429591940f65f35f9db9df4c6dea4a389ca55036e16a44d2be5d26c379d711e156bd458c060c645629db486cd418b66afa62002c449de2f883865503c9aa971d3ca49e73dce8abe8e423091b27ec6e9d22ad1db2f09697617f45a2cba3915cd24bf0ff8f1cebd0b6224d35bbc7b07041c48e02fa1a440eef544c8a41b6eb837ffb2ab5b318325584eb46a21920020c73f7453c947202520e93a1d14e0fd87240089cf6c5804704c5449809c1a719256b015b05873f231833e3eb818f369d82b399192ac83a5dd4b223d6e74980306ac9c4855893687c113820d4ad1de4eb666c9be5edeceec50fd04b98484f283af1b2d016601f2355c986b938890f59f7e8d9ee1a35d4c1bcbca687d916e299d181dc8892e3b6675774a7fcb726c7e266ef261cdc3353473df27c46b8e268e3a1aacd0d9946fff7cb17e65b24d2a59a2853611ddf8a4b5c6373f77593da177b32b977efb34712b8a7df98589dd0b6d10fb780b16b7e597a7753b10350fb3fb3c13a868c5e9d62f25497f7381e221812a1dc5a6e23825111c9066e99938fdceab5648d38a59c31ddd6f1ccbb282f9b9ca6e3a762063e52fdc99740150131ab7f66a5283cb41324725f8088f5bfb1259553e53a526dbe0453ba153e6ffc75c5cc694f08d3fd602f9edc97b8ef09ec599791bc71f7bb13ca0c707e1e39a2f578fe7c8b67985d5d07279162b77d06133f827da767af46acd03de56ec17d04df5ab37f0bb83d6d08eb26f234e8699e576aee8f91547249c124e786049c73885eb7ada8e9be8a4d120b6a48365c377dc5fa760cee83e9aeafb137eb871a1f655e4da32fd513b8be42013175dc77d1d0d3669de6a46887afbdf6781e24baf0982a4b99ef62113f4cf2dc15b75d1abf8457eefa9a1323f6eae67150338dee3f59f6bde23d2cba2d5912812dffce4f9674fe2c19841a4a10487bace50ca7d1a39d9971e50fd3ff079fc2be006f66b4117ce46ae3e705b0a9c65afc60a7681c6673522eaacdf94fb2dc3aec230093133977824571695bd7b9a1de29ca12207d7bfb202dcd4ed024592736f5ee20afe191a6516204da670c43e9156cd11918a54c56fedc90631707a872130f8000766e50b415b614594a60caceef37d8c89aae09dfbfd360f16018ae8895234623a66308efca060ba1163f75ade6300506dd1e69ff498db71653dc3c48f017024d0e2f224f010f24f20068df48a4e2a78e98777f6784f10efb952f20ac95e815fbc370637ee0edabcf45f44c2916956d725038e8467ad7fd7fab0ee92ec164f28d74c1d542423d88bfa897926a8838e439b9908958e436199a6765fc49b57ec8a03169d5dcb3b0e494590bf4082f063a84ee8fff7d5c20bc56819ab86560f46d4fe7f869b40fe60e9ed8afeaa72ef332d783133e9f9d3234b1e2

コード上では、ハッシュ値が見つかった場合、SHA512のソース文字列を表示するようにしました。
ちなみに今回の座席並べ替え情報は、「2013」の四文字でした。

追記:
座席並べ替え情報は、「0123」で四人の座席を表しているのかもしれません。
そうすると24パターンだけで良いかもしれないですね。

このソースを、SHA512に変換するサイトで試してみましょう。

SHA512ハッシュ生成ツール | ハッシュジェネレータ
SHA512ハッシュの生成ツールです。

事前に公開されていたハッシュ値を生成することができました。

ソース全体

#include <iostream>
#include <windows.h>

#include <openssl/sha.h>
#include "base64.h"

#define MTRAND_N 624

// 乱数種
char MTseed_b64[] = "ODOjB41g/RuCNAnSd2JoIMvmjl9prJjPPFe2KqP+aOm8r8J5cmFTR1W64ZEtsAvrU2ibyJU7anejdtxnfRuyicx7/pi/0A8zc95w1WS5lrOwijXLNDGEvr9WudfVw8uqNOgMHjlWzix7NRjkIe1vwfOABMxydPiTazTrr3bg1fsypopcZW18+dJhUofnLG0tnkgHgLgJWPOP7a+AI+GuvRviA8WZwBT5NAIYlxnFqmcx5QfM2En7SndQKc4ifaAM90ERXAnwfR1tGcl2DIqOBGfDrahBxohH2TN8oRzVNiK7X/HNh+Fr0lORCPCU+I5WVH99zdNa7HuZTt8Myfwwc9YwadlJSugGJL2kJOTVeohwpCtgT816325z4NpR0gQ+dwEDiGoMBMkse4yHQyzhmY7V+hYZ4uvfyWsWIQsQkx7jRxgoxRq/d1kRtBUIQXq/+uZUiTxyMy6s+NW/5/ec5lmpL51DpFyyD5acWyVOjI+IRT5QMa84BCC+aS30NN7/pOUOp2HZaMOyIZXmFnhlYHKEg/gXEOA+k7U/wClj7/k3n037mTuZZwEc3HxGgrQfUjGw1tUKoVGmFeOz4jeXMIsLdCZUo7VIHQbpXkkbwlrUZ9Dujjrdtjd7A56HUH8gBpf1NSX7wAOfYEyXXgiV8A13u4WCteJlvYXS5uSSCdKZiF5ksekbsa5b6/b7glnrfHgH5waPd5bK+uICSUSVFoKT1X962Ea1R3DLJkTwlIU0GDZVML84iGlJWf0GrsMhseDw3vu2MxFq7xBjCfBT97jl8V+niaMXknLUNRnkfFbuENLFp6pejsvWCTAr9oovXBytli06cuP86XkdA6pwVaGghtRKVHjJ+qpYdfiv5Co3QT5Ec2T8rEuMaK++XDIXNekIOOsi1k3Rj7XE41XzMSed3698+qctMIxEHRNJ4JoppjF4+HKTW0XwfQGPGFXCXk3mwMgGXOzYTnwHGSC1g8MUXeegxPKzlVEX1ZAbuSQF4cDGMvz55GJeJDyuB/kiImnNLlVRg0ubnbHOITc3OgUVXNREhK31f1dlI/YeDW+kEw96SZ6ozHMDYHWmRe/+V4mW7SLkp/cIUI6Y3/+hLJaFfVgB00Vfszy03j8d6krpZnahlpAhLkse0wpfTk2UUfKDZYG0IGInhT1r5Qz7fEiz+RXi3tvgZv8OIMLPR8QB8omIrBrcLN3KBHwaXoCbvfndpFsIjvvT5NEIYAvVPyPbQuQQobIv4zJYtSc0Mhe5Lmpt3d80NpWHFPhD7vCua8XQPBP+fbvCvU+Ky6USu2Cm7eReF+rgjgbdwsbW+08NC6d7xb0qPhGcVvP7N7nGzKka7WphMsgzFc1J5HDm0fpoGBn8bqOQwJoIU9CFXQ884ENcHN462uMpvanodAGLXeJ/l4uwyWWx0mtnt/f/5YghhOuRhH8rsx+bb0bOPdfuI4IaUA4XLrbikybSrrXTWppbftS/36OLMvZPI+Nm8kO43pgkcIahyNqUPyGNo3azN+KCyq5Umfjh6+V1ciNqgZfSkD4yVzIZl/GiRrz4gYRS3WqXwKNhvvVKE5wyY55suvXXUXws/RuNcCy2QXSVCIX0DS0QeHDABuJq2j3Rt9+oG0UMESytPEVpjmfzIh+1DnDmw7TPhZjCKHutdB5GrZtp2vtGGx2QE5shY25m0U4Zt9m0+DZLKVqzXSKVZC+oD5oBzkrylWVGDaT1RGjpht89Kkp5JdzU59KasGunKqdOa+TZjoKa+jCsm29fEupEMOAU5BTUMi17NfJp8z9N95QSOPe1M5L9zp5Y37Zi/BJZL8Zo9yyAQbBn/8IzI9ZSpZAQdU/R/Tils9iIFE92yQ+7azAujTxeQRbGUU3LDWg3jotGLe/vhPnuTNYaFJBBKiCygATLYnCN/jUhaOUav5iZm037o5YetcFOcRZ0hVJkNtzm6zIFOwXCfIIhHePAxf8YypxR+YnWfsxb1mknHkKVkZQPZfNfnbnfTG3qSjicpVA24WpE0r5dJsN51xHhVr1FjAYMZFYp20hs1Bi2avpiACxEneL4g4ZVA8mqlx08pJ5z3Oir6OQjCRsn7G6dIq0dsvCWl2F/RaLLo5Fc0kvw/48c69C2Ik01u8ewcEHEjgL6GkQO71RMikG264N/+yq1sxgyVYTrRqIZIAIMc/dFPJRyAlIOk6HRTg/YckAInPbFgEcExUSYCcGnGSVrAVsFhz8jGDPj64GPNp2Cs5kZKsg6XdSyI9bnSYAwasnEhViTaHwROCDUrR3k62Zsm+Xt7O7FD9BLmEhPKDrxstAWYB8jVcmGuTiJD1n36NnuGjXUwby8pofZFuKZ0YHciJLjtmdXdKf8tybH4mbvJhzcM1NHPfJ8RrjiaOOhqs0NmUb/98sX5lsk0qWaKFNhHd+KS1xjc/d1k9oXezK5d++zRxK4p9+YWJ3QttEPt4Cxa35ZendTsQNQ+z+zwTqGjF6dYvJUl/c4HiIYEqHcWm4jglERyQZumZOP3Oq1ZI04pZwx3dbxzLsoL5ucpuOnYgY+Uv3Jl0AVATGrf2alKDy0EyRyX4CI9b+xJZVT5TpSbb4EU7oVPm/8dcXMaU8I0/1gL57cl7jvCexZl5G8cfe7E8oMcH4eOaL1eP58i2eYXV0HJ5Fit30GEz+Cfadnr0as0D3lbsF9BN9as38LuD1tCOsm8jToaZ5Xau6PkVRyScEk54YEnHOIXretqOm+ik0SC2pINlw3fcX6dgzug+mur7E364caH2VeTaMv1RO4vkIBMXXcd9HQ02ad5qRoh6+99ngeJLrwmCpLme9iET9M8twVt10av4RX7vqaEyP26uZxUDON7j9Z9r3iPSy6LVkSgS3/zk+WdP4sGYQaShBIe6zlDKfRo52ZceUP0/8Hn8K+AG9mtBF85Grj5wWwqcZa/GCnaBxmc1Iuqs35T7LcOuwjAJMTOXeCRXFpW9e5od4pyhIgfXv7IC3NTtAkWSc29e4gr+GRplFiBNpnDEPpFWzRGRilTFb+3JBjFweochMPgAB2blC0FbYUWUpgys7vN9jImq4J37/TYPFgGK6IlSNGI6ZjCO/KBguhFj91reYwBQbdHmn/SY23FlPcPEjwFwJNDi8iTwEPJPIAaN9IpOKnjph3f2eE8Q77lS8grJXoFfvDcGN+4O2rz0X0TCkWlW1yUDjoRnrX/X+rDukuwWTyjXTB1UJCPYi/qJeSaog45Dm5kIlY5DYZmmdl/Em1fsigMWnV3LOw5JRZC/QILwY6hO6P/31cILxWgZq4ZWD0bU/n+Gm0D+YOntiv6qcu8zLXgxM+n50yNLHi";
// 事前に公開されているハッシュ値
char beforeHash[] = "3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4";

int main() {
    int i;

    // 2496byte+1(終了コード追加用の+1)
    unsigned char MTseed[MTRAND_N * 4 + 1]; 

    // base64でデコード
    base64Decode(MTseed_b64, (char*)MTseed);

    // 乱数種のバイナリデータを16進の文字列で表示
    // 2496byteを2文字の16進で表示→4992文字
    char seedStr16[4992 + 1] = "";
    // 終了コードは除く
    for (int i = 0; i < sizeof(MTseed) - 1; ++i) {

        // 16進の文字列で代入
        char buf[3] = "";
        sprintf_s(buf, sizeof(buf), "%02x", MTseed[i]);

        // 文字列を格納
        strcat_s(seedStr16, buf);
    }

    // 16進の乱数種を表示
    //printf("seed:\r\n%s\r\n", seedStr16);

    // 座席並べ替え情報は総当たりで計算
    // 2byte: 256*256 = 65536
    // 0000 ~ ffff
    for (int i = 0; i < 65536; ++i) {

        // 座席並べ替え情報
        char buf[5];
        sprintf_s(buf, sizeof(buf), "%04x", i);

        // SHA512のソース(座席並べ替え情報 + 乱数種)
        // 4996文字
        char source[4996 + 1] = "";
        strcat_s(source, buf);
        strcat_s(source, seedStr16);

        // SHA512でハッシュ化
        unsigned char bufHash[SHA512_DIGEST_LENGTH];
        SHA512((BYTE*)source, sizeof(source) - 1, (BYTE*)bufHash);

        // ハッシュを16進文字列で取り出し(2文字ずつ)
        char hashStr16[SHA512_DIGEST_LENGTH * 2 + 1] = "";
        for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {

            char buf[3];
            sprintf_s(buf, sizeof(buf), "%02x", (unsigned char)bufHash[i]);
            strcat_s(hashStr16, buf);

        }

        // ハッシュの表示
        //printf("hash:%s\r\n", hashStr16);

        // ハッシュの比較
        if (!strcmp(beforeHash, hashStr16)) {
            // ハッシュを発見!
            printf("\r\n========\r\nHash Found!\r\nsource:%s", source);
            break;
        }
    }

    return 0;
}

さいごに

今回は、牌譜からハッシュ値の生成を検証しました。
天鳳では、牌操作が無いことを証明できたと思います。

「配牌を選択して提供することは不可能」
「次のツモ牌をすりかえることは不可能」
「牌山を見て乱数種を逆算することは不可能」

https://tenhou.net/support.html

ということで、天鳳やろっと✋

コメント

タイトルとURLをコピーしました