mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-02-20 08:14:55 -05:00
Add integrity hash support and SRP tuning options
This commit is contained in:
@@ -90,6 +90,7 @@ set(WOWEE_SOURCES
|
||||
src/auth/auth_opcodes.cpp
|
||||
src/auth/auth_packets.cpp
|
||||
src/auth/pin_auth.cpp
|
||||
src/auth/integrity.cpp
|
||||
src/auth/srp.cpp
|
||||
src/auth/big_num.cpp
|
||||
src/auth/crypto.cpp
|
||||
@@ -444,6 +445,28 @@ set_target_properties(auth_probe PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
|
||||
)
|
||||
|
||||
# ---- Tool: auth_login_probe (challenge + proof probe) ----
|
||||
add_executable(auth_login_probe
|
||||
tools/auth_login_probe/main.cpp
|
||||
src/auth/auth_packets.cpp
|
||||
src/auth/auth_opcodes.cpp
|
||||
src/auth/crypto.cpp
|
||||
src/auth/integrity.cpp
|
||||
src/auth/big_num.cpp
|
||||
src/auth/srp.cpp
|
||||
src/network/packet.cpp
|
||||
src/network/socket.cpp
|
||||
src/network/tcp_socket.cpp
|
||||
src/core/logger.cpp
|
||||
)
|
||||
target_include_directories(auth_login_probe PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries(auth_login_probe PRIVATE Threads::Threads OpenSSL::Crypto)
|
||||
set_target_properties(auth_login_probe PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
|
||||
)
|
||||
|
||||
# ---- Tool: blp_convert (BLP ↔ PNG) ----
|
||||
add_executable(blp_convert
|
||||
tools/blp_convert/main.cpp
|
||||
|
||||
@@ -111,6 +111,7 @@ private:
|
||||
uint8_t securityFlags_ = 0;
|
||||
uint32_t pinGridSeed_ = 0;
|
||||
std::array<uint8_t, 16> pinServerSalt_{}; // from LOGON_CHALLENGE response
|
||||
std::array<uint8_t, 16> checksumSalt_{}; // from LOGON_CHALLENGE response (integrity salt)
|
||||
std::string pendingSecurityCode_;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ struct LogonChallengeResponse {
|
||||
std::vector<uint8_t> g; // Generator (variable, usually 1 byte)
|
||||
std::vector<uint8_t> N; // Prime modulus (variable, usually 256 bytes)
|
||||
std::vector<uint8_t> salt; // Salt (32 bytes)
|
||||
std::array<uint8_t, 16> checksumSalt{}; // aka "crc_salt"/integrity salt
|
||||
uint8_t securityFlags;
|
||||
|
||||
// PIN extension (securityFlags & 0x01)
|
||||
@@ -66,6 +67,7 @@ public:
|
||||
static network::Packet build(const std::vector<uint8_t>& A,
|
||||
const std::vector<uint8_t>& M1,
|
||||
uint8_t securityFlags,
|
||||
const std::array<uint8_t, 20>* crcHash,
|
||||
const std::array<uint8_t, 16>* pinClientSalt,
|
||||
const std::array<uint8_t, 20>* pinHash);
|
||||
};
|
||||
|
||||
35
include/auth/integrity.hpp
Normal file
35
include/auth/integrity.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
|
||||
// Computes the LOGON_PROOF "CRC hash" / integrity hash for the legacy WoW login protocol.
|
||||
//
|
||||
// Algorithm (per WoWDev/gtker docs):
|
||||
// checksum = HMAC_SHA1(checksumSalt, concatenated_file_bytes)
|
||||
// crc_hash = SHA1(clientPublicKey || checksum)
|
||||
//
|
||||
// clientPublicKey is the 32-byte A as sent on the wire.
|
||||
//
|
||||
// Returns false if any file is missing/unreadable.
|
||||
bool computeIntegrityHashWin32(const std::array<uint8_t, 16>& checksumSalt,
|
||||
const std::vector<uint8_t>& clientPublicKeyA,
|
||||
const std::string& miscDir,
|
||||
std::array<uint8_t, 20>& outHash,
|
||||
std::string& outError);
|
||||
|
||||
// Same as computeIntegrityHashWin32, but allows selecting the EXE filename used in the file set.
|
||||
bool computeIntegrityHashWin32WithExe(const std::array<uint8_t, 16>& checksumSalt,
|
||||
const std::vector<uint8_t>& clientPublicKeyA,
|
||||
const std::string& miscDir,
|
||||
const std::string& exeName,
|
||||
std::array<uint8_t, 20>& outHash,
|
||||
std::string& outError);
|
||||
|
||||
} // namespace auth
|
||||
} // namespace wowee
|
||||
@@ -27,6 +27,15 @@ public:
|
||||
const std::vector<uint8_t>& N,
|
||||
const std::vector<uint8_t>& salt);
|
||||
|
||||
// Some SRP implementations use k = H(N|g) instead of the WoW-specific k=3.
|
||||
// Default is false (k=3).
|
||||
void setUseHashedK(bool enabled) { useHashedK_ = enabled; }
|
||||
|
||||
// Controls how SHA1 outputs are interpreted when converted to big integers (x, u, optionally k).
|
||||
// Many SRP implementations treat hash outputs as big-endian integers.
|
||||
// Default is false (treat hash outputs as little-endian integers).
|
||||
void setHashBigEndian(bool enabled) { hashBigEndian_ = enabled; }
|
||||
|
||||
// Get client public ephemeral (A) - send to server
|
||||
std::vector<uint8_t> getA() const;
|
||||
|
||||
@@ -73,6 +82,8 @@ private:
|
||||
std::vector<uint8_t> stored_auth_hash; // Pre-computed SHA1(UPPER(user):UPPER(pass))
|
||||
|
||||
bool initialized = false;
|
||||
bool useHashedK_ = false;
|
||||
bool hashBigEndian_ = false;
|
||||
};
|
||||
|
||||
} // namespace auth
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "auth/auth_handler.hpp"
|
||||
#include "auth/pin_auth.hpp"
|
||||
#include "auth/integrity.hpp"
|
||||
#include "network/tcp_socket.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include "core/logger.hpp"
|
||||
@@ -7,6 +8,7 @@
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
@@ -105,6 +107,7 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass,
|
||||
securityFlags_ = 0;
|
||||
pinGridSeed_ = 0;
|
||||
pinServerSalt_ = {};
|
||||
checksumSalt_ = {};
|
||||
|
||||
// Initialize SRP
|
||||
srp = std::make_unique<SRP>();
|
||||
@@ -139,6 +142,7 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto
|
||||
securityFlags_ = 0;
|
||||
pinGridSeed_ = 0;
|
||||
pinServerSalt_ = {};
|
||||
checksumSalt_ = {};
|
||||
|
||||
// Initialize SRP with pre-computed hash
|
||||
srp = std::make_unique<SRP>();
|
||||
@@ -196,6 +200,7 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
|
||||
srp->feed(response.B, response.g, response.N, response.salt);
|
||||
|
||||
securityFlags_ = response.securityFlags;
|
||||
checksumSalt_ = response.checksumSalt;
|
||||
if (securityFlags_ & 0x01) {
|
||||
pinGridSeed_ = response.pinGridSeed;
|
||||
pinServerSalt_ = response.pinSalt;
|
||||
@@ -222,6 +227,8 @@ void AuthHandler::sendLogonProof() {
|
||||
std::array<uint8_t, 20> pinHash{};
|
||||
const std::array<uint8_t, 16>* pinClientSaltPtr = nullptr;
|
||||
const std::array<uint8_t, 20>* pinHashPtr = nullptr;
|
||||
std::array<uint8_t, 20> crcHash{};
|
||||
const std::array<uint8_t, 20>* crcHashPtr = nullptr;
|
||||
|
||||
if (securityFlags_ & 0x01) {
|
||||
try {
|
||||
@@ -236,20 +243,54 @@ void AuthHandler::sendLogonProof() {
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol < 8 uses a shorter proof packet (no securityFlags byte).
|
||||
if (clientInfo.protocolVersion < 8) {
|
||||
auto packet = LogonProofPacket::buildLegacy(A, M1);
|
||||
socket->send(packet);
|
||||
} else {
|
||||
auto packet = LogonProofPacket::build(A, M1, securityFlags_, pinClientSaltPtr, pinHashPtr);
|
||||
socket->send(packet);
|
||||
|
||||
if (securityFlags_ & 0x04) {
|
||||
// TrinityCore-style Google Authenticator token: send immediately after proof.
|
||||
const std::string token = pendingSecurityCode_;
|
||||
auto tokPkt = AuthenticatorTokenPacket::build(token);
|
||||
socket->send(tokPkt);
|
||||
// Legacy client integrity hash (aka "CRC hash"). Some servers enforce this for classic builds.
|
||||
// We compute it when checksumSalt was provided (always present on success challenge) and files exist.
|
||||
{
|
||||
std::vector<std::string> candidateDirs;
|
||||
if (const char* env = std::getenv("WOWEE_INTEGRITY_DIR")) {
|
||||
if (env && *env) candidateDirs.push_back(env);
|
||||
}
|
||||
// Default local extraction layout
|
||||
candidateDirs.push_back("Data/misc");
|
||||
// Common turtle repack location used in this workspace
|
||||
if (const char* home = std::getenv("HOME")) {
|
||||
if (home && *home) {
|
||||
candidateDirs.push_back(std::string(home) + "/Downloads/twmoa_1180");
|
||||
candidateDirs.push_back(std::string(home) + "/twmoa_1180");
|
||||
}
|
||||
}
|
||||
|
||||
const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" };
|
||||
bool ok = false;
|
||||
std::string lastErr;
|
||||
for (const auto& dir : candidateDirs) {
|
||||
for (const char* exe : candidateExes) {
|
||||
std::string err;
|
||||
if (computeIntegrityHashWin32WithExe(checksumSalt_, A, dir, exe, crcHash, err)) {
|
||||
crcHashPtr = &crcHash;
|
||||
LOG_INFO("Integrity hash computed from ", dir, " (", exe, ")");
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
lastErr = err;
|
||||
}
|
||||
if (ok) break;
|
||||
}
|
||||
if (!ok) {
|
||||
LOG_WARNING("Integrity hash not computed (", lastErr,
|
||||
"). Server may reject classic clients without it. "
|
||||
"Set WOWEE_INTEGRITY_DIR to your client folder.");
|
||||
}
|
||||
}
|
||||
|
||||
auto packet = LogonProofPacket::build(A, M1, securityFlags_, crcHashPtr, pinClientSaltPtr, pinHashPtr);
|
||||
socket->send(packet);
|
||||
|
||||
if ((securityFlags_ & 0x04) && clientInfo.protocolVersion >= 8) {
|
||||
// TrinityCore-style Google Authenticator token: send immediately after proof.
|
||||
const std::string token = pendingSecurityCode_;
|
||||
auto tokPkt = AuthenticatorTokenPacket::build(token);
|
||||
socket->send(tokPkt);
|
||||
}
|
||||
|
||||
setState(AuthState::PROOF_SENT);
|
||||
|
||||
@@ -126,9 +126,9 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge
|
||||
response.salt[i] = packet.readUInt8();
|
||||
}
|
||||
|
||||
// Unknown/padding - 16 bytes
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
packet.readUInt8();
|
||||
// Integrity salt / CRC salt - 16 bytes
|
||||
for (size_t i = 0; i < response.checksumSalt.size(); ++i) {
|
||||
response.checksumSalt[i] = packet.readUInt8();
|
||||
}
|
||||
|
||||
// Security flags
|
||||
@@ -162,7 +162,7 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge
|
||||
|
||||
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
|
||||
const std::vector<uint8_t>& M1) {
|
||||
return build(A, M1, 0, nullptr, nullptr);
|
||||
return build(A, M1, 0, nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
network::Packet LogonProofPacket::buildLegacy(const std::vector<uint8_t>& A,
|
||||
@@ -185,6 +185,7 @@ network::Packet LogonProofPacket::buildLegacy(const std::vector<uint8_t>& A,
|
||||
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
|
||||
const std::vector<uint8_t>& M1,
|
||||
uint8_t securityFlags,
|
||||
const std::array<uint8_t, 20>* crcHash,
|
||||
const std::array<uint8_t, 16>* pinClientSalt,
|
||||
const std::array<uint8_t, 20>* pinHash) {
|
||||
if (A.size() != 32) {
|
||||
@@ -202,9 +203,11 @@ network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
|
||||
// M1 (client proof) - 20 bytes
|
||||
packet.writeBytes(M1.data(), M1.size());
|
||||
|
||||
// CRC hash - 20 bytes (zeros)
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
packet.writeUInt8(0);
|
||||
// CRC hash / integrity hash - 20 bytes
|
||||
if (crcHash) {
|
||||
packet.writeBytes(crcHash->data(), crcHash->size());
|
||||
} else {
|
||||
for (int i = 0; i < 20; ++i) packet.writeUInt8(0);
|
||||
}
|
||||
|
||||
// Number of keys
|
||||
|
||||
91
src/auth/integrity.cpp
Normal file
91
src/auth/integrity.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "auth/integrity.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
|
||||
static bool readWholeFile(const std::string& path, std::vector<uint8_t>& out, std::string& err) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) {
|
||||
err = "missing: " + path;
|
||||
return false;
|
||||
}
|
||||
f.seekg(0, std::ios::end);
|
||||
std::streamoff size = f.tellg();
|
||||
if (size < 0) size = 0;
|
||||
f.seekg(0, std::ios::beg);
|
||||
out.resize(static_cast<size_t>(size));
|
||||
if (size > 0) {
|
||||
f.read(reinterpret_cast<char*>(out.data()), size);
|
||||
if (!f) {
|
||||
err = "read failed: " + path;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool computeIntegrityHashWin32WithExe(const std::array<uint8_t, 16>& checksumSalt,
|
||||
const std::vector<uint8_t>& clientPublicKeyA,
|
||||
const std::string& miscDir,
|
||||
const std::string& exeName,
|
||||
std::array<uint8_t, 20>& outHash,
|
||||
std::string& outError) {
|
||||
// Files expected by 1.12.x Windows clients for the integrity check.
|
||||
// If this needs to vary by build, make it data-driven in expansion.json later.
|
||||
const char* kFiles[] = {
|
||||
nullptr, // exeName
|
||||
"fmod.dll",
|
||||
"ijl15.dll",
|
||||
"dbghelp.dll",
|
||||
"unicows.dll",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> allFiles;
|
||||
std::string err;
|
||||
for (size_t idx = 0; idx < (sizeof(kFiles) / sizeof(kFiles[0])); ++idx) {
|
||||
const char* name = kFiles[idx];
|
||||
std::string nameStr = name ? std::string(name) : exeName;
|
||||
std::vector<uint8_t> bytes;
|
||||
std::string path = miscDir;
|
||||
if (!path.empty() && path.back() != '/') path += '/';
|
||||
path += nameStr;
|
||||
if (!readWholeFile(path, bytes, err)) {
|
||||
outError = err;
|
||||
return false;
|
||||
}
|
||||
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
// HMAC_SHA1(checksumSalt, allFiles)
|
||||
std::vector<uint8_t> key(checksumSalt.begin(), checksumSalt.end());
|
||||
const std::vector<uint8_t> checksum = Crypto::hmacSHA1(key, allFiles); // 20 bytes
|
||||
|
||||
// SHA1(A || checksum)
|
||||
std::vector<uint8_t> shaIn;
|
||||
shaIn.reserve(clientPublicKeyA.size() + checksum.size());
|
||||
shaIn.insert(shaIn.end(), clientPublicKeyA.begin(), clientPublicKeyA.end());
|
||||
shaIn.insert(shaIn.end(), checksum.begin(), checksum.end());
|
||||
const std::vector<uint8_t> finalHash = Crypto::sha1(shaIn);
|
||||
|
||||
if (finalHash.size() != outHash.size()) {
|
||||
outError = "unexpected sha1 size";
|
||||
return false;
|
||||
}
|
||||
std::copy(finalHash.begin(), finalHash.end(), outHash.begin());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool computeIntegrityHashWin32(const std::array<uint8_t, 16>& checksumSalt,
|
||||
const std::vector<uint8_t>& clientPublicKeyA,
|
||||
const std::string& miscDir,
|
||||
std::array<uint8_t, 20>& outHash,
|
||||
std::string& outError) {
|
||||
return computeIntegrityHashWin32WithExe(checksumSalt, clientPublicKeyA, miscDir, "WoW.exe", outHash, outError);
|
||||
}
|
||||
|
||||
} // namespace auth
|
||||
} // namespace wowee
|
||||
@@ -58,6 +58,18 @@ void SRP::feed(const std::vector<uint8_t>& B_bytes,
|
||||
this->N = BigNum(N_bytes, true);
|
||||
this->s = BigNum(salt_bytes, true);
|
||||
|
||||
if (useHashedK_) {
|
||||
// k = H(N | g) (SRP-6a style)
|
||||
std::vector<uint8_t> Ng;
|
||||
Ng.insert(Ng.end(), N_bytes.begin(), N_bytes.end());
|
||||
Ng.insert(Ng.end(), g_bytes.begin(), g_bytes.end());
|
||||
std::vector<uint8_t> k_bytes = Crypto::sha1(Ng);
|
||||
k = BigNum(k_bytes, !hashBigEndian_);
|
||||
LOG_DEBUG("Using hashed SRP multiplier k=H(N|g)");
|
||||
} else {
|
||||
k = BigNum(K_VALUE);
|
||||
}
|
||||
|
||||
LOG_DEBUG("SRP challenge data loaded");
|
||||
|
||||
// Now compute everything in sequence
|
||||
@@ -72,7 +84,7 @@ void SRP::feed(const std::vector<uint8_t>& B_bytes,
|
||||
x_input.insert(x_input.end(), salt_bytes.begin(), salt_bytes.end());
|
||||
x_input.insert(x_input.end(), auth_hash.begin(), auth_hash.end());
|
||||
std::vector<uint8_t> x_bytes = Crypto::sha1(x_input);
|
||||
x = BigNum(x_bytes, true);
|
||||
x = BigNum(x_bytes, !hashBigEndian_);
|
||||
LOG_DEBUG("Computed x (salted password hash)");
|
||||
|
||||
// 3. Generate client ephemeral (a, A)
|
||||
@@ -151,7 +163,7 @@ void SRP::computeSessionKey() {
|
||||
AB.insert(AB.end(), B_bytes_u.begin(), B_bytes_u.end());
|
||||
|
||||
std::vector<uint8_t> u_bytes = Crypto::sha1(AB);
|
||||
u = BigNum(u_bytes, true);
|
||||
u = BigNum(u_bytes, !hashBigEndian_);
|
||||
|
||||
LOG_DEBUG("Scrambler u calculated");
|
||||
|
||||
|
||||
350
tools/auth_login_probe/main.cpp
Normal file
350
tools/auth_login_probe/main.cpp
Normal file
@@ -0,0 +1,350 @@
|
||||
#include "auth/auth_packets.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "auth/integrity.hpp"
|
||||
#include "auth/srp.hpp"
|
||||
#include "network/tcp_socket.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace wowee;
|
||||
|
||||
static void usage() {
|
||||
std::cerr
|
||||
<< "Usage:\n"
|
||||
<< " auth_login_probe <host> <port> <account> <major> <minor> <patch> <build> <proto> <locale> \\\n"
|
||||
<< " (--password <pass> | --hash <hexsha1>) [--proof legacy|v8|auto]\n"
|
||||
<< "\n"
|
||||
<< "Notes:\n"
|
||||
<< " - --hash expects SHA1(UPPER(user):UPPER(pass)) in hex.\n"
|
||||
<< " - This tool only probes auth; it does not connect to world.\n";
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> hexToBytes(const std::string& hex) {
|
||||
std::vector<uint8_t> out;
|
||||
std::string h;
|
||||
h.reserve(hex.size());
|
||||
for (char c : hex) {
|
||||
if (!std::isspace(static_cast<unsigned char>(c))) h.push_back(c);
|
||||
}
|
||||
if (h.size() % 2 != 0) throw std::runtime_error("hex length must be even");
|
||||
out.reserve(h.size() / 2);
|
||||
for (size_t i = 0; i < h.size(); i += 2) {
|
||||
auto byteStr = h.substr(i, 2);
|
||||
uint8_t b = static_cast<uint8_t>(std::stoul(byteStr, nullptr, 16));
|
||||
out.push_back(b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string upperAscii(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
enum class ProofFormat { Auto, Legacy, V8 };
|
||||
enum class CrcAFormat { Wire, BigEndian };
|
||||
enum class WireAFormat { Little, Big };
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 11) {
|
||||
usage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
const std::string host = argv[1];
|
||||
const int port = std::atoi(argv[2]);
|
||||
const std::string account = argv[3];
|
||||
const int major = std::atoi(argv[4]);
|
||||
const int minor = std::atoi(argv[5]);
|
||||
const int patch = std::atoi(argv[6]);
|
||||
const int build = std::atoi(argv[7]);
|
||||
const int proto = std::atoi(argv[8]);
|
||||
const std::string locale = argv[9];
|
||||
|
||||
std::string password;
|
||||
std::vector<uint8_t> authHash;
|
||||
bool havePassword = false;
|
||||
bool haveHash = false;
|
||||
ProofFormat proofFmt = ProofFormat::Auto;
|
||||
CrcAFormat crcA = CrcAFormat::Wire;
|
||||
WireAFormat wireA = WireAFormat::Little;
|
||||
std::string integrityExe = "WoW.exe";
|
||||
bool serverValuesBigEndian = false;
|
||||
std::string miscDir = "Data/misc";
|
||||
bool useHashedK = false;
|
||||
bool hashBigEndian = false;
|
||||
|
||||
for (int i = 10; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
if (a == "--password" && i + 1 < argc) {
|
||||
password = argv[++i];
|
||||
havePassword = true;
|
||||
continue;
|
||||
}
|
||||
if (a == "--hash" && i + 1 < argc) {
|
||||
authHash = hexToBytes(argv[++i]);
|
||||
haveHash = true;
|
||||
continue;
|
||||
}
|
||||
if (a == "--proof" && i + 1 < argc) {
|
||||
std::string v = argv[++i];
|
||||
if (v == "auto") proofFmt = ProofFormat::Auto;
|
||||
else if (v == "legacy") proofFmt = ProofFormat::Legacy;
|
||||
else if (v == "v8") proofFmt = ProofFormat::V8;
|
||||
else {
|
||||
std::cerr << "Unknown --proof value: " << v << "\n";
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (a == "--crc-a" && i + 1 < argc) {
|
||||
std::string v = argv[++i];
|
||||
if (v == "wire") crcA = CrcAFormat::Wire;
|
||||
else if (v == "be") crcA = CrcAFormat::BigEndian;
|
||||
else {
|
||||
std::cerr << "Unknown --crc-a value: " << v << " (expected wire|be)\n";
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (a == "--integrity-exe" && i + 1 < argc) {
|
||||
integrityExe = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a == "--misc-dir" && i + 1 < argc) {
|
||||
miscDir = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a == "--server-values" && i + 1 < argc) {
|
||||
std::string v = argv[++i];
|
||||
if (v == "le") serverValuesBigEndian = false;
|
||||
else if (v == "be") serverValuesBigEndian = true;
|
||||
else {
|
||||
std::cerr << "Unknown --server-values value: " << v << " (expected le|be)\n";
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (a == "--wire-a" && i + 1 < argc) {
|
||||
std::string v = argv[++i];
|
||||
if (v == "le") wireA = WireAFormat::Little;
|
||||
else if (v == "be") wireA = WireAFormat::Big;
|
||||
else {
|
||||
std::cerr << "Unknown --wire-a value: " << v << " (expected le|be)\n";
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (a == "--k" && i + 1 < argc) {
|
||||
std::string v = argv[++i];
|
||||
if (v == "3") useHashedK = false;
|
||||
else if (v == "hashed") useHashedK = true;
|
||||
else {
|
||||
std::cerr << "Unknown --k value: " << v << " (expected 3|hashed)\n";
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (a == "--hash-endian" && i + 1 < argc) {
|
||||
std::string v = argv[++i];
|
||||
if (v == "le") hashBigEndian = false;
|
||||
else if (v == "be") hashBigEndian = true;
|
||||
else {
|
||||
std::cerr << "Unknown --hash-endian value: " << v << " (expected le|be)\n";
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
std::cerr << "Unknown arg: " << a << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!havePassword && !haveHash) {
|
||||
std::cerr << "Must supply --password or --hash\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
auth::ClientInfo info;
|
||||
info.majorVersion = static_cast<uint8_t>(major);
|
||||
info.minorVersion = static_cast<uint8_t>(minor);
|
||||
info.patchVersion = static_cast<uint8_t>(patch);
|
||||
info.build = static_cast<uint16_t>(build);
|
||||
info.protocolVersion = static_cast<uint8_t>(proto);
|
||||
info.locale = locale;
|
||||
info.platform = "x86";
|
||||
info.os = "Win";
|
||||
|
||||
std::atomic<bool> done{false};
|
||||
std::atomic<bool> sawDisconnect{false};
|
||||
std::atomic<bool> challengeOk{false};
|
||||
std::atomic<int> proofStatus{-1};
|
||||
std::atomic<int> chalCode{-1};
|
||||
|
||||
network::TCPSocket sock;
|
||||
std::unique_ptr<auth::SRP> srp;
|
||||
uint8_t securityFlags = 0;
|
||||
uint32_t pinSeed = 0;
|
||||
std::array<uint8_t, 16> pinSalt{};
|
||||
std::array<uint8_t, 16> checksumSalt{};
|
||||
|
||||
auto sendProof = [&]() {
|
||||
if (!srp) return;
|
||||
auto A = srp->getA();
|
||||
if (wireA == WireAFormat::Big) {
|
||||
std::reverse(A.begin(), A.end());
|
||||
}
|
||||
auto M1 = srp->getM1();
|
||||
|
||||
ProofFormat fmt = proofFmt;
|
||||
if (fmt == ProofFormat::Auto) {
|
||||
fmt = (info.protocolVersion < 8) ? ProofFormat::Legacy : ProofFormat::V8;
|
||||
}
|
||||
|
||||
// Try to compute the classic client integrity hash using local Data/misc.
|
||||
std::array<uint8_t, 20> crcHash{};
|
||||
const std::array<uint8_t, 20>* crcHashPtr = nullptr;
|
||||
{
|
||||
std::string err;
|
||||
std::vector<uint8_t> crcABytes = A;
|
||||
if (crcA == CrcAFormat::BigEndian) {
|
||||
std::reverse(crcABytes.begin(), crcABytes.end());
|
||||
}
|
||||
if (auth::computeIntegrityHashWin32WithExe(checksumSalt, crcABytes, miscDir, integrityExe, crcHash, err)) {
|
||||
crcHashPtr = &crcHash;
|
||||
std::cerr << "Computed integrity hash using " << miscDir << " (" << integrityExe << ")\n";
|
||||
} else {
|
||||
std::cerr << "Integrity hash not computed: " << err << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (fmt == ProofFormat::Legacy) {
|
||||
auto pkt = auth::LogonProofPacket::buildLegacy(A, M1);
|
||||
sock.send(pkt);
|
||||
std::cerr << "Sent LOGON_PROOF legacy (proto=" << (int)info.protocolVersion << ")\n";
|
||||
} else {
|
||||
auto pkt = auth::LogonProofPacket::build(A, M1, securityFlags, crcHashPtr, nullptr, nullptr);
|
||||
sock.send(pkt);
|
||||
std::cerr << "Sent LOGON_PROOF v8 (secFlags=0x" << std::hex << (int)securityFlags << std::dec << ")\n";
|
||||
}
|
||||
};
|
||||
|
||||
sock.setPacketCallback([&](const network::Packet& p) {
|
||||
network::Packet pkt = p;
|
||||
if (pkt.getSize() < 1) return;
|
||||
|
||||
uint8_t opcode = pkt.readUInt8();
|
||||
if (opcode == static_cast<uint8_t>(auth::AuthOpcode::LOGON_CHALLENGE)) {
|
||||
auth::LogonChallengeResponse resp{};
|
||||
if (!auth::LogonChallengeResponseParser::parse(pkt, resp)) {
|
||||
std::cerr << "Challenge parse failed\n";
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
chalCode = static_cast<int>(resp.result);
|
||||
if (!resp.isSuccess()) {
|
||||
std::cerr << "Challenge FAIL: " << auth::getAuthResultString(resp.result)
|
||||
<< " (0x" << std::hex << (int)resp.result << std::dec << ")\n";
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
|
||||
challengeOk = true;
|
||||
securityFlags = resp.securityFlags;
|
||||
pinSeed = resp.pinGridSeed;
|
||||
pinSalt = resp.pinSalt;
|
||||
checksumSalt = resp.checksumSalt;
|
||||
|
||||
srp = std::make_unique<auth::SRP>();
|
||||
srp->setUseHashedK(useHashedK);
|
||||
srp->setHashBigEndian(hashBigEndian);
|
||||
if (haveHash) {
|
||||
srp->initializeWithHash(account, authHash);
|
||||
} else {
|
||||
srp->initialize(account, password);
|
||||
}
|
||||
if (serverValuesBigEndian) {
|
||||
auto rev = [](std::vector<uint8_t> v) {
|
||||
std::reverse(v.begin(), v.end());
|
||||
return v;
|
||||
};
|
||||
srp->feed(rev(resp.B), rev(resp.g), rev(resp.N), rev(resp.salt));
|
||||
} else {
|
||||
srp->feed(resp.B, resp.g, resp.N, resp.salt);
|
||||
}
|
||||
|
||||
sendProof();
|
||||
return;
|
||||
}
|
||||
|
||||
if (opcode == static_cast<uint8_t>(auth::AuthOpcode::LOGON_PROOF)) {
|
||||
auth::LogonProofResponse resp{};
|
||||
if (!auth::LogonProofResponseParser::parse(pkt, resp)) {
|
||||
std::cerr << "Proof parse failed\n";
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
proofStatus = resp.status;
|
||||
if (resp.isSuccess()) {
|
||||
std::cerr << "Proof SUCCESS\n";
|
||||
} else {
|
||||
std::cerr << "Proof FAIL status=0x" << std::hex << (int)resp.status << std::dec << "\n";
|
||||
}
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (!sock.connect(host, static_cast<uint16_t>(port))) {
|
||||
std::cerr << "Connect failed\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
auto chal = auth::LogonChallengePacket::build(account, info);
|
||||
sock.send(chal);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!done) {
|
||||
sock.update();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
if (!sock.isConnected() && !done) {
|
||||
sawDisconnect = true;
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > 6000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sock.disconnect();
|
||||
|
||||
if (!done && sock.isConnected()) {
|
||||
std::cerr << "Timeout\n";
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (sawDisconnect && challengeOk && proofStatus.load() < 0) {
|
||||
std::cerr << "Server disconnected after challenge (no proof response parsed)\n";
|
||||
return 6;
|
||||
}
|
||||
|
||||
if (chalCode.load() >= 0 && chalCode.load() != 0) return chalCode.load();
|
||||
if (proofStatus.load() >= 0) return proofStatus.load();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user