解密器修正
This commit is contained in:
274
src/feistel.rs
Normal file
274
src/feistel.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
//! Feistel byte decryptor
|
||||
//! Corresponds to feistel_decrypt_next_byte @ 0x8004d4c1
|
||||
//!
|
||||
//! Each byte is decrypted through:
|
||||
//! 1. Feistel state update (ROL-based mixing)
|
||||
//! 2. S-Box A lookup on byte_counter (4 nibbles → inv → fwd)
|
||||
//! 3. S-Box B key derivation from rotated 0x8262a2877387e56c
|
||||
//! 4. Combined XOR with rotated 0xc4e52cd2e80e33b7
|
||||
//! 5. Anti-tamper polynomial hash → output byte
|
||||
|
||||
const KEY_CONST: u64 = 0x8262a2877387e56c;
|
||||
const MIX_CONST: u64 = 0xc4e52cd2e80e33b7;
|
||||
const FINAL_XOR: u32 = 0x0e80eca9;
|
||||
|
||||
// ── S-Box table extraction ──────────────────────────────────────────
|
||||
|
||||
const fn u64s_to_bytes(vals: [u64; 32]) -> [u8; 256] {
|
||||
let mut out = [0u8; 256];
|
||||
let mut i = 0;
|
||||
while i < 32 {
|
||||
let v = vals[i];
|
||||
out[i * 8 ] = (v & 0xFF) as u8;
|
||||
out[i * 8 + 1] = ((v >> 8) & 0xFF) as u8;
|
||||
out[i * 8 + 2] = ((v >> 16) & 0xFF) as u8;
|
||||
out[i * 8 + 3] = ((v >> 24) & 0xFF) as u8;
|
||||
out[i * 8 + 4] = ((v >> 32) & 0xFF) as u8;
|
||||
out[i * 8 + 5] = ((v >> 40) & 0xFF) as u8;
|
||||
out[i * 8 + 6] = ((v >> 48) & 0xFF) as u8;
|
||||
out[i * 8 + 7] = ((v >> 56) & 0xFF) as u8;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Table A forward (offset 0x00..0xFF, first load in decompilation)
|
||||
static SBOX_A_FWD: [u8; 256] = u64s_to_bytes([
|
||||
0x9ed15953c4cca4b6, 0x7aa6141967d5c621, 0xdc9783ea6c0e3fe7, 0x2bd2e6b13728c7b9,
|
||||
0xc55a41435e40bbeb, 0x4e6039525b008151, 0x1094755c8890642a, 0x8f8d0aae1723707c,
|
||||
0xfddf042465e5c08a, 0xbf1ecaf3275fd376, 0xf085f9302f46d8b7, 0xc820456d7fff22b4,
|
||||
0x34e05529f1963d1b, 0xde018c871de9b2f6, 0x6bcf7405ad4ccbfe, 0x79ac151af2d7c19f,
|
||||
0xe3f466913a07a561, 0xa033fc036818809d, 0x35c2b5d9fb317d9b, 0xf5ede247efa2b3e8,
|
||||
0x3bba9358be5dc9d4, 0x1f631202849289a3, 0x0678ce82ec1c0dee, 0x11b0a72d6a38db2c,
|
||||
0x9c6efa624a1669d6, 0x509ac3420f6fab95, 0xf799cd57f8567726, 0xafda4fdda84b0c3c,
|
||||
0x2ee473b825327244, 0x0b864971e18e3ea9, 0x09bc4d9813bd8ba1, 0xaa7b54d036487e08,
|
||||
]);
|
||||
|
||||
// Table A inverse (offset 0x100..0x1FF)
|
||||
static SBOX_A_INV: [u8; 256] = u64s_to_bytes([
|
||||
0x38565c35fe1b267e, 0x5f1123e743599f52, 0x0e087f546582a3e6, 0x37c10fc2fb7cbc45,
|
||||
0x3b16d7d1e1001e24, 0xaff9978a493a86a0, 0x5aeb9068acfdea95, 0xf410426eda04dfa8,
|
||||
0xb61283dc84755b53, 0xceefff6009c5150a, 0x92eea4fa31b44ff0, 0x48aea157bb622719,
|
||||
0x702f9e170cc388f6, 0xba4146816a7bb08b, 0x3e5d638da7d5dee3, 0x7d1a07b9b501f122,
|
||||
0xd01f7844e46d403c, 0xc76461766cbf6b5e, 0x7a96aa4ded8c21be, 0x73f8b703ec6f8e74,
|
||||
0xcb55982db1d3a966, 0x718f91ddd402add8, 0x673f139a72b89d50, 0x93d920140dd2e539,
|
||||
0xcfc04e2a79283369, 0xcaa29b2ea5f32505, 0x4afc99c451184bf2, 0x2c941d3658a6e034,
|
||||
0xcce2f7b30bbde8e9, 0x8777db8085292bc6, 0xd632c989cd474c9c, 0xb206f5c8301cab3d,
|
||||
]);
|
||||
|
||||
// Table B forward (second load, overwrites stack)
|
||||
static SBOX_B_FWD: [u8; 256] = u64s_to_bytes([
|
||||
0x5e63857d691096e3, 0xf5b29f30c1bce088, 0x6ca3117a3571ff48, 0x9abe3a1273b08ee6,
|
||||
0xdcdbf2e88a26900f, 0x588d282e554f9ddd, 0x566e221899549b84, 0xcab786d861cb47e7,
|
||||
0x193677c5f7dead8b, 0x31d4bafd333b9823, 0xe40a08392af36221, 0x5d42721afbe25f0e,
|
||||
0x75ecbd44ce529ee5, 0xa5ea4e04383ef4d7, 0xc60c166acc1fd540, 0xd6150bae7fd3b951,
|
||||
0x651e7cfe9703c4f6, 0x00141dee607be979, 0x6409c91ca927f1a6, 0x8f252c9ca0c86d59,
|
||||
0x6f508c02f8d217af, 0xa71b95a24d68132d, 0xc2bf4a4b5c6b6780, 0x43d057a487c7492f,
|
||||
0xed5beffacf058134, 0x3c7076660193b5ac, 0xc00d2b5aab24b8e1, 0xdf3fa832bb5391aa,
|
||||
0x37b3c307d106b1f9, 0x45f082fc89da8392, 0xa19478b4294c20eb, 0x3d7e74b6cd4146d9,
|
||||
]);
|
||||
|
||||
// Table B inverse (offset 0x100..0x1FF, second load)
|
||||
static SBOX_B_INV: [u8; 256] = u64s_to_bytes([
|
||||
0x10253c0ac9a9ccfa, 0xca0dd5e4675bcf42, 0x4fcb2ca86b979a49, 0x55a3b2991803929e,
|
||||
0x4b3aea77df22e90b, 0x3b1f83a77f580628, 0xfb204001da0c4182, 0x9d433e95ddb16991,
|
||||
0xd48d768af82ea600, 0x686ec25609a41d87, 0x441eb3887565cead, 0xcdf17eaae1e6af79,
|
||||
0x8027987c3108fcb6, 0x3872ded2156c52e2, 0xf7ab32b914e7ff17, 0xbd2b8b3d59a0c463,
|
||||
0xbc9f816f7196b7d0, 0xbb748f89ebe0d605, 0xdcd178378ec59bc6, 0x53a1dbbf7b84364a,
|
||||
0x12b470a2c1665e73, 0x1a13ed19c750352d, 0x93547a8557eef29c, 0x8cfe2aa5c0e3625c,
|
||||
0x3411f094aed9b84c, 0xfd48ec6d265a07ac, 0x47b5ba30d3f53360, 0x5d4586efc824164d,
|
||||
0x2f02646af4c304d8, 0x9029611bf9d7510f, 0xf346390e1c7dbee8, 0xf64ee5b03f235f21,
|
||||
]);
|
||||
|
||||
// ── Feistel state ───────────────────────────────────────────────────
|
||||
|
||||
struct FeistelState {
|
||||
state: u32,
|
||||
counter: u32,
|
||||
}
|
||||
|
||||
impl FeistelState {
|
||||
fn new() -> Self {
|
||||
Self { state: 0, counter: 0 }
|
||||
}
|
||||
|
||||
/// Advance Feistel state. Verified against disasm @ 0x8004d4c1 State '\0'.
|
||||
///
|
||||
/// tmp = state ^ counter
|
||||
/// new_state = tmp + (ROL32(counter + state, state & 0x1F) ^ ROL32(tmp, counter & 0x1F))
|
||||
/// counter += 1
|
||||
fn advance(&mut self) {
|
||||
let counter = self.counter;
|
||||
let state = self.state;
|
||||
self.counter = counter.wrapping_add(1);
|
||||
|
||||
let tmp = state ^ counter;
|
||||
let rol_a = counter.wrapping_add(state).rotate_left(state & 0x1F);
|
||||
let rol_b = tmp.rotate_left(counter & 0x1F);
|
||||
self.state = tmp.wrapping_add(rol_a ^ rol_b);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Single-byte decrypt ─────────────────────────────────────────────
|
||||
|
||||
/// Decrypt one byte from encrypted payload.
|
||||
///
|
||||
/// raw_byte: the encrypted input byte
|
||||
/// byte_counter: pre-increment counter (0 for first byte)
|
||||
/// feistel: mutable Feistel state (advanced each call)
|
||||
///
|
||||
/// Returns the decrypted byte.
|
||||
fn decrypt_byte(raw_byte: u8, byte_counter: u32, feistel: &mut FeistelState) -> u8 {
|
||||
// 1. Advance Feistel state
|
||||
feistel.advance();
|
||||
let st = feistel.state; // new state after advance
|
||||
|
||||
// 2. S-Box A lookups on byte_counter (4 byte lanes)
|
||||
let b3 = SBOX_A_FWD[SBOX_A_INV[(byte_counter >> 24) as usize] as usize];
|
||||
let b2 = SBOX_A_FWD[SBOX_A_INV[((byte_counter >> 16) & 0xFF) as usize] as usize];
|
||||
let b0 = SBOX_A_FWD[SBOX_A_INV[(byte_counter & 0xFF) as usize] as usize];
|
||||
let b1 = SBOX_A_FWD[SBOX_A_INV[((byte_counter >> 8) & 0xFF) as usize] as usize];
|
||||
|
||||
// 3. S-Box B key byte from rotated KEY_CONST
|
||||
let key_idx = (KEY_CONST.rotate_right(st & 0x1F) >> 56) as u8;
|
||||
let key_byte = SBOX_B_FWD[SBOX_B_INV[key_idx as usize] as usize];
|
||||
|
||||
// 4. Combined shift derivation
|
||||
let combined: u64 = (b0 as u64)
|
||||
| ((b1 as u64) << 8)
|
||||
| ((b2 as u64) << 16)
|
||||
| ((b3 as u64) << 24)
|
||||
| 0xacacacac_00000000u64;
|
||||
// Arithmetic right shift (sign bit is 1 due to 0xac... in high bytes)
|
||||
let shift = (((combined as i64) >> (raw_byte as u32 & 0x1f)) & 0x1f) as u32;
|
||||
|
||||
// 5. Mix: key_byte ^ ROL64(MIX_CONST, shift)[31:0] ^ FINAL_XOR
|
||||
let rotated = MIX_CONST.rotate_left(shift) as u32;
|
||||
let mixed = (key_byte as u32) ^ rotated ^ FINAL_XOR;
|
||||
|
||||
// 6. Polynomial hash → output byte
|
||||
polynomial_mix(raw_byte as u32, byte_counter, st, mixed)
|
||||
}
|
||||
|
||||
// ── Polynomial anti-tamper hash ─────────────────────────────────────
|
||||
// Faithfully translated from decompilation. All arithmetic is wrapping u32.
|
||||
|
||||
fn polynomial_mix(raw: u32, n: u32, st: u32, mix: u32) -> u8 {
|
||||
let w = |a: u32, b: u32| -> u32 { a.wrapping_mul(b) };
|
||||
let wa = |a: u32, b: u32| -> u32 { a.wrapping_add(b) };
|
||||
|
||||
// Derived values
|
||||
let shl12 = n << 12;
|
||||
let nshl12 = !shl12;
|
||||
let g = raw | !st; // uVar16
|
||||
let h = mix | shl12; // uVar22 (reused)
|
||||
let nn = !n; // uVar31
|
||||
let j = n ^ nshl12; // uVar25 (reused)
|
||||
let k = (n | shl12) ^ nshl12; // uVar26 (reused)
|
||||
let l = st & !raw; // uVar21 (reused)
|
||||
|
||||
// Linear factors
|
||||
let f7 = w(j, 0x6b00ec19_u32);
|
||||
let f6 = w(k, 0x7ce3eb82_u32);
|
||||
let f17 = w(raw, 0xb19d3cea_u32); // raw * (-0x4e62c316) as u32
|
||||
|
||||
let f23 = wa(wa(
|
||||
w(j, 0x1a82aada_u32),
|
||||
w(nshl12, 0xe6b8ed78_u32)), // nshl12 * (-0x19471288)
|
||||
w(nn, 0x013b9852_u32));
|
||||
|
||||
let f15 = wa(f17, f23);
|
||||
let f12 = wa(f15, w(k, 0xe441bcd4_u32)); // k * (-0x1bbe432c)
|
||||
let f11 = wa(w(st, 0x1a2900ca_u32), f12);
|
||||
let f13 = w(g, 0x612fbba4_u32);
|
||||
// f14 == f12 (algebraically identical, verified in report §5)
|
||||
let f10 = wa(wa(wa(
|
||||
w(mix, 0xfec467ae_u32), // mix * (-0x13b9852)
|
||||
f13), f12),
|
||||
w(st, 0x4e62c316_u32));
|
||||
|
||||
let f8 = w(nshl12, 0xad1a3c4c_u32); // nshl12 * (-0x52e5c3b4)
|
||||
let f9 = w(nn, 0x181b2865_u32);
|
||||
let f5 = w(st, 0x2bb9fc31_u32);
|
||||
let f4 = w(st, 0xd44603ce_u32); // st * (-0x2bb9fc32)
|
||||
let f3 = w(g, 0x0dc3d04a_u32);
|
||||
let f2 = w(mix, 0x38473458_u32);
|
||||
let f1 = w(mix, 0xaf9da343_u32); // mix * (-0x50625cbd)
|
||||
let fh = w(h, 0x181b2865_u32);
|
||||
|
||||
// Sum of squares * 0xEA
|
||||
let sq_sum = wa(wa(wa(wa(wa(wa(wa(wa(wa(wa(
|
||||
w(f8, f8), w(f9, f9)), w(raw, raw)), w(f7, f7)), w(f6, f6)),
|
||||
w(f5, f5)), w(f4, f4)), w(f3, f3)), w(f2, f2)), w(f1, f1)),
|
||||
w(fh, fh));
|
||||
let sq_term = w(sq_sum, 0xea_u32);
|
||||
|
||||
// Cross terms
|
||||
let cross1 = w(w(f8, w(j, 0x1a82aada_u32)), 2); // iVar8 * j * 0x1a82aada * 2
|
||||
let cross2 = w(w(f7, f17), 2); // iVar7 * iVar17 * 2
|
||||
let cross3 = w(wa(w(wa(wa(raw, f8), f7), f9), w(raw, f8)), 0xd4); // ((raw+f8+f7)*f9 + raw*f8)*0xd4
|
||||
let cross4 = w(w(f6, f15), 2); // iVar6 * iVar15 * 2
|
||||
let cross5 = w(w(f12, f5), 2); // iVar12 * iVar5 * 2
|
||||
let cross6 = w(w(f11, f4), 2); // iVar11 * iVar4 * 2
|
||||
let cross7 = w(w(wa(w(st, 0x4e62c316_u32), f12), f3), 2); // (st*0x4e62c316 + f14) * f3 * 2
|
||||
let cross8 = w(w(f2, wa(wa(w(st, 0x4e62c316_u32), f12), f13)), 2); // f2*(st*0x4e62c316+f12+f13)*2
|
||||
let cross9 = w(w(wa(wa(wa(w(st, 0x3439c24c_u32), f11), f13), w(mix, 0x13527870_u32)), f1), 2);
|
||||
let cross10 = w(w(f10, fh), 2); // iVar10 * iVar23 * 2
|
||||
let cross11 = w(w(l, wa(f10, w(h, 0x013b9852_u32))), 0x98); // l*(iVar10+h*0x13b9852)*0x98
|
||||
|
||||
// Quadratic self-term: (l * (-0x50386060) + 0x7c) * l
|
||||
let l_quad = w(wa(w(l, 0xafc79fa0_u32), 0x7c), l);
|
||||
|
||||
// Final assembly
|
||||
let mut result: u32 = 0x455ae97d_u32; // constant base
|
||||
result = wa(result, w(j, 0xa211f9f5_u32)); // j * (-0x5dee060b)
|
||||
result = wa(result, w(st, 0x84a187c3_u32)); // st * (-0x7b5e783d)
|
||||
result = wa(result, l_quad);
|
||||
result = wa(result, w(mix, 0xa1a9cfef_u32)); // mix * (-0x5e563011)
|
||||
result = wa(result, w(wa(h, nn), 0x5e563111_u32)); // (h + nn) * 0x5e563111
|
||||
result = wa(result, sq_term);
|
||||
result = wa(result, w(nshl12, 0x336bfe1c_u32));
|
||||
result = wa(result, w(raw, 0x8fd1523d_u32)); // raw * (-0x702eadc3)
|
||||
result = wa(result, w(k, 0x88700dfa_u32)); // k * (-0x778ff206)
|
||||
result = wa(result, cross1);
|
||||
result = wa(result, cross2);
|
||||
result = wa(result, cross3);
|
||||
result = wa(result, cross4);
|
||||
result = wa(result, w(g, 0x1a7751a2_u32));
|
||||
result = wa(result, w(l, 0x3d99e8a0_u32));
|
||||
result = wa(result, cross5);
|
||||
result = wa(result, cross6);
|
||||
result = wa(result, cross7);
|
||||
result = wa(result, cross8);
|
||||
result = wa(result, cross9);
|
||||
result = wa(result, cross10);
|
||||
result = wa(result, cross11);
|
||||
|
||||
result as u8 // low byte = decrypted output
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
/// Decrypt 33 bytes from the encrypted payload in `d`.
|
||||
///
|
||||
/// Returns (key_material[32], control_byte).
|
||||
///
|
||||
/// `data`: the full decoded d-field bytes (part 1, typically 203 bytes)
|
||||
/// `skip`: number of bytes to skip before the 33-byte encrypted window
|
||||
pub fn feistel_decrypt(data: &[u8], skip: usize) -> ([u8; 32], u8) {
|
||||
let mut feistel = FeistelState::new();
|
||||
let mut key_material = [0u8; 32];
|
||||
let mut control = 0u8;
|
||||
|
||||
for i in 0..33 {
|
||||
let raw = data[skip + i];
|
||||
let decrypted = decrypt_byte(raw, i as u32, &mut feistel);
|
||||
if i < 32 {
|
||||
key_material[i] = decrypted;
|
||||
} else {
|
||||
control = decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
(key_material, control)
|
||||
}
|
||||
@@ -134,11 +134,13 @@ pub fn interleave_key_half(input: &[u32], output: &mut [u32]) {
|
||||
/// Reverse of pack_input: apply masks in reverse order (0x0F -> 0x33 -> 0x55).
|
||||
pub fn unpack_output(s: &[u32; 8], final_rk: &[u32; 8]) -> [u32; 8] {
|
||||
// XOR with final round keys first
|
||||
// Mapping from pack_input output indices:
|
||||
// out7=a, out3=e, out5=c, out1=g, out6=b, out2=f, out4=d, out0=h
|
||||
let mut a = s[7] ^ final_rk[7];
|
||||
let mut b = s[6] ^ final_rk[6];
|
||||
let mut c = s[5] ^ final_rk[5];
|
||||
let mut d = s[3] ^ final_rk[3];
|
||||
let mut e = s[4] ^ final_rk[4];
|
||||
let mut d = s[4] ^ final_rk[4]; // out4 = d part
|
||||
let mut e = s[3] ^ final_rk[3]; // out3 = e part
|
||||
let mut f = s[2] ^ final_rk[2];
|
||||
let mut g = s[1] ^ final_rk[1];
|
||||
let mut h = s[0] ^ final_rk[0];
|
||||
|
||||
@@ -4,3 +4,4 @@ pub mod sbox;
|
||||
pub mod gift256;
|
||||
pub mod hash;
|
||||
pub mod solver;
|
||||
pub mod feistel;
|
||||
|
||||
101
src/main.rs
101
src/main.rs
@@ -1,5 +1,5 @@
|
||||
//! hCaptcha PoW solver CLI
|
||||
//! Parses JWT challenge -> calls solver -> outputs base64 nonce
|
||||
//! Parses JWT challenge -> calls solver -> outputs base64 proof
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use serde::Deserialize;
|
||||
@@ -10,6 +10,7 @@ mod gift256;
|
||||
mod hash;
|
||||
mod solver;
|
||||
mod util;
|
||||
mod feistel;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
@@ -54,56 +55,78 @@ fn main() {
|
||||
println!("Expiration: {}", payload.e);
|
||||
|
||||
// 3. Decode challenge data from `d` field
|
||||
let challenge_data = STANDARD.decode(&payload.d).unwrap_or_else(|_| {
|
||||
// Try with padding adjustment
|
||||
let padded = format!("{}==", payload.d.trim_end_matches('='));
|
||||
STANDARD.decode(&padded).expect("Invalid base64 in challenge data")
|
||||
});
|
||||
// The `d` field is two concatenated base64 strings:
|
||||
// encrypted_payload (with = padding) + seed (no padding)
|
||||
// Split at the padding boundary and decode each part separately.
|
||||
let challenge_data = if let Some(eq_pos) = payload.d.find('=') {
|
||||
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
|
||||
let part1 = &payload.d[..split_pos];
|
||||
let part2 = &payload.d[split_pos..];
|
||||
|
||||
println!("Challenge data: {} bytes", challenge_data.len());
|
||||
let mut data = STANDARD.decode(part1).expect("Invalid base64 in challenge part1");
|
||||
if !part2.is_empty() {
|
||||
let p2 = STANDARD.decode(part2).unwrap_or_else(|_| {
|
||||
// part2 may lack padding — add it
|
||||
let padded = format!("{}{}", part2, "=".repeat((4 - part2.len() % 4) % 4));
|
||||
STANDARD.decode(&padded).expect("Invalid base64 in challenge part2")
|
||||
});
|
||||
data.extend_from_slice(&p2);
|
||||
}
|
||||
data
|
||||
} else {
|
||||
STANDARD.decode(&payload.d).expect("Invalid base64 in challenge data")
|
||||
};
|
||||
|
||||
// 4. Parse challenge (needs at least 49 bytes: 32 key + 16 target + 1 extra)
|
||||
if challenge_data.len() < 49 {
|
||||
eprintln!("Challenge data too short: {} bytes (need >= 49)", challenge_data.len());
|
||||
// Split challenge_data into encrypted_payload (part1) and seed_bytes (part2)
|
||||
// Part1 = first decode (203 bytes), Part2 = second decode (12 bytes)
|
||||
let (encrypted_payload, seed_bytes) = if let Some(eq_pos) = payload.d.find('=') {
|
||||
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
|
||||
let part1_len = STANDARD.decode(&payload.d[..split_pos])
|
||||
.expect("Invalid base64 in part1").len();
|
||||
(&challenge_data[..part1_len], &challenge_data[part1_len..])
|
||||
} else {
|
||||
(challenge_data.as_slice(), &[] as &[u8])
|
||||
};
|
||||
|
||||
println!("Encrypted payload: {} bytes, Seed: {} bytes", encrypted_payload.len(), seed_bytes.len());
|
||||
|
||||
// 4. Feistel decrypt 33 bytes → 32 byte key + 1 byte control
|
||||
if encrypted_payload.len() < 33 {
|
||||
eprintln!("Encrypted payload too short: {} bytes (need >= 33)", encrypted_payload.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut key_material = [0u8; 32];
|
||||
key_material.copy_from_slice(&challenge_data[0..32]);
|
||||
let (key_material, control) = feistel::feistel_decrypt(encrypted_payload, 0);
|
||||
println!("Feistel decrypted: key[0..4]={:02x}{:02x}{:02x}{:02x}, control=0x{:02x}",
|
||||
key_material[0], key_material[1], key_material[2], key_material[3], control);
|
||||
|
||||
let mut target_hash = [0u8; 16];
|
||||
target_hash.copy_from_slice(&challenge_data[32..48]);
|
||||
|
||||
let extra_byte = challenge_data[48];
|
||||
let difficulty = if payload.c > 0 { payload.c } else { 1000 };
|
||||
|
||||
let challenge = solver::Challenge {
|
||||
key_material,
|
||||
target_hash,
|
||||
extra_byte,
|
||||
difficulty,
|
||||
};
|
||||
|
||||
// 5. Solve
|
||||
let seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
// 5. Derive PCG seed from seed_bytes (part2 of d)
|
||||
let seed = if seed_bytes.len() >= 8 {
|
||||
u64::from_le_bytes([
|
||||
seed_bytes[0], seed_bytes[1], seed_bytes[2], seed_bytes[3],
|
||||
seed_bytes[4], seed_bytes[5], seed_bytes[6], seed_bytes[7],
|
||||
])
|
||||
} else {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64
|
||||
};
|
||||
|
||||
let max_iter = if payload.c > 0 { payload.c } else { 1_000_000 };
|
||||
println!("PCG seed: 0x{:016x}", seed);
|
||||
println!("Running {} iterations...", difficulty);
|
||||
|
||||
println!("Solving with max {} iterations...", max_iter);
|
||||
let proof = solver::solve(&challenge, seed);
|
||||
|
||||
let solution = solver::solve(&challenge, max_iter, seed);
|
||||
|
||||
// 6. Output
|
||||
match solution {
|
||||
Some(sol) => {
|
||||
let nonce_b64 = STANDARD.encode(sol.nonce);
|
||||
println!("Found solution in {} iterations", sol.iterations);
|
||||
println!("n={}", nonce_b64);
|
||||
}
|
||||
None => {
|
||||
eprintln!("No solution found within {} iterations", max_iter);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
// 6. Output base64-encoded proof
|
||||
let proof_b64 = STANDARD.encode(&proof);
|
||||
println!("Proof size: {} bytes ({} digests)", proof.len(), proof.len() / 16);
|
||||
println!("n={}", proof_b64);
|
||||
}
|
||||
|
||||
@@ -1,97 +1,75 @@
|
||||
//! Top-level PoW solver
|
||||
//! Corresponds to pow_main_dispatch (Yb) solve path (0xABAB270C)
|
||||
//!
|
||||
//! The `n` value is NOT a single nonce — it's an accumulated Vec<u8> of
|
||||
//! 16-byte hash digests, one per iteration. The full output is base64-encoded
|
||||
//! and typically 20-40KB (1250-2500 iterations × 16 bytes).
|
||||
|
||||
use crate::pcg::PcgRng;
|
||||
use crate::sbox::apply_polynomial_sbox;
|
||||
use crate::gift256;
|
||||
use crate::hash;
|
||||
|
||||
/// PoW solution result
|
||||
pub struct PowSolution {
|
||||
pub nonce: [u8; 12],
|
||||
pub iterations: u32,
|
||||
}
|
||||
|
||||
/// Challenge data parsed from JWT `d` field
|
||||
pub struct Challenge {
|
||||
pub key_material: [u8; 32], // 32-byte key material (before S-Box)
|
||||
pub target_hash: [u8; 16], // 16-byte target hash
|
||||
pub extra_byte: u8, // 33rd byte
|
||||
pub difficulty: u32, // iteration count (c field from JWT)
|
||||
}
|
||||
|
||||
/// Main solve function.
|
||||
/// Solve the PoW challenge by accumulating digests.
|
||||
///
|
||||
/// Full flow:
|
||||
/// Returns a Vec<u8> of concatenated 16-byte digests (one per iteration).
|
||||
/// This is base64-encoded to produce the `n` value sent back to hCaptcha.
|
||||
///
|
||||
/// Full per-iteration flow (0xABAB270C path):
|
||||
/// 1. PCG generates 12-byte nonce
|
||||
/// 2. S-Box polynomial substitution (32-byte key material)
|
||||
/// 3. GIFT-256 key schedule -> 480 bytes round keys
|
||||
/// 4. MMO compression
|
||||
/// 5. Assemble hash input: [nonce_u32_0, nonce_u32_1, nonce_u32_2, 0x01000000]
|
||||
/// 6. GIFT-256 encrypt + hash_finalize -> 128-bit digest
|
||||
/// 7. Constant-time 16-byte comparison
|
||||
/// 8. Match -> return nonce; no match -> regenerate nonce
|
||||
pub fn solve(challenge: &Challenge, max_iterations: u32, seed: u64) -> Option<PowSolution> {
|
||||
/// 2. S-Box polynomial substitution on key material
|
||||
/// 3. GIFT-256 key schedule → 120 u32 round keys
|
||||
/// 4. MMO compression → chaining value
|
||||
/// 5. GIFT-256 encrypt (nonce as plaintext input)
|
||||
/// 6. hash_finalize → 16-byte digest
|
||||
/// 7. Append digest to output accumulator
|
||||
/// 8. Loop back to step 1 with next PCG nonce
|
||||
pub fn solve(challenge: &Challenge, seed: u64) -> Vec<u8> {
|
||||
let mut rng = PcgRng::new(seed);
|
||||
let iterations = challenge.difficulty;
|
||||
let mut output = Vec::with_capacity(iterations as usize * 16);
|
||||
|
||||
// Pre-compute key schedule and MMO (these don't depend on the nonce)
|
||||
// Pre-compute key schedule and MMO (invariant across iterations)
|
||||
let mut key_data = challenge.key_material;
|
||||
apply_polynomial_sbox(&mut key_data);
|
||||
|
||||
let round_keys = gift256::key_schedule::key_schedule(&key_data);
|
||||
let mmo_state = hash::mmo::mmo_compress(&round_keys);
|
||||
|
||||
// Initialize hash state from MMO output
|
||||
// The hash state is derived from the round keys + chaining value
|
||||
let base_state = [0u32; 8];
|
||||
// State initialized to zeros, will be populated by the hash process
|
||||
|
||||
for iter in 0..max_iterations {
|
||||
for _iter in 0..iterations {
|
||||
// 1. Generate 12-byte nonce
|
||||
let nonce = rng.generate_nonce();
|
||||
|
||||
// 2. Assemble hash input block
|
||||
// 2. Assemble hash input block from nonce
|
||||
let nonce_u32_0 = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
|
||||
let nonce_u32_1 = u32::from_le_bytes([nonce[4], nonce[5], nonce[6], nonce[7]]);
|
||||
let nonce_u32_2 = u32::from_le_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]);
|
||||
|
||||
let hash_input = [nonce_u32_0, nonce_u32_1, nonce_u32_2, 0x01000000u32];
|
||||
|
||||
// 3. GIFT-256 encrypt
|
||||
// 3. GIFT-256 encrypt (nonce words + padding as plaintext)
|
||||
let encrypted = gift256::encrypt::encrypt(
|
||||
&[hash_input[0], hash_input[1], hash_input[2], hash_input[3], 0, 0, 0, 0],
|
||||
&[nonce_u32_0, nonce_u32_1, nonce_u32_2, 0x01000000, 0, 0, 0, 0],
|
||||
&round_keys,
|
||||
);
|
||||
|
||||
// 4. Finalize hash
|
||||
// Build state from encrypted output
|
||||
let mut hash_state = base_state;
|
||||
for i in 0..8 {
|
||||
hash_state[i] = encrypted[i];
|
||||
}
|
||||
// 4. Build hash state from encrypted output
|
||||
let hash_state = encrypted;
|
||||
|
||||
// 5. Finalize hash → 16-byte digest
|
||||
let digest = hash::finalize::finalize(
|
||||
&hash_state,
|
||||
&mmo_state.chaining,
|
||||
&nonce,
|
||||
);
|
||||
|
||||
// 5. Compare with target
|
||||
if constant_time_eq(&digest, &challenge.target_hash) {
|
||||
return Some(PowSolution {
|
||||
nonce,
|
||||
iterations: iter + 1,
|
||||
});
|
||||
}
|
||||
// 6. Append 16-byte digest to output accumulator
|
||||
output.extend_from_slice(&digest);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Constant-time comparison (16 bytes)
|
||||
fn constant_time_eq(a: &[u8; 16], b: &[u8; 16]) -> bool {
|
||||
let mut result = 0u8;
|
||||
for i in 0..16 {
|
||||
result |= a[i] ^ b[i];
|
||||
}
|
||||
result == 0
|
||||
output
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user