fluffy - GoogleCTF 2025
#GoogleCTF_2025 #Flutter #Android
token, pinでsecretを暗号化できるAndroidアプリ
初期状態でsecretが3つあり、これらにflagが保存されていそう
https://github.com/worawit/blutter でディスアセンブルした
macOSでは動かず、Linuxでは動いた
生成されたfridaのスクリプトを使うと動的解析ができて便利
LLMに生成されたアセンブリにコメントを書いてもらい、その後dartのコードに変換してもらうと読みやすい
tokenは以下のように生成されていた
code:python
import time
import hashlib
def base62_encode(data: bytes) -> str:
_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
_BASE = 62
if not data:
return ""
number = int.from_bytes(data, 'big')
if number == 0:
return _ALPHABET0
result_chars = []
while number > 0:
number, remainder = divmod(number, _BASE)
result_chars.append(_ALPHABETremainder)
return "".join(reversed(result_chars))
def generate_token() -> str:
tick = int(time.time())
digest = hashlib.sha1(f"gctf25_{tick}".encode()).digest()
return base62_encode(digest:8)
print(generate_token())
暗号化の処理は以下
code:python
def encrypt(pin: int, token_str: str, secret_str: str) -> str:
token = base62_decode(token_str)
secret = bytearray(secret_str.encode())
for round in range(pin):
new_secret = []
for i in range(len(secret)):
v1 = (secreti + tokeni % len(token)) & 0xff
new_secret.append(_rotl8(v1, i & 7))
secret = [new_secret-1]
secret.extend(new_secret:-1)
new_token = list(token1:)
new_token.append(token0)
for i in range(len(new_token)):
v1 = ((round & 3) + 1) & 7
v2 = (pin ^ v1) & 7
new_tokeni = _rotr8(new_tokeni, v2)
token = new_token
return base62_encode(bytes(secret))
これを元に復号コードを書いた
code:python
def decrypt(pin: int, token_str: str, enc_str: str) -> str:
init_token = list(base62_decode(token_str))
token_states: list[listint] = []
token = init_token
for rnd in range(pin):
token_states.append(token.copy())
tmp = token1: + token:1
v1 = ((rnd & 3) + 1) & 7
v2 = (pin ^ v1) & 7
token = _rotr8(t, v2) for t in tmp
secret = list(base62_decode(enc_str))
for rnd in range(pin - 1, -1, -1):
token = token_statesrnd
tmp = secret1: + secret:1
prev = []
for i, b in enumerate(tmp):
x = _rotr8(b, i & 7)
prev.append((x - tokeni % len(token)) & 0xFF)
secret = prev
return bytes(secret).decode()
pinとtoken(作成日時からある程度予測できる)が不明なので、これらをbruteforceする必要がある
Rustのコードに変換してもらい、した
code:rust
// -----------------------------------------------------------------------------
// Rust brute‑force tool – BigInt‑based Base‑62 (no external base62 crate)
//
// Cargo.toml:
// sha1 = "0.10"
// rayon = "1.10"
// chrono = "0.4"
// num-bigint = "0.4"
// num-traits = "0.2"
// num-integer = "0.1"
// -----------------------------------------------------------------------------
use chrono::{FixedOffset, NaiveDate, TimeZone};
use num_bigint::BigUint;
use num_integer::Integer;
use num_traits::{FromPrimitive, ToPrimitive, Zero};
use rayon::prelude::*;
use sha1::{Digest, Sha1}; // for div_rem
const ALPHABET: &u8; 62 = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const BASE: u32 = 62;
// ------------------------ BigInt Base‑62 ----------------------------------
#inline
fn base62_encode(data: &u8) -> String {
if data.is_empty() {
return String::new();
}
let mut num = BigUint::from_bytes_be(data);
if num.is_zero() {
return "0".to_string();
}
let base = BigUint::from_u32(BASE).unwrap();
let mut buf = Vec::new();
while !num.is_zero() {
let (q, r) = num.div_rem(&base);
buf.push(ALPHABETr.to_usize().unwrap() as char);
num = q;
}
buf.iter().rev().collect()
}
#inline
fn base62_decode(s: &str) -> Vec<u8> {
if s.is_empty() {
return Vec::new();
}
let base = BigUint::from_u32(BASE).unwrap();
let mut num = BigUint::zero();
for &b in s.as_bytes() {
let val = ALPHABET
.iter()
.position(|&c| c == b)
.expect("invalid base62 char") as u32;
num = num * &base + BigUint::from_u32(val).unwrap();
}
num.to_bytes_be()
}
// -------------------------- Helpers ---------------------------------------
#inline
fn rotl8(v: u8, n: u8) -> u8 {
((v << n) | (v >> (8 - n))) & 0xFF
}
#inline
fn rotr8(v: u8, n: u8) -> u8 {
((v >> n) | (v << (8 - n))) & 0xFF
}
fn generate_token(tick: i64) -> String {
let digest = Sha1::digest(format!("gctf25_{tick}").as_bytes());
base62_encode(&digest..8)
}
// ----------------------- Cipher core --------------------------------------
fn encrypt(pin: u16, token_str: &str, secret_str: &str) -> String {
let mut token: Vec<u8> = base62_decode(token_str);
let mut secret: Vec<u8> = secret_str.as_bytes().to_vec();
for round in 0..pin {
// --- secret transformation ---
let mut new_secret = Vec::with_capacity(secret.len());
for (i, &b) in secret.iter().enumerate() {
let v1 = b.wrapping_add(tokeni % token.len());
new_secret.push(rotl8(v1, (i & 7) as u8));
}
secret.clear();
secret.push(*new_secret.last().unwrap());
secret.extend_from_slice(&new_secret..new_secret.len() - 1);
// --- token transformation ---
let mut new_token = token1...to_vec();
new_token.push(token0);
let v1 = ((round & 3) + 1) & 7;
let v2 = (pin ^ v1) & 7;
for t in &mut new_token {
*t = rotr8(*t, v2 as u8);
}
token = new_token;
}
base62_encode(&secret)
}
fn decrypt(pin: u16, token_str: &str, enc_str: &str) -> Option<String> {
let init_token = base62_decode(token_str);
let mut token_states: Vec<Vec<u8>> = Vec::with_capacity(pin as usize);
// forward pass – capture token states
let mut token = init_token.clone();
for rnd in 0..pin {
token_states.push(token.clone());
let mut tmp = token1...to_vec();
tmp.push(token0);
let v1 = ((rnd & 3) + 1) & 7;
let v2 = (pin ^ v1) & 7;
token = tmp.into_iter().map(|t| rotr8(t, v2 as u8)).collect();
}
let mut secret = base62_decode(enc_str);
// reverse pass
for rnd in (0..pin).rev() {
let token = &token_statesrnd as usize;
let mut tmp = secret1...to_vec();
tmp.push(secret0);
let mut prev = Vec::with_capacity(tmp.len());
for (i, b) in tmp.into_iter().enumerate() {
let x = rotr8(b, (i & 7) as u8);
prev.push(x.wrapping_sub(tokeni % token.len()));
}
secret = prev;
}
String::from_utf8(secret).ok()
}
// ------------------------- Brute‑force ------------------------------------
#derive(Debug)
struct SecretEntry {
time: (i32, i32, i32, i32, i32), // Y,M,D,h,m (sec is brute‑forced)
ciphertext: &'static str,
}
fn unix_time(t: (i32, i32, i32, i32, i32), sec: u32) -> i64 {
NaiveDate::from_ymd_opt(t.0, t.1 as u32, t.2 as u32)
.unwrap()
.and_hms_opt(t.3 as u32, t.4 as u32, sec)
.unwrap()
.and_utc()
.timestamp()
}
fn crack(entry: &SecretEntry) {
println!("=== ciphertext: {} ===", entry.ciphertext);
(0u32..60).into_par_iter().for_each(|sec| {
println!("sec: {sec}");
let tick = unix_time(entry.time, sec);
let token = generate_token(tick);
(0u16..=9999u16).into_par_iter().for_each(|pin| {
if let Some(plain) = decrypt(pin, &token, entry.ciphertext) {
println!("{sec:02} {pin:04} {} -> {plain}", entry.ciphertext);
}
});
});
}
fn main() {
let secrets = [
SecretEntry {
time: (2023, 8, 4, 13, 37),
ciphertext: "fmMf7mIMbHcPoQmLGx1CO0XVGBmhjTaYhB0",
},
SecretEntry {
time: (2024, 9, 7, 18, 52),
ciphertext: "506WRgCajs3QSTyohnu2hldds18mjkx",
},
SecretEntry {
time: (2025, 3, 3, 22, 7),
ciphertext: "fgv99dOvazsvEESh7DPKbb3k0I3RW",
},
];
for secret in &secrets {
crack(secret);
}
}