From 78a66b23786c5e884dece335669dc4df07df0280 Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sun, 12 Dec 2021 18:35:51 +0000 Subject: [PATCH] initial commit - it works! --- .gitignore | 5 +- .gitmodules | 3 + CMakeLists.txt | 10 + CMakeSettings.json | 46 ++++ LICENSE | 21 ++ QMC2-crypto/CMakeLists.txt | 39 ++++ QMC2-crypto/include/qmc2-crypto/IKeyDec.h | 10 + .../qmc2-crypto/IStreamEncryptAndDecrypt.h | 12 + QMC2-crypto/include/qmc2-crypto/KeyDec.h | 16 ++ .../include/qmc2-crypto/StreamCencrypt.h | 40 ++++ QMC2-crypto/qmc2-crypto/KeyDec.cpp | 88 ++++++++ QMC2-crypto/qmc2-crypto/StreamCencrypt.cpp | 209 ++++++++++++++++++ QMC2-crypto/vendor/TarsCpp | 1 + QMC2-decoder/CMakeLists.txt | 16 ++ QMC2-decoder/QMC2-decoder.cpp | 90 ++++++++ QMC2-decoder/QMC2-decoder.h | 8 + .../crypto/IStreamEncryptAndDecrypt.h | 8 + QMC2-decoder/crypto/KeyDec.cpp | 26 +++ QMC2-decoder/crypto/KeyDec.h | 13 ++ QMC2-decoder/crypto/StreamCencrypt.cpp | 200 +++++++++++++++++ QMC2-decoder/crypto/StreamCencrypt.h | 36 +++ README.md | 30 +++ 22 files changed, 926 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 CMakeSettings.json create mode 100644 LICENSE create mode 100644 QMC2-crypto/CMakeLists.txt create mode 100644 QMC2-crypto/include/qmc2-crypto/IKeyDec.h create mode 100644 QMC2-crypto/include/qmc2-crypto/IStreamEncryptAndDecrypt.h create mode 100644 QMC2-crypto/include/qmc2-crypto/KeyDec.h create mode 100644 QMC2-crypto/include/qmc2-crypto/StreamCencrypt.h create mode 100644 QMC2-crypto/qmc2-crypto/KeyDec.cpp create mode 100644 QMC2-crypto/qmc2-crypto/StreamCencrypt.cpp create mode 160000 QMC2-crypto/vendor/TarsCpp create mode 100644 QMC2-decoder/CMakeLists.txt create mode 100644 QMC2-decoder/QMC2-decoder.cpp create mode 100644 QMC2-decoder/QMC2-decoder.h create mode 100644 QMC2-decoder/crypto/IStreamEncryptAndDecrypt.h create mode 100644 QMC2-decoder/crypto/KeyDec.cpp create mode 100644 QMC2-decoder/crypto/KeyDec.h create mode 100644 QMC2-decoder/crypto/StreamCencrypt.cpp create mode 100644 QMC2-decoder/crypto/StreamCencrypt.h create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 9491a2f..a5f3a8e 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + + +build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0c56613 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "QMC2-crypto/vendor/TarsCpp"] + path = QMC2-crypto/vendor/TarsCpp + url = https://github.com/TarsCloud/TarsCpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2a9539b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +# CMakeList.txt : Top-level CMake project file, do global configuration +# and include sub-projects here. +# +cmake_minimum_required (VERSION 3.8) + +project ("QMC2") + +# Include sub-projects. +add_subdirectory ("QMC2-crypto") +add_subdirectory ("QMC2-decoder") diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000..dc6991b --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,46 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + }, + { + "name": "Linux-GCC-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "cmakeExecutable": "cmake", + "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "linux_x64" ], + "remoteMachineName": "${defaultRemoteMachineName}", + "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", + "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", + "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", + "remoteCopySources": true, + "rsyncCommandArgs": "-t --delete --delete-excluded", + "remoteCopyBuildOutput": false, + "remoteCopySourcesMethod": "rsync" + }, + { + "name": "x64-Release", + "generator": "Ninja", + "configurationType": "RelWithDebInfo", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "msvc_x64_x64" ], + "variables": [] + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6979efb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jixun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/QMC2-crypto/CMakeLists.txt b/QMC2-crypto/CMakeLists.txt new file mode 100644 index 0000000..65e15d3 --- /dev/null +++ b/QMC2-crypto/CMakeLists.txt @@ -0,0 +1,39 @@ +# CMakeList.txt : CMake project for QMC2-decoder, include source and define +# project specific logic here. +# +cmake_minimum_required (VERSION 3.8) + +include_directories( + vendor + vendor/TarsCpp/util/include + include +) + +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS YES CACHE BOOL "Export all symbols") + +# We don't need all TarsCpp code +set(SOURCE_TEA + "vendor/TarsCpp/util/src/tc_base64.cpp" + "vendor/TarsCpp/util/src/tc_tea.cpp" +) + +# Add source to this project's executable. +add_library (QMC2-crypto + ${SOURCE_TEA} + "include/qmc2-crypto/IKeyDec.h" + "include/qmc2-crypto/StreamCencrypt.h" + "include/qmc2-crypto/IStreamEncryptAndDecrypt.h" + "include/qmc2-crypto/KeyDec.h" + "qmc2-crypto/StreamCencrypt.cpp" + "qmc2-crypto/KeyDec.cpp") + +if (!MSVC) + target_link_libraries(QMC2-crypto m) +endif() + +target_include_directories(QMC2-crypto PUBLIC + $ + $ +) + +# TODO: Add tests and install targets if needed. diff --git a/QMC2-crypto/include/qmc2-crypto/IKeyDec.h b/QMC2-crypto/include/qmc2-crypto/IKeyDec.h new file mode 100644 index 0000000..1c12974 --- /dev/null +++ b/QMC2-crypto/include/qmc2-crypto/IKeyDec.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +class IKeyDec { +protected: + uint8_t* decoded_key = nullptr; + virtual void GetKey(uint8_t*& key, size_t& key_size) = 0; + virtual void SetKey(const char* key, const size_t key_size) = 0; +}; diff --git a/QMC2-crypto/include/qmc2-crypto/IStreamEncryptAndDecrypt.h b/QMC2-crypto/include/qmc2-crypto/IStreamEncryptAndDecrypt.h new file mode 100644 index 0000000..869fdcc --- /dev/null +++ b/QMC2-crypto/include/qmc2-crypto/IStreamEncryptAndDecrypt.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include + +class IStreamEncryptAndDecrypt { +public: + IStreamEncryptAndDecrypt() {}; + ~IStreamEncryptAndDecrypt() {}; + + virtual void StreamEncrypt(uint64_t offset, uint8_t* buf, size_t len) = 0; + virtual void StreamDecrypt(uint64_t offset, uint8_t* buf, size_t len) = 0; +}; diff --git a/QMC2-crypto/include/qmc2-crypto/KeyDec.h b/QMC2-crypto/include/qmc2-crypto/KeyDec.h new file mode 100644 index 0000000..3621971 --- /dev/null +++ b/QMC2-crypto/include/qmc2-crypto/KeyDec.h @@ -0,0 +1,16 @@ +#pragma once +#include "IKeyDec.h" +#include + +class KeyDec : public IKeyDec { +public: + KeyDec() {}; + ~KeyDec(); + + virtual void GetKey(uint8_t*& key, size_t& key_size) override; + virtual void SetKey(const char* key, const size_t key_size) override; + +private: + uint8_t* key = nullptr; + size_t key_len = 0; +}; diff --git a/QMC2-crypto/include/qmc2-crypto/StreamCencrypt.h b/QMC2-crypto/include/qmc2-crypto/StreamCencrypt.h new file mode 100644 index 0000000..6e30691 --- /dev/null +++ b/QMC2-crypto/include/qmc2-crypto/StreamCencrypt.h @@ -0,0 +1,40 @@ +#pragma once + +#include "IStreamEncryptAndDecrypt.h" +#include "KeyDec.h" + +#include +#include + +class StreamCencrypt : public IStreamEncryptAndDecrypt { +public: + StreamCencrypt() {}; + ~StreamCencrypt() {}; + + void StreamEncrypt(uint64_t offset, uint8_t* buf, size_t len) override; + void StreamDecrypt(uint64_t offset, uint8_t* buf, size_t len) override; + + void SetKeyDec(KeyDec* key_dec); + + bool CheckCallerLegal() { + return true; + } +private: + uint32_t key_hash = 0; + + // RC4 vars + uint8_t* rc4_key = nullptr; + uint8_t* S = nullptr; + uint8_t* S2 = nullptr; + size_t N = 0; + + void InitRC4KSA(); + void GetHashBase(); + uint8_t mapL(uint64_t offset); + + uint64_t GetSegmentKey(uint64_t a, uint64_t b); + void Uninit(); + void EncASegment(uint8_t* sbox, size_t offset, uint8_t* buf, size_t len); + void EncFirstSegment(size_t offset, uint8_t* buffer, size_t size); + void ProcessByRC4(size_t offset, uint8_t* buffer, size_t buffer_size); +}; diff --git a/QMC2-crypto/qmc2-crypto/KeyDec.cpp b/QMC2-crypto/qmc2-crypto/KeyDec.cpp new file mode 100644 index 0000000..7b17df7 --- /dev/null +++ b/QMC2-crypto/qmc2-crypto/KeyDec.cpp @@ -0,0 +1,88 @@ +#include "qmc2-crypto/KeyDec.h" + +#include +#include + +#include +#include +#include + +using tars::TC_Base64; +using tars::TC_Tea; + +KeyDec::~KeyDec() +{ + if (key) { + delete[] key; + key = nullptr; + } + + if (key_len) { + key_len = 0; + } +} + +void KeyDec::GetKey(uint8_t*& key_out, size_t& key_len_out) +{ + if (key && key_len > 0) { + key_len_out = key_len; + key_out = new uint8_t[key_len]; + memcpy(key_out, key, key_len); + } + else { + key_len_out = 0; + } +} + +void SimpleMakeKey(uint8_t seed, size_t len, uint8_t* buf) { + for (int i = 0; len > i; ++i) { + buf[i] = (uint8_t)(fabs(tan((float)seed + (double)i * 0.1)) * 100.0); + } +} + +void KeyDec::SetKey(const char* key, const size_t key_size) +{ + TC_Base64 b64; + TC_Tea tea; + size_t decode_len = key_size / 4 * 3 + 4; + // should be 0x210 + std::vector ekey_decoded; + ekey_decoded.resize(decode_len); + + uint8_t simple_key_buf[8] = { 0 }; + SimpleMakeKey(106, 8, simple_key_buf); +#if _DEBUG + // 69 56 46 38 2b 20 15 0b + assert(simple_key_buf[0] == 0x69); + assert(simple_key_buf[1] == 0x56); + assert(simple_key_buf[2] == 0x46); + assert(simple_key_buf[3] == 0x38); + assert(simple_key_buf[4] == 0x2b); + assert(simple_key_buf[5] == 0x20); + assert(simple_key_buf[6] == 0x15); + assert(simple_key_buf[7] == 0x0b); +#endif + + decode_len = b64.decode(key, key_size, ekey_decoded.data()); + + uint8_t tea_key[16]; + for (int i = 0; i < 16; i += 2) { + tea_key[i + 0] = simple_key_buf[i / 2]; + tea_key[i + 1] = ekey_decoded[i / 2]; + } + + if (this->key) { + delete[] this->key; + this->key = nullptr; + } + + this->key = new uint8_t[decode_len * 2](); + + // 拷贝前 8 个字节 + memcpy(this->key, ekey_decoded.data(), 8u); + + std::vector decrypted_buf; + tea.decrypt(reinterpret_cast(tea_key), reinterpret_cast(ekey_decoded.data()) + 8, decode_len - 8, decrypted_buf); + key_len = decrypted_buf.size() + 8; + memcpy(&this->key[8], decrypted_buf.data(), decrypted_buf.size()); +} diff --git a/QMC2-crypto/qmc2-crypto/StreamCencrypt.cpp b/QMC2-crypto/qmc2-crypto/StreamCencrypt.cpp new file mode 100644 index 0000000..dfa47d0 --- /dev/null +++ b/QMC2-crypto/qmc2-crypto/StreamCencrypt.cpp @@ -0,0 +1,209 @@ +#include "qmc2-crypto/StreamCencrypt.h" + +#include +#include + +#include + +void StreamCencrypt::StreamEncrypt(uint64_t offset, uint8_t* buf, size_t len) +{ + if (N > 300) { + ProcessByRC4(offset, buf, len); + } + else { + for (size_t i = 0; i < len; i++) + { + buf[i] ^= mapL(offset + i); + } + } +} + +void StreamCencrypt::StreamDecrypt(uint64_t offset, uint8_t* buf, size_t len) +{ + this->StreamEncrypt(offset, buf, len); +} + +void StreamCencrypt::SetKeyDec(KeyDec* key_dec) +{ + Uninit(); + rc4_key = nullptr; + if (key_dec) { + key_dec->GetKey(this->rc4_key, N); + if (N > 300) { + InitRC4KSA(); + } + } +} + +void StreamCencrypt::Uninit() +{ + // reset initial rc4 key + if (rc4_key) { + delete[] rc4_key; + rc4_key = nullptr; + } + + // reset sbox + this->N = 0; + if (S) { + delete[] S; + S = nullptr; + } +} + +#define FIRST_SEGMENT_SIZE (0x80) +#define SEGMENT_SIZE (0x1400) + +void StreamCencrypt::ProcessByRC4(size_t offset, uint8_t* buf, size_t size) +{ + uint8_t* orig_buf = buf; + + uint8_t* last_addr = orig_buf + size; + + auto len = size; + + // Initial segment + if (offset < FIRST_SEGMENT_SIZE) { + auto len_segment = std::min(size, FIRST_SEGMENT_SIZE - offset); + EncFirstSegment(offset, buf, len_segment); + len -= len_segment; + buf += len_segment; + offset += len_segment; + } + + uint8_t* S = new uint8_t[N]; + memset(S, 0, N); + + // Align segment + if (offset % SEGMENT_SIZE != 0) { + auto len_segment = std::min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), len); + EncASegment(S, offset, buf, len_segment); + len -= len_segment; + buf += len_segment; + offset += len_segment; + } + + // Batch process segments + while (len > SEGMENT_SIZE) { + auto len_segment = std::min(size_t{ SEGMENT_SIZE }, len); + EncASegment(S, offset, buf, len_segment); + len -= len_segment; + buf += len_segment; + offset += len_segment; + } + + // Last segment (incomplete segment) + if (len > 0) { + EncASegment(S, offset, buf, len); + } + + assert(last_addr == buf + len); + + delete[] S; +} + +uint64_t StreamCencrypt::GetSegmentKey(uint64_t id, uint64_t seed) +{ + return uint64_t((double)this->key_hash / double((id + 1) * seed) * 100.0); +} + +void StreamCencrypt::EncFirstSegment(size_t offset, uint8_t* buf, size_t len) +{ + for (size_t i = 0; i < len; i++) { + uint64_t key = uint64_t{ this->rc4_key[offset % this->N] }; + buf[i] ^= this->rc4_key[GetSegmentKey(offset, key) % this->N]; + offset++; + } +} + +void StreamCencrypt::EncASegment(uint8_t* S, size_t offset, uint8_t* buf, size_t len) +{ + if (rc4_key == nullptr) { + // We need to initialise RC4 key first! + return; + } + + const auto N = this->N; + + // Initialise a new seedbox + memcpy(S, this->S, N); + + // Calculate segment id + int segment_id = (offset / SEGMENT_SIZE) & 0x1FF; + + // Calculate the number of bytes to skip. + // The initial "key" derived from segment id, plus the current offset. + auto skip_len = GetSegmentKey(offset / SEGMENT_SIZE, this->rc4_key[segment_id]) & 0x1FF; + skip_len += offset % SEGMENT_SIZE; + + int j = 0; + int k = 0; + for (size_t i = 0; i < skip_len; i++) { + j = (j + 1) % N; + k = (S[j] + k) % N; + std::swap(S[j], S[k]); + } + + // Now we also manipulate the buffer: + for (size_t i = 0; i < len; i++) { + j = (j + 1) % N; + k = (S[j] + k) % N; + std::swap(S[j], S[k]); + + buf[i] ^= S[(S[j] + S[k]) % N]; + } +} + +void StreamCencrypt::InitRC4KSA() +{ + if (!S) { + S = new uint8_t[N](); + } + + for (size_t i = 0; i < N; ++i) { + S[i] = i & 0xFF; + } + + int j = 0; + for (size_t i = 0; i < N; ++i) { + j = (S[i] + j + rc4_key[i % N]) % N; + std::swap(S[i], S[j]); + } + + GetHashBase(); +} + +void StreamCencrypt::GetHashBase() +{ + this->key_hash = 1; + for (size_t i = 0; i < this->N; i++) { + int32_t value = int32_t{ this->rc4_key[i] }; + + // ignore if key char is '\x00' + if (!value) continue; + + auto next_hash = this->key_hash * value; + if (next_hash == 0 || next_hash <= this->key_hash) + break; + + this->key_hash = next_hash; + } +} + +inline uint8_t rotate(uint8_t value, int bits) { + int rotate = (bits + 4) % 8; + auto left = value << rotate; + auto right = value >> rotate; + return uint8_t(left | right); +} + +uint8_t StreamCencrypt::mapL(uint64_t offset) +{ + if (offset > 0x7FFF) + offset %= 0x7FFF; + + uint64_t key = (offset * offset + 71214) % this->N; + + uint8_t value = this->rc4_key[key]; + return rotate(value, key & 0b0111); +} diff --git a/QMC2-crypto/vendor/TarsCpp b/QMC2-crypto/vendor/TarsCpp new file mode 160000 index 0000000..a6d5ed8 --- /dev/null +++ b/QMC2-crypto/vendor/TarsCpp @@ -0,0 +1 @@ +Subproject commit a6d5ed85271344939b2964ad105f29c5053a4f70 diff --git a/QMC2-decoder/CMakeLists.txt b/QMC2-decoder/CMakeLists.txt new file mode 100644 index 0000000..9a4a14c --- /dev/null +++ b/QMC2-decoder/CMakeLists.txt @@ -0,0 +1,16 @@ +# CMakeList.txt : CMake project for QMC2-decoder, include source and define +# project specific logic here. +# +cmake_minimum_required (VERSION 3.8) + +# Add source to this project's executable. +add_executable (QMC2-decoder + "QMC2-decoder.cpp" + "QMC2-decoder.h" +) + +include_directories($) + +target_link_libraries(QMC2-decoder QMC2-crypto) + +# TODO: Add tests and install targets if needed. diff --git a/QMC2-decoder/QMC2-decoder.cpp b/QMC2-decoder/QMC2-decoder.cpp new file mode 100644 index 0000000..458637e --- /dev/null +++ b/QMC2-decoder/QMC2-decoder.cpp @@ -0,0 +1,90 @@ +// QMC2-decoder.cpp : Defines the entry point for the application. +// + +#include "QMC2-decoder.h" + +// HACK: Make VS 2022 happy +#include "../QMC2-crypto/include/qmc2-crypto/StreamCencrypt.h" +#include "../QMC2-crypto/include/qmc2-crypto/KeyDec.h" + +#include + +#include +#include +#include + +using namespace std; + +StreamCencrypt* createInstWidthEKey(const char* ekey_b64) { + StreamCencrypt* stream = new StreamCencrypt(); + KeyDec* key_dec = new KeyDec(); + key_dec->SetKey(ekey_b64, strlen(ekey_b64)); + stream->SetKeyDec(key_dec); + delete key_dec; + return stream; +} + +constexpr size_t read_size = 4096; +constexpr size_t footer_detection_size = 0x40; +constexpr size_t encrypted_key_size = 704; + +int main(int argc, char** argv) +{ + fprintf(stderr, "QMC2 decoder (cli) v1.0 by Jixun\n\n"); + + if (argc < 3) + { + printf("usage: %s [ignored]\n", argv[0]); + return 1; + } + + ifstream mgg(argv[1], ios::in | ios::binary); + ofstream ogg(argv[2], ios::out | ios::binary); + + uint64_t offset = 0; + + uint8_t buf[read_size] = {}; + mgg.seekg(0, ios::end); + auto input_file_len = size_t(mgg.tellg()); + mgg.seekg(input_file_len - footer_detection_size, ios::beg); + mgg.read(reinterpret_cast(buf), footer_detection_size); + + // Magic: 32 | 00 00 02 CC 51 54 61 67 + if (*(uint64_t*)(&buf[footer_detection_size - 8]) != 0x67615451CC020000 + || (buf[footer_detection_size - 8 - 1]) != '2') + { + cout << "unknown encryption method" << endl; + return 1; + } + + size_t decrypted_file_size = 0; + for (int i = 0; i < footer_detection_size; i++) { + if (buf[i] == ',') { + decrypted_file_size = input_file_len - footer_detection_size + i - encrypted_key_size; + break; + } + } + + mgg.seekg(decrypted_file_size, ios::beg); + mgg.read(reinterpret_cast(buf), encrypted_key_size); + buf[encrypted_key_size] = 0; + auto stream = createInstWidthEKey(reinterpret_cast(buf)); + mgg.seekg(0, ios::beg); + + size_t to_decrypt_len = decrypted_file_size; + while (to_decrypt_len > 0) { + auto block_size = std::min(read_size, to_decrypt_len); + mgg.read(reinterpret_cast(buf), block_size); + auto bytes_read = mgg.gcount(); + + stream->StreamDecrypt(offset, buf, bytes_read); + ogg.write(reinterpret_cast(buf), bytes_read); + + offset += bytes_read; + to_decrypt_len -= bytes_read; + } + + cout << "ok" << endl; + + return 0; +} diff --git a/QMC2-decoder/QMC2-decoder.h b/QMC2-decoder/QMC2-decoder.h new file mode 100644 index 0000000..fa81b2c --- /dev/null +++ b/QMC2-decoder/QMC2-decoder.h @@ -0,0 +1,8 @@ +// QMC2-decoder.h : Include file for standard system include files, +// or project specific include files. + +#pragma once + +#include + +// TODO: Reference additional headers your program requires here. diff --git a/QMC2-decoder/crypto/IStreamEncryptAndDecrypt.h b/QMC2-decoder/crypto/IStreamEncryptAndDecrypt.h new file mode 100644 index 0000000..5b3461b --- /dev/null +++ b/QMC2-decoder/crypto/IStreamEncryptAndDecrypt.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +class IStreamEncryptAndDecrypt { + virtual void Decrypt(uint64_t offset, uint8_t* buf, size_t len) = 0; + virtual void Encrypt(uint64_t offset, uint8_t* buf, size_t len) = 0; +}; diff --git a/QMC2-decoder/crypto/KeyDec.cpp b/QMC2-decoder/crypto/KeyDec.cpp new file mode 100644 index 0000000..d92f223 --- /dev/null +++ b/QMC2-decoder/crypto/KeyDec.cpp @@ -0,0 +1,26 @@ +#include "KeyDec.h" + +#include + +KeyDec::~KeyDec() +{ + if (key) { + delete[] key; + key = nullptr; + } + + if (key_len) { + key_len = 0; + } +} + +void KeyDec::GetKey(uint8_t*& key, size_t& key_size) +{ + if (key && key_len > 0 && key) { + key_size = key_len; + key = new uint8_t[key_len]; + memcpy(key, this->key, key_len); + } else { + key_size = 0; + } +} diff --git a/QMC2-decoder/crypto/KeyDec.h b/QMC2-decoder/crypto/KeyDec.h new file mode 100644 index 0000000..f307780 --- /dev/null +++ b/QMC2-decoder/crypto/KeyDec.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class KeyDec { +public: + void GetKey(uint8_t*& key, size_t& key_size); + ~KeyDec(); + +private: + uint8_t* key; + size_t key_len; +}; diff --git a/QMC2-decoder/crypto/StreamCencrypt.cpp b/QMC2-decoder/crypto/StreamCencrypt.cpp new file mode 100644 index 0000000..88bef9c --- /dev/null +++ b/QMC2-decoder/crypto/StreamCencrypt.cpp @@ -0,0 +1,200 @@ +#include "StreamCencrypt.h" + +#include + +void StreamCencrypt::StreamEncrypt(uint64_t offset, uint8_t* buf, size_t len) +{ + if (N > 300) { + ProcessByRC4(offset, buf, len); + } + else { + for (size_t i = 0; i < len; i++) + { + buf[i] ^= mapL(offset + i); + } + } +} + +void StreamCencrypt::StreamDecrypt(uint64_t offset, uint8_t* buf, size_t len) +{ + this->StreamEncrypt(offset, buf, len); +} + +void StreamCencrypt::SetKeyDec(KeyDec* key_dec) +{ + Uninit(); + rc4_key = nullptr; + if (key_dec) { + key_dec->GetKey(this->rc4_key, N); + if (N > 300) { + InitRC4KSA(); + } + } +} + +void StreamCencrypt::Uninit() +{ + // reset initial rc4 key + if (rc4_key) { + delete[] rc4_key; + rc4_key = nullptr; + } + + // reset sbox + this->N = 0; + if (S) { + delete[] S; + S = nullptr; + } +} + +#define FIRST_SEGMENT_SIZE (0x80) +#define SEGMENT_SIZE (0x1400) + +void StreamCencrypt::ProcessByRC4(size_t offset, uint8_t* buf, size_t size) +{ + auto len = size; + + // Initial segment + if (offset < FIRST_SEGMENT_SIZE) { + auto len_segment = std::min(size, FIRST_SEGMENT_SIZE - offset); + EncFirstSegment(offset, buf, len_segment); + len -= len_segment; + buf += len_segment; + offset += len_segment; + } + + uint8_t* S = new uint8_t[N]; + memset(S, 0, N); + + // Align segment + if (offset % SEGMENT_SIZE != 0) { + auto len_segment = std::min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), len); + EncASegment(S, offset, buf, len_segment); + len -= len_segment; + buf += len_segment; + offset += len_segment; + } + + // Batch process segments + while (len > SEGMENT_SIZE) { + EncASegment(S, offset, buf, SEGMENT_SIZE); + len -= SEGMENT_SIZE; + buf += SEGMENT_SIZE; + offset += SEGMENT_SIZE; + } + + // Last segment (incomplete segment) + if (len > 0) { + EncASegment(S, offset, buf, len); + } + + delete[] S; +} + +uint64_t StreamCencrypt::GetSegmentKey(uint64_t id, uint64_t seed) +{ + return uint64_t((double)this->key_hash / double((id + 1) * seed) * 100.0); +} + +void StreamCencrypt::EncFirstSegment(size_t offset, uint8_t* buf, size_t len) +{ + for (size_t i = 0; i < len; i++) { + offset++; + + uint64_t key = uint64_t{ this->rc4_key[offset % this->N] }; + buf[i] ^= this->rc4_key[GetSegmentKey(offset, key) % this->N]; + } +} + +void StreamCencrypt::EncASegment(uint8_t* S, size_t offset, uint8_t* buf, size_t len) +{ + if (rc4_key == nullptr) { + // We need to initialise RC4 key first! + return; + } + + const auto N = this->N; + + // Initialise a new seedbox + memcpy(S, this->S, N); + + // Calculate segment id + int segment_id = (offset / SEGMENT_SIZE) & 0x1FF; + + // Calculate the number of bytes to skip. + // The initial "key" derived from segment id, plus the current offset. + auto skip_len = GetSegmentKey(offset / SEGMENT_SIZE, this->rc4_key[segment_id]) & 0x1FF; + skip_len += offset % SEGMENT_SIZE; + + int j = 0; + int k = 0; + for (size_t i = 0; i < skip_len; i++) { + j = (j + 1) % N; + k = (S[j] + k) % N; + std::swap(S[j], S[k]); + } + + // Now we also manipulate the buffer: + for (size_t i = 0; i < skip_len; i++) { + j = (j + 1) % N; + k = (S[j] + k) % N; + std::swap(S[j], S[k]); + + buf[i] ^= S[(S[j] + S[k]) % N]; + } +} + +void StreamCencrypt::InitRC4KSA() +{ + if (!S) { + S = new uint8_t[N](); + } + + for (size_t i = 0; i < N; ++i) { + S[i] = i & 0xFF; + } + + int j = 0; + for (size_t i = 0; i < N; ++i) { + j = (S[i] + j + rc4_key[i % N]) % N; + std::swap(S[i], S[j]); + } + + GetHashBase(); +} + +void StreamCencrypt::GetHashBase() +{ + this->key_hash = 1; + for (size_t i = 0; i < this->N; i++) { + int32_t value = int32_t{ this->rc4_key[i] }; + + // ignore if key char is '\x00' + if (!value) continue; + + auto next_hash = this->key_hash * value; + if (next_hash == 0 || next_hash <= this->key_hash) + break; + + this->key_hash = next_hash; + } +} + +inline uint8_t rotate(uint8_t value, int bits) { + int rotate = (bits + 4) % 8; + auto left = value << rotate; + auto right = value >> rotate; + return uint8_t(left | right); +} + +uint8_t StreamCencrypt::mapL(uint64_t offset) +{ + if (offset > 0x7FFF) + offset %= 0x7FFF; + + uint64_t key = (offset * offset + 71214) % this->N; + + uint8_t value = this->rc4_key[key]; + return rotate(value, key & 0b0111); +} diff --git a/QMC2-decoder/crypto/StreamCencrypt.h b/QMC2-decoder/crypto/StreamCencrypt.h new file mode 100644 index 0000000..748ffd6 --- /dev/null +++ b/QMC2-decoder/crypto/StreamCencrypt.h @@ -0,0 +1,36 @@ +#pragma once + +#include "IStreamEncryptAndDecrypt.h" +#include "KeyDec.h" + +#include + +class StreamCencrypt : public IStreamEncryptAndDecrypt { +public: + void StreamEncrypt(uint64_t offset, uint8_t* buf, size_t len); + void StreamDecrypt(uint64_t offset, uint8_t* buf, size_t len); + + bool CheckCallerLegal() { + return true; + } + void Uninit(); + void EncASegment(uint8_t* sbox, size_t offset, uint8_t* buf, size_t len); + void EncFirstSegment(size_t offset, uint8_t* buffer, size_t size); + void ProcessByRC4(size_t offset, uint8_t* buffer, size_t buffer_size); + +private: + uint32_t key_hash = 0; + + // RC4 vars + uint8_t* rc4_key = nullptr; + uint8_t* S = nullptr; + size_t N = 0; + + void InitRC4KSA(); + void GetHashBase(); + uint8_t mapL(uint64_t offset); + + uint64_t GetSegmentKey(uint64_t a, uint64_t b); + + void SetKeyDec(KeyDec* key_dec); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b1da0c --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# QMC2-Decode + +解密 `mflac` / `mgg1` 文件到 `flac` / `ogg` 文件。 + +## 构建 + +注意:克隆仓库的时候 submodule 也要一并同步。 + +```sh +mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make +``` + +二进制文件可以在 `build/QMC2-decoder/QMC2-decoder` 找到 (Windows 下加上 .exe)。 + +## 使用方式 + +```sh +QMC2-decoder encrypted_file.mflac decrypted.flac +``` + +## 致谢 + +- [2021/08/26 MGG/MFLAC研究进展][research] by @ix64 & @Akarinnnnn +- [unlock-music 项目][unlock-music] +- 使用 Visual Studio 2022 进行开发 + +[research]: https://gist.github.com/ix64/bcd72c151f21e1b050c9cc52d6ff27d5 +[unlock-music]: https://github.com/unlock-music/unlock-music