totsugeki icon indicating copy to clipboard operation
totsugeki copied to clipboard

Broken by December patch?

Open halvnykterist opened this issue 2 years ago • 30 comments

Totsugeki doens't seem functional after the December patch. On one hand, the speedups aren't really necessary any more, but the rating-update integration is nice and I was hoping to use totsugeki to help decode the new API - they seem to have done something other than just change the version number and I'm having trouble reverse engineering it.

halvnykterist avatar Dec 16 '22 19:12 halvnykterist

While connection times aren't obcenely bad anymore, Totsugeki was still better and stations still fail way too often compared to just hop and play.

fefo-dev avatar Dec 19 '22 01:12 fefo-dev

I've started looking into this & have so far been able to capture & decrypt the api traffic successfully using other tools. For example, I can see a (possibly) new endpoint /api/user/save_cross_user_id that occurs immediately after the expected /api/user/login.

(However, I hadn't looked into this before the current patch, so forgive me if this was already present & well-known)

If anyone knows, what are the steps needed to fix Totsugeki (mostly for the purpose of suporting @halvnykterist's rating update)? I hadn't used Totsugeki or looked at the internals before today, so any clues are appreciated. Otherwise, I'll keep working on trying to reverse the api changes & post updates when I can.

crtado avatar Dec 20 '22 21:12 crtado

Fantastic! Can you share which tools you're using? The route I'm mainly interested in is /api/catalog/get_replay, although other paths might be interesting too for getting additional data on users.

halvnykterist avatar Dec 20 '22 22:12 halvnykterist

Fantastic! Can you share which tools you're using?

I used proxifier to proxy all (Or just GGST) traffic through to polarproxy which decrypts & writes to a .pcap file for inspection in wireshark.

The route I'm mainly interested in is /api/catalog/get_replay, although other paths might be interesting too for getting additional data on users.

I'll poke around the get_replay endpoint then too. I'll start by toggling the new "platform" field in the replay search-menu & see what ticks. At minimum, there's likely a new u8 field corresponding to 'all' vs 'same' platform in the RequestQuery structure.

crtado avatar Dec 21 '22 02:12 crtado

So I went and used this method and am I crazy or are these no longer msgpack?

a few examples:

data=W-SV4L61mtkFzNJ90I9y3_yn062-YQk9Feo01Ce34lyfabRryOdJRSe5A4kQbcy3rz5UjoW8r0-7YQQYtyMgm1lRZzwHbgX23A

data=Ye_3VzHXDZ-hXZ-7KdGtJpiELTjrLuDQWmSfqgovV1zjPKBf0ieU_ip_G90gPW064XT7dVEBf7JWpWL8V-tvqG6k3k0b9ocheH8kUNef-7PR1dpVj59r

data=jyTGYmxdFS4iP8IAQMasVX48yFGCDc9PqD1lWIqEQ50wRo4hXhNwWOkJ1poB_1givDa2cDCitn-3-B443wGTs9aQCs8Gif2X7JB3s9l78IJ5VUb8aDS9

data=AQ8fqyHCGzSTGGDc6H5hxOicIWO_tJJQ5xlD0IOJIzS1pNt_EBCY673EGFaEWH_0FsR2f5NvO5UgV7-JoVQrat6QuoTOh9xC5UJOJJb0pCJKL6rIJU1h

data=wedlqiPPTpbo4DAkfWfRDlMaCJThbgCj5GGjW9WsfZJpRMAHdnpDwI1EJ018POH3va4upQWso3nnz4iMUZcgA9EM2KJ0KOqDN6fqAYQKFSoYPHXN0W7I

Theoretical avatar Dec 21 '22 18:12 Theoretical

I don't have any prior experience with messagepack, but using your first example & doing a quick test with cyberchef. I can't seem to decode the data as messagepack either.

However, I don't any pre-patch data to compare it to so I cannot say for sure that I haven't just made a mistake.

crtado avatar Dec 21 '22 18:12 crtado

Perhaps they moved to protobuf instead?

Jyosua avatar Dec 22 '22 02:12 Jyosua

After a long ass day I figured it out!

it's still using msgpack but it's actually encrypted now!

they added GCM encryption to the requests, i've dumped they key and managed to decrypt outgoing requests, here's a small python script w/ info + examples.

from binascii import hexlify, unhexlify
from base64 import urlsafe_b64decode
from Cryptodome.Cipher import AES

# key obtained from hooking strive EVP_EncryptInit_ex_0
# RVA: 0x3036460
# Encoding + Concating RVA: 0xB248D0
# GGST Timestamp: 63906742
key = unhexlify('EEBC1F57487F51921C0465665F8AE6D1658BB26DE6F8A069A3520293A572078F')

# examples from my pcap dump for catalog/get_replay
examples = [
    'Ye_3VzHXDZ-hXZ-7KdGtJpiELTjrLuDQWmSfqgovV1zjPKBf0ieU_ip_G90gPW064XT7dVEBf7JWpWL8V-tvqG6k3k0b9ocheH8kUNef-7PR1dpVj59r',
    'jyTGYmxdFS4iP8IAQMasVX48yFGCDc9PqD1lWIqEQ50wRo4hXhNwWOkJ1poB_1givDa2cDCitn-3-B443wGTs9aQCs8Gif2X7JB3s9l78IJ5VUb8aDS9',
    'AQ8fqyHCGzSTGGDc6H5hxOicIWO_tJJQ5xlD0IOJIzS1pNt_EBCY673EGFaEWH_0FsR2f5NvO5UgV7-JoVQrat6QuoTOh9xC5UJOJJb0pCJKL6rIJU1h',
    'wedlqiPPTpbo4DAkfWfRDlMaCJThbgCj5GGjW9WsfZJpRMAHdnpDwI1EJ018POH3va4upQWso3nnz4iMUZcgA9EM2KJ0KOqDN6fqAYQKFSoYPHXN0W7I'
]

for example in examples:
    decoded = urlsafe_b64decode(example)
    iv = decoded[:12]

    cipher = AES.new(key, AES.MODE_GCM, iv)
    decrypted = cipher.decrypt(decoded[12:])
    print(f"Message pack: \n{hexlify(decrypted)}\n")```

Theoretical avatar Dec 22 '22 06:12 Theoretical

Damn, you beat me to the punch! Good idea on hooking EVP_EncryptInit.

crtado avatar Dec 22 '22 07:12 crtado

my dumb self originally hooked decrypt and stumbled on IV for a bit too long :( but glad we got it!

Theoretical avatar Dec 22 '22 08:12 Theoretical

Nice! Going to write up some stuff locally to hopefully encrypt/decrypt requests. Do you think the responses work in a similar way? I've noticed that the responses for replays all seem to start with 0020000000000000d5

halvnykterist avatar Dec 22 '22 09:12 halvnykterist

Yes, it seems to work similarly for responses. Here is one decrypted response from my traffic last night from the endpoint /api/catalog/get_replay:

[
    [
        "63a3eedaa0aac",
        0,
        "2022/12/22 05:44:58",
        "0.1.7",
        "0.0.2",
        "0.0.2",
        "",
        ""
    ],
    [
        0,
        0,
        5,
        [
            [
                221222054453618850,
                15,
                99,
                21,
                12,
                [
                    "210608040603294336",
                    "dlegooh",
                    "6239017313081128764",
                    "dlegooh_",
                    1,
                    9
                ],
                [
                    "210611072546981835",
                    "Waeve",
                    "76561197988647455",
                    "110000101b1121f",
                    3,
                    9
                ],
                2,
                "2022-12-22 05:44:53",
                1,
                0,
                0,
                0
            ],
            [
                221222054443297380,
                15,
                9,
                12,
                21,
                [
                    "210814203235952623",
                    "GOTE-SAMA",
                    "76561198216174113",
                    "11000010f40da21",
                    3,
                    9
                ],
                [
                    "210719213919866739",
                    "Cloud Strife",
                    "2942875306480629096",
                    "TheREalCloud160",
                    1,
                    6
                ],
                1,
                "2022-12-22 05:44:45",
                1,
                0,
                0,
                0
            ],
            [
                221222054340881020,
                15,
                10,
                21,
                12,
                [
                    "211229080611433091",
                    "CrippledCart",
                    "76561198192746828",
                    "11000010ddb614c",
                    3,
                    9
                ],
                [
                    "210608222155215755",
                    "Name to go",
                    "3719338700773288793",
                    "Muellerboyz2",
                    1,
                    9
                ],
                2,
                "2022-12-22 05:43:40",
                1,
                0,
                0,
                0
            ],
            [
                221222054316694900,
                15,
                99,
                12,
                21,
                [
                    "210626192602429871",
                    "AS",
                    "76561198325175998",
                    "110000115c016be",
                    3,
                    9
                ],
                [
                    "210611191934397127",
                    "Front-Ward",
                    "76561198354052885",
                    "11000011778b715",
                    3,
                    9
                ],
                1,
                "2022-12-22 05:43:22",
                1,
                0,
                0,
                0
            ],
            [
                221222054306381100,
                15,
                99,
                21,
                12,
                [
                    "210608040603294336",
                    "dlegooh",
                    "6239017313081128764",
                    "dlegooh_",
                    1,
                    9
                ],
                [
                    "210611072546981835",
                    "Waeve",
                    "76561197988647455",
                    "110000101b1121f",
                    3,
                    9
                ],
                2,
                "2022-12-22 05:43:06",
                1,
                0,
                0,
                0
            ]
        ]
    ]
]

crtado avatar Dec 22 '22 13:12 crtado

The above can be reproduced with a slight tweak to @Theoretical's snippet:

from binascii import unhexlify
from Crypto.Cipher import AES  # pycryptodome
import json
import msgpack

# key obtained from hooking strive EVP_EncryptInit_ex_0
# RVA: 0x3036460
# Encoding + Concating RVA: 0xB248D0
# GGST Timestamp: 63906742
key = unhexlify('EEBC1F57487F51921C0465665F8AE6D1658BB26DE6F8A069A3520293A572078F')

# response from pcap dump for /api/catalog/get_replay
# NOTE: raw hex stream
response = '3a5da56a5d64459fea61c12b2a3a8752ab7263b07014a9bd157364bc4ad351239362dd8eb8ab2ed8637d369e7bbbb2a63094ede47742a5d59714084a1fb34851fb50779e09184854564910d36a964dc2df37a9f424e698bd613c1575bfaad1be42a4d43ec2825302d67040ba6cc3f471f6b19b809c9c21b0815f610fb141da15594c5eecf26031823469edd0d1fe57615850fab324f90b0ebbf023a1ea3886bb52c9d4e878b56f5034ae036bf96bda786024784e5f7fd14b620c217e0cbb42295560efd9614fa583d9e1e6c4b015ce06d7d9e200b1a11e1c9bab59e5b4b8b8215333ae9b089639b4b9594968c27e46cb4967b0189599da91973f644807f2eecdcc13b76e734c9e00ce25a37e43174b62edd036c21e316372c0d3d3bc8c51e30bba7486a8f63fb65c4bbaa4c870971ce34945610717296d5613fddbef474752c99a11b965fc270e902db2621f884020de77ee60738efbcecd591f4a62a5168b8548b9151e12351e073c4fef26c5653c1a6164a6e3536651f6711c6fe26e028cc21d9d26873f74cffaa0a0e4f471d27d91d2ae9d2f6c466b9cb5ab33e69cbfeb342d94a36b6603c1b94617fea67ff10413aafa58ce50873b73c61e104703bd396de07b8a6ad99ca9aa6a3fb91dc177871c8d7dffc3c8fc4df47ed2ad6950350074b8959dcfeba24c11e76fe63591f034653479e28b8d416e2ad8d8a05e4a4205ecb09a1bf06a3efcc39370bc5c6efc5c74c27a6a01f3336bd926842c15558d9c7648a43e9c5e7ac13401787c2b303365b2f9d0118a453ad4bb0bae3c58ec93ea0410e72657d2e48b9951447adc707c704a6ec2ff7b371724adf980bfae8646a3c9595b3271984412d0cf0d51851355ceef91c62475bcc65012b2a1e98db6f7515bb7de4b52cf42ea1cf0bcef0d736023253f3972a54b60d4e0dbbae9c4b62f95bddb5cb04f2f27bc150082f84d6a98a3f689a275f551e812a1003cd925a1672ff1001ef973ea570703c3814d4bfaf78babd6aa996f6ecfdc800790035d0a9c1c85a0626419c7b4320a6907d2dce53fe282f3799d6c77fdff19c9722a2c206932a26513b1679350c4818574260f82dd2f066a11977765dfa23663802a4ee26171a81255c0efab279873116cdcc1f565c365fd23794178dcaa96658abcfd4576cb976ef0e14d91d2b372ff2358fa8456fef689ae660f94ebfb16f5404ed661e4157373569b6a4c79a0579bd7e30243e22ca4b7502c23165cf8714a92a840bb2f1fd68f20f44084cf55b0b325bb4917bbc5b11cb7505920a87d187e7f5e547c'

decoded = unhexlify(response)
iv = decoded[:12]

cipher = AES.new(key, AES.MODE_GCM, iv)
decrypted = cipher.decrypt(decoded[12:])
# NOTE: the last 16 bytes are not part of the decrypted data (auth tag?)
print(json.dumps(msgpack.unpackb(decrypted[:-16]), indent=4))

Or by just pasting your response data hex bytes directly into this cyberchef recipe

crtado avatar Dec 22 '22 14:12 crtado

Looks like there's another 16 bytes appended to a request though :(

Gotta figure this last part out really fast!

Theoretical avatar Dec 22 '22 14:12 Theoretical

Looks like there's another 16 bytes appended to a request though :(

I believe that's just GCM authentication tag that gets appended to the ciphertext

crtado avatar Dec 22 '22 14:12 crtado

I tried adding that but it doesn't seem like it actually, hit me up on discord real fast: Inve#0001

Theoretical avatar Dec 22 '22 14:12 Theoretical

It appears to be working!

Here's the code for building/sending a request. The important part is the get_replays_request function.


from base64 import urlsafe_b64decode, urlsafe_b64encode
from binascii import unhexlify, hexlify
from Crypto.Cipher import AES  # pycryptodome
from Crypto.Random import get_random_bytes
import json
import msgpack
import requests

# key obtained from hooking strive EVP_EncryptInit_ex_0
# RVA: 0x3036460
# Encoding + Concating RVA: 0xB248D0
# GGST Timestamp: 63906742
key = unhexlify('EEBC1F57487F51921C0465665F8AE6D1658BB26DE6F8A069A3520293A572078F')


def main():
    get_replays_request()


# Test request for /api/catalog/get_replay
# '5277536c36544a4f55727359624263437963506a6a47736f6e396b356158766b3470463755326c5f6b536449516479446e5954595464594b5f5563795f734e366c4432456b4146524e70476d68466266347734337a5a474f7345354665316d736d6948514832646b64705138475f30362d616151'  # 00
# '675933724531555a493365766a67766274357753625061545542467762747770436451686f4a51335175554e2d30616861586130387262595358793537317236765848314f72633344736c437559735835504b57556a41665a6f63686e5472497266786b655277745174396c624d4d7038714f63'  # 00
#
# Decrypt request data, provided as hex string
def decrypt_request_data(data):
    decoded = urlsafe_b64decode(unhexlify(data))
    iv = decoded[:12]
    cipher = AES.new(key, AES.MODE_GCM, iv)
    decrypted = cipher.decrypt(decoded[12:])
    return msgpack.unpackb(decrypted[:-16])


# Test response for /api/catalog/get_replay
# '3a5da56a5d64459fea61c12b2a3a8752ab7263b07014a9bd157364bc4ad351239362dd8eb8ab2ed8637d369e7bbbb2a63094ede47742a5d59714084a1fb34851fb50779e09184854564910d36a964dc2df37a9f424e698bd613c1575bfaad1be42a4d43ec2825302d67040ba6cc3f471f6b19b809c9c21b0815f610fb141da15594c5eecf26031823469edd0d1fe57615850fab324f90b0ebbf023a1ea3886bb52c9d4e878b56f5034ae036bf96bda786024784e5f7fd14b620c217e0cbb42295560efd9614fa583d9e1e6c4b015ce06d7d9e200b1a11e1c9bab59e5b4b8b8215333ae9b089639b4b9594968c27e46cb4967b0189599da91973f644807f2eecdcc13b76e734c9e00ce25a37e43174b62edd036c21e316372c0d3d3bc8c51e30bba7486a8f63fb65c4bbaa4c870971ce34945610717296d5613fddbef474752c99a11b965fc270e902db2621f884020de77ee60738efbcecd591f4a62a5168b8548b9151e12351e073c4fef26c5653c1a6164a6e3536651f6711c6fe26e028cc21d9d26873f74cffaa0a0e4f471d27d91d2ae9d2f6c466b9cb5ab33e69cbfeb342d94a36b6603c1b94617fea67ff10413aafa58ce50873b73c61e104703bd396de07b8a6ad99ca9aa6a3fb91dc177871c8d7dffc3c8fc4df47ed2ad6950350074b8959dcfeba24c11e76fe63591f034653479e28b8d416e2ad8d8a05e4a4205ecb09a1bf06a3efcc39370bc5c6efc5c74c27a6a01f3336bd926842c15558d9c7648a43e9c5e7ac13401787c2b303365b2f9d0118a453ad4bb0bae3c58ec93ea0410e72657d2e48b9951447adc707c704a6ec2ff7b371724adf980bfae8646a3c9595b3271984412d0cf0d51851355ceef91c62475bcc65012b2a1e98db6f7515bb7de4b52cf42ea1cf0bcef0d736023253f3972a54b60d4e0dbbae9c4b62f95bddb5cb04f2f27bc150082f84d6a98a3f689a275f551e812a1003cd925a1672ff1001ef973ea570703c3814d4bfaf78babd6aa996f6ecfdc800790035d0a9c1c85a0626419c7b4320a6907d2dce53fe282f3799d6c77fdff19c9722a2c206932a26513b1679350c4818574260f82dd2f066a11977765dfa23663802a4ee26171a81255c0efab279873116cdcc1f565c365fd23794178dcaa96658abcfd4576cb976ef0e14d91d2b372ff2358fa8456fef689ae660f94ebfb16f5404ed661e4157373569b6a4c79a0579bd7e30243e22ca4b7502c23165cf8714a92a840bb2f1fd68f20f44084cf55b0b325bb4917bbc5b11cb7505920a87d187e7f5e547c'
#
# Decrypt response data, provided as hex string
def decrypt_response_data(data):
    decoded = unhexlify(data)
    iv = decoded[:12]
    cipher = AES.new(key, AES.MODE_GCM, iv)
    decrypted = cipher.decrypt(decoded[12:])
    return msgpack.unpackb(decrypted[:-16])


# Test decryption for request and response 
def test_decrypt():
    # Test request decryption
    request = '5277536c36544a4f55727359624263437963506a6a47736f6e396b356158766b3470463755326c5f6b536449516479446e5954595464594b5f5563795f734e366c4432456b4146524e70476d68466266347734337a5a474f7345354665316d736d6948514832646b64705138475f30362d616151'
    clear_req = decrypt_request_data(request)
    print(json.dumps(clear_req, indent=4))

    # Test response decryption
    response = '3a5da56a5d64459fea61c12b2a3a8752ab7263b07014a9bd157364bc4ad351239362dd8eb8ab2ed8637d369e7bbbb2a63094ede47742a5d59714084a1fb34851fb50779e09184854564910d36a964dc2df37a9f424e698bd613c1575bfaad1be42a4d43ec2825302d67040ba6cc3f471f6b19b809c9c21b0815f610fb141da15594c5eecf26031823469edd0d1fe57615850fab324f90b0ebbf023a1ea3886bb52c9d4e878b56f5034ae036bf96bda786024784e5f7fd14b620c217e0cbb42295560efd9614fa583d9e1e6c4b015ce06d7d9e200b1a11e1c9bab59e5b4b8b8215333ae9b089639b4b9594968c27e46cb4967b0189599da91973f644807f2eecdcc13b76e734c9e00ce25a37e43174b62edd036c21e316372c0d3d3bc8c51e30bba7486a8f63fb65c4bbaa4c870971ce34945610717296d5613fddbef474752c99a11b965fc270e902db2621f884020de77ee60738efbcecd591f4a62a5168b8548b9151e12351e073c4fef26c5653c1a6164a6e3536651f6711c6fe26e028cc21d9d26873f74cffaa0a0e4f471d27d91d2ae9d2f6c466b9cb5ab33e69cbfeb342d94a36b6603c1b94617fea67ff10413aafa58ce50873b73c61e104703bd396de07b8a6ad99ca9aa6a3fb91dc177871c8d7dffc3c8fc4df47ed2ad6950350074b8959dcfeba24c11e76fe63591f034653479e28b8d416e2ad8d8a05e4a4205ecb09a1bf06a3efcc39370bc5c6efc5c74c27a6a01f3336bd926842c15558d9c7648a43e9c5e7ac13401787c2b303365b2f9d0118a453ad4bb0bae3c58ec93ea0410e72657d2e48b9951447adc707c704a6ec2ff7b371724adf980bfae8646a3c9595b3271984412d0cf0d51851355ceef91c62475bcc65012b2a1e98db6f7515bb7de4b52cf42ea1cf0bcef0d736023253f3972a54b60d4e0dbbae9c4b62f95bddb5cb04f2f27bc150082f84d6a98a3f689a275f551e812a1003cd925a1672ff1001ef973ea570703c3814d4bfaf78babd6aa996f6ecfdc800790035d0a9c1c85a0626419c7b4320a6907d2dce53fe282f3799d6c77fdff19c9722a2c206932a26513b1679350c4818574260f82dd2f066a11977765dfa23663802a4ee26171a81255c0efab279873116cdcc1f565c365fd23794178dcaa96658abcfd4576cb976ef0e14d91d2b372ff2358fa8456fef689ae660f94ebfb16f5404ed661e4157373569b6a4c79a0579bd7e30243e22ca4b7502c23165cf8714a92a840bb2f1fd68f20f44084cf55b0b325bb4917bbc5b11cb7505920a87d187e7f5e547c'
    clear_resp = decrypt_response_data(response)
    print(json.dumps(clear_resp, indent=4))


# Encrypt request data, returns hex bytes by default.
# NOTE: To use as request data, unhexlify the result & urlsafe_b64encode it.
def encrypt_request_data(data):
    msg = msgpack.packb(data)
    iv = get_random_bytes(12)
    cipher = AES.new(key, AES.MODE_GCM, iv)
    encrypted = cipher.encrypt(msg)
    tag = cipher.digest()
    return hexlify(iv + encrypted + tag)


def get_replays_request():
    data_header = [
        "210611111531170608",
        "63a4b3207a9b3",
        2,
        "0.1.7",
        3
    ]
    data_params = [
        1,
        0,
        10,
        [
            -1,
            0,
            1,
            99,
            [],
            -1,
            -1,
            0,
            0,
            1
        ],
        6  # all platforms? (3 = PC?)
    ]
    data = [data_header, data_params]

    encrypted = encrypt_request_data(data)

    print(f'Encrypted request data: {encrypted}')
    # print(f'Sanity check: {json.dumps(decrypt_response_data(encrypted), indent=4)}')

    encoded = urlsafe_b64encode(unhexlify(encrypted))
    print(f'Encoded request data: {encoded}')

    r = requests.post(
        r'https://ggst-game.guiltygear.com/api/catalog/get_replay',
        headers={
            'Cache-Control': r'no-store',
            'Content-Type': r'application/x-www-form-urlencoded',
            'User-Agent': r'GGST/Steam',
            'x-client-version': r'1',
            'Content-Length': r'122'
        },
        data={
            'data': encoded
        },
    )

    content = r.content
    # print(f'Response (hex): {content.hex()}')
    print(f'Decrypted response: \n{json.dumps(decrypt_response_data(content.hex()), indent=4)}')


if __name__ == "__main__":
    main()

Which produces the following response from the server:

[
    [
        "63a4d2a868986",
        0,
        "2022/12/22 21:56:56",
        "0.1.7",
        "0.0.2",
        "0.0.2",
        "",
        ""
    ],
    [
        0,
        0,
        10,
        [
            [
                221222215030712763,
                15,
                10,
                19,
                16,
                [
                    "210621212649463684",
                    "Tsuruda",
                    "76561198044832921",
                    "1100001050a6499",
                    3,
                    6
                ],
                [
                    "211219202343275742",
                    "Sonex",
                    "76561198056994822",
                    "110000105c3f806",
                    3,
                    9
                ],
                2,
                "2022-12-23 00:50:30",
                1,
                0,
                0,
                0
            ],
            [
                221222214000059027,
                15,
                99,
                1,
                19,
                [
                    "210611073644800042",
                    "Gone",
                    "76561198068250117",
                    "1100001066fb605",
                    3,
                    9
                ],
                [
                    "210614071944904510",
                    "Free",
                    "76561198027320062",
                    "110000103ff2afe",
                    3,
                    9
                ],
                1,
                "2022-12-23 00:38:34",
                1,
                0,
                0,
                0
            ],
            [
                221222213349808973,
                15,
                10,
                9,
                16,
                [
                    "210814203235952623",
                    "GOTE-SAMA",
                    "76561198216174113",
                    "11000010f40da21",
                    3,
                    8
                ],
                [
                    "211219202343275742",
                    "Sonex",
                    "76561198056994822",
                    "110000105c3f806",
                    3,
                    9
                ],
                2,
                "2022-12-23 00:33:49",
                1,
                0,
                0,
                0
            ],
            [
                221222213227454031,
                15,
                10,
                9,
                16,
                [
                    "210814203235952623",
                    "GOTE-SAMA",
                    "76561198216174113",
                    "11000010f40da21",
                    3,
                    9
                ],
                [
                    "211219202343275742",
                    "Sonex",
                    "76561198056994822",
                    "110000105c3f806",
                    3,
                    9
                ],
                2,
                "2022-12-23 00:32:27",
                1,
                0,
                0,
                0
            ],
            [
                221222213245126948,
                15,
                99,
                1,
                5,
                [
                    "210611073644800042",
                    "Gone",
                    "76561198068250117",
                    "1100001066fb605",
                    3,
                    9
                ],
                [
                    "210611145137490808",
                    "Ya Boi Squanto",
                    "76561198058818878",
                    "110000105dfcd3e",
                    3,
                    9
                ],
                2,
                "2022-12-23 00:31:19",
                1,
                0,
                0,
                0
            ],
            [
                221222212952963881,
                15,
                10,
                9,
                16,
                [
                    "210814203235952623",
                    "GOTE-SAMA",
                    "76561198216174113",
                    "11000010f40da21",
                    3,
                    9
                ],
                [
                    "211219202343275742",
                    "Sonex",
                    "76561198056994822",
                    "110000105c3f806",
                    3,
                    9
                ],
                1,
                "2022-12-23 00:29:52",
                1,
                0,
                0,
                0
            ],
            [
                221222213116149241,
                15,
                99,
                1,
                5,
                [
                    "210611073644800042",
                    "Gone",
                    "76561198068250117",
                    "1100001066fb605",
                    3,
                    9
                ],
                [
                    "210611145137490808",
                    "Ya Boi Squanto",
                    "76561198058818878",
                    "110000105dfcd3e",
                    3,
                    9
                ],
                1,
                "2022-12-23 00:29:50",
                1,
                0,
                0,
                0
            ],
            [
                221222212635786904,
                15,
                99,
                1,
                21,
                [
                    "210611073644800042",
                    "Gone",
                    "76561198068250117",
                    "1100001066fb605",
                    3,
                    9
                ],
                [
                    "210704200643605565",
                    "Guess the Meme",
                    "76561198989946472",
                    "11000013d5fae68",
                    3,
                    9
                ],
                2,
                "2022-12-23 00:25:10",
                1,
                1,
                0,
                0
            ],
            [
                221222212510328349,
                15,
                10,
                16,
                1,
                [
                    "211219202343275742",
                    "Sonex",
                    "76561198056994822",
                    "110000105c3f806",
                    3,
                    9
                ],
                [
                    "210707080322158563",
                    "Vixo",
                    "76561198014768536",
                    "1100001033fa598",
                    3,
                    9
                ],
                1,
                "2022-12-23 00:25:10",
                1,
                0,
                0,
                0
            ],
            [
                221222212433465929,
                15,
                99,
                1,
                21,
                [
                    "210611073644800042",
                    "Gone",
                    "76561198068250117",
                    "1100001066fb605",
                    3,
                    9
                ],
                [
                    "210704200643605565",
                    "Guess the Meme",
                    "76561198989946472",
                    "11000013d5fae68",
                    3,
                    9
                ],
                1,
                "2022-12-23 00:23:07",
                1,
                0,
                0,
                0
            ]
        ]
    ]
]

And I can confirm that it matches what I'm seeing in my game locally.

crtado avatar Dec 22 '22 22:12 crtado

Amazing work! I'll start translating this to Rust so I can run it on the website.

halvnykterist avatar Dec 23 '22 12:12 halvnykterist

Hey all, I opened a PR https://github.com/optix2000/totsugeki/pull/87 to help address this in totsugeki. I worked the example python code into a Go package, and then was able to rewrite /api/sys/get_env so that the proxy works correctly again. The requests are getting proxied, but because I only encrypted/decrypt in one spot, GGST just errors on the rest of the endpoints. I don't know all the spots that it needs to go through the crypto code yet, digging through that now (unless someone already knows all the spots :))

scw007 avatar Dec 28 '22 19:12 scw007

I can come back to this & contribute to your PR after the holidays wrap up.

Also, something important to note is that @Theoretical discovered that the "token" (63a4b3207a9b3 in my example above) used in the requests sent to the server actually expires after a period of time, so it can't be hardcoded anymore. They tried to use my example above & it didn't work because of this token expiry. I'm guessing that @halvnykterist will probably hit the same issue if they try reusing the same token too.

However, it turns out that this token is at least partially composed of a hex timestamp. For example, the first four bytes of the aforementioned token (63a4b320) represent 1671738144 in decimal which corresponds to Thursday, December 22, 2022 7:42:24 PM. Which iirc is the same time I was capturing traffic & reusing the token. Therefore it might be possible to generate a valid token for each request on the fly.

I haven't had the chance to try this yet, so someone else might have made more progress in the meantime.

crtado avatar Dec 28 '22 19:12 crtado

Any chance someone with the proxy setup could post several successful requests or responses? I've been pretty unsuccessful brute forcing the last 5 hex digits of the token (there's just over a million combinations...). I'll keep picking at it but more examples couldn't hurt.

I'm sure this is known, but it may be helpful. It seems like the first piece of the response object is the token provided in the request. The second piece appears to be a "failure" flag, in the successful responses it looked to be 0.

Decrypted response for 9999999999999: 
[
    [
        "9999999999999",
        1,
        "2022/12/31 16:54:39",
        "0.1.7",
        "0.0.2",
        "0.0.2",
        "",
        ""
    ],
    [
        4
    ]
]

EricPHamilton avatar Dec 31 '22 16:12 EricPHamilton

It seems token expired each time when the game tries to login via /api/user/login with content

[['', '', 2, '0.1.7', 3],
 [1, <stem_id>, <steam_hex_id>,  256, <unknown very long string>]]

It is not possible to have several valid tokens anymore.

And I cannot figure out how to get this very long string from steam client to automatize getting token.

spam-izra avatar Jan 01 '23 17:01 spam-izra

Is the very long string different each time?

halvnykterist avatar Jan 03 '23 18:01 halvnykterist

yes, a random set of hex bytes

Theoretical avatar Jan 04 '23 03:01 Theoretical

hi everyone! the first part of the string 140000005088EF43300E7A510FF1DD0B0100100162EDB66318000000010000000200000082EC43A97F211FB1EB546E0303000000 is definitely user auth ticket as u can see on screenshot, next part is always static to me 24010000A4000000040000000FF1DD0B01001001E01E1500F44913256800A8C0000000002F0CB663AFBBD1630100E1B707001300D09017000000D19017000000D29017000000D39017000000D49017000000D59017000000D69017000000D79017000000D89017000000D82C190000009A38190000009B38190000001AC11F0000001BC11F0000001CC11F0000001DC11F0000001EC11F0000001FC11F00000020C11F0000000000 the last part might be an application ticket (5C63E04253B887B23F3D776A2B9570E87CCF76EF2A1DB8CBF2F5E38E7E454EE2E08A5CA3D66FEC9BDAB6BECF8ACF6345FF52964B33B6A02893EC4F89703F2CC6BF08A00313C3274B4EBEC232AF0293244313042DAF1C98386BC21CC4A3A39B15C272A99A690A475CDA2AFB69BA365937CDC779C38865668550A389FA3FEF95904F7AD3458A3E5AF7658653B20834295BBC47571E772850037D6F10AF565A0C372FC81F372377A7C0277F6594AEF4465838A01C75B46DFA9DA886F16ABD90305AA681CC9AB0FDB7089D323B7C6A25351A84AF73D9DD7DA8B1BB4E7ABCC006A24C), but it's encrypted and i can't figure out how to decrypt it. Bet that only arcsys, as an app owners can.

image

P.S. for hooking steam api calls i'm using NetHook2

F0cu53d avatar Jan 05 '23 16:01 F0cu53d

Hello everyone! I finally figured out how build GGST API token (short hexlify string). Thanks @F0cu53d for the information.

All numbers are encoded as integers (4 bytes) in little endian. <app_id> is 1384160 (GGST id in steam store)

As before, we need to send a request to https://ggst-game.guiltygear.com/api/user/login and the request body looks like this: [1, <stem_id>, <steam_hex_id>, 256, <auth_session_ticket>]

<auth_session_ticket> structure:
    <auth_ticket> (usually occupies 52 bytes)
    <app_ticket_length>
    <app_ticket>
<auth_ticket> structure:
    <game_token_length>
    <game_token>
    <session_size> (always equals 24)
    <unknown_1> (always equals 1)
    <unknown_2> (always equals 2)
    <ip_address> (obfuscated ip address, I suppose just <auth_ticket> identifier)
    <unknown_3> (random number, but 0 working for me)
    <connection_time> (time of session in milliseconds)
    <connection_count>

Steps for getting <auth_session_ticket> is as follows (all requests to the steam client api):

  1. Get <app_ticket> with request ClientGetAppOwnershipTicket
  2. Get <game_token> (steam client itself sends it as ClientGameConnectTokens request)
  3. Build <auth_ticket>, calculate crc32, send it with request ClientAuthList then wait ClientAuthListAck
  4. Build <auth_session_ticket> (all parts we have)
  5. Done.

The <auth_session_ticket> will be valid until a new one is generated (or maybe expired).

I took most of the information about building <auth_session_ticket> from this code

I did not begin to paint a detailed algorithm of work here, since I am still unsure of many things. If someone is well versed in the steam client api, then he will be able to understand what to do. In the near future I will try to make just a web server that will issue the coveted token upon request, I will write about success here.

If anyone is interested about steam client api, I use this Python library.

vdudnikov avatar Jan 17 '23 20:01 vdudnikov

have you managed to successfully post a login? I broke down the code into python but get 500 for each request

game_tokens = []
session_time = None

def on_game_tokens(msg):
    global game_tokens

    print('??? tokens')
    print(msg)
    print(msg.body.tokens)
    game_tokens.extend(msg.body.tokens)

def create_auth_ticket(token, session_time):
    # dumb shit
    session_size = 24
    public_ip = get('https://checkip.amazonaws.com').text.strip()

    msg = b''
    msg += pack('I', len(token))
    msg += token
    msg += pack('III', session_size, 1, 2)

    ip = inet_aton(public_ip)
    ip = bytearray(ip)
    ip.reverse()

    msg += ip
    msg += pack('III', 0, int(session_time), 1)
    
    return msg


def build_login_token():
    global game_tokens, session_time

    app_id = 1384160 # Strive appId
    client = SteamClient()
    client.on(EMsg.ClientGameConnectTokens, on_game_tokens)
    #client.on(EMsg.ClientAuthListAck, on_ticket_ack)
    session_time = time()
    client.cli_login()

    app_ticket = client.get_app_ticket(app_id).ticket
    auth_ticket = create_auth_ticket(game_tokens[0], time() - session_time)
    crc = crc32(auth_ticket)

    message = MsgProto(EMsg.ClientAuthList)
    message.body.tokens_left = len(game_tokens)
    message.body.app_ids.extend([app_id])

    tickets = message.body.tickets.add()

    # wtf is this proto bullshit.
    ticket = {
        'gameid': app_id,
        'ticket': auth_ticket,
        'ticket_crc': crc
    }

    proto_fill_from_dict(tickets, ticket)

    resp = client.send_message_and_wait(message, EMsg.ClientAuthListAck)
    print('Looking for ack for our ticket...')
    print('Auth tickent len: %d' % len(auth_ticket))
    print(resp)

    # build login token
    msg = auth_ticket
    msg += pack('I', len(app_ticket))
    msg += app_ticket

    return hexlify(msg)

Theoretical avatar Jan 18 '23 11:01 Theoretical

I got it working and spun up as small repo:

https://github.com/Theoretical/ggst-py/tree/main

Theoretical avatar Jan 18 '23 14:01 Theoretical

Fantastic work everyone, guys! Thank you!

F0cu53d avatar Jan 18 '23 14:01 F0cu53d

@v-dudnikov i was using the same kit for hooking and didn't realize that the answer was right under my nose.. 😆

F0cu53d avatar Jan 18 '23 15:01 F0cu53d