Written by virchau13
It's kinda like doing your laundry, if you think about it hard enough.
nc challs.watctf.org 2333
Attachment:
use rand::{seq::SliceRandom, Rng};
use rustc_hash::FxBuildHasher;
use std::{hash::{BuildHasher, Hash}, io::{self, BufRead, Read, Write}};
use simplehash::fnv1a_64;
fn do_hash(x: i32) -> u64 {
fnv1a_64(&x.to_le_bytes())
}
const HASH_SIZE: usize = 32;
fn test_perm_restricted_memory(perm: &[i32]) -> bool {
let mut map = [-1i32; HASH_SIZE];
for x in perm {
let idx = (do_hash(*x) % HASH_SIZE as u64) as usize;
if map[idx] == *x {
// duplicate, fail
return false;
} else {
// not a duplicate, continue
}
map[idx] = *x;
}
true
}
fn parse_perm(inp: &str) -> Result<Vec<i32>, &'static str> {
let ans: Vec<i32> = inp.trim().split(' ').map(|x| x.parse::<u32>().unwrap() as i32).collect();
if ans.len() != 256 {
return Err("permutation is not of length 256");
}
for x in &ans {
if *x < 0 || 256 <= *x {
return Err("permutation element out of range");
}
}
if !test_perm_restricted_memory(&ans) {
return Err("permutation has non-unique elements");
}
Ok(ans)
}
fn compose_perms(p1: &[i32], p2: &[i32]) -> Vec<i32> {
p1.iter().map(|i| p2[*i as usize]).collect()
}
fn main() {
let mut rng = rand::rng();
let mut cipherperm: Vec<i32> = (0..256).collect();
cipherperm.shuffle(&mut rng);
let stdin = io::stdin();
let mut line_iter = stdin.lock().lines();
let mut get_line = || -> String {
line_iter.next().unwrap().unwrap()
};
println!("Welcome to the Permutation Oracle.");
loop {
println!("Main Menu");
println!("1. Give the oracle a permutation");
println!("2. Guess the secret permutation");
print!("Enter your choice: ");
io::stdout().flush().unwrap();
let choice = get_line();
let choice: u32 = choice.trim().parse().unwrap();
if choice == 1 {
print!("Enter the permutation seperated by spaces: ");
io::stdout().flush().unwrap();
let perm_str = get_line();
let perm = match parse_perm(&perm_str) {
Err(e) => {
println!("Error: {e}");
continue;
},
Ok(v) => v,
};
let res = compose_perms(&perm, &cipherperm);
let i = rng.random_range(0..res.len());
println!("The oracle has divined... {}", res[i]);
} else if choice == 2 {
print!("Enter the permutation seperated by spaces: ");
io::stdout().flush().unwrap();
let perm_str = get_line();
let perm = match parse_perm(&perm_str) {
Err(e) => {
println!("Error: {e}");
continue;
},
Ok(v) => v,
};
if perm == cipherperm {
println!("Good job! Here's your reward: {}", std::env::var("FLAG").unwrap());
} else {
println!("Unfortunately, wrong :/");
}
}
}
}
We can send a permutation of 0..256
to the server, then the server combines the permutation with the oracle and responds with a random element. However, the validation is buggy:
fn test_perm_restricted_memory(perm: &[i32]) -> bool {
let mut map = [-1i32; HASH_SIZE];
for x in perm {
let idx = (do_hash(*x) % HASH_SIZE as u64) as usize;
if map[idx] == *x {
// duplicate, fail
return false;
} else {
// not a duplicate, continue
}
map[idx] = *x;
}
true
}
If we send two values that map to the same bucket in turns, it does not report duplication. So, firstly we need to find the values that belong to the same bucket:
use rand::{seq::SliceRandom, Rng};
use rustc_hash::FxBuildHasher;
use simplehash::fnv1a_64;
use std::{
hash::{BuildHasher, Hash},
io::{self, BufRead, Read, Write},
};
fn do_hash(x: i32) -> u64 {
fnv1a_64(&x.to_le_bytes())
}
const HASH_SIZE: usize = 32;
fn main() {
let mut buckets = vec![vec![]; HASH_SIZE];
for i in 0..256 {
let idx = (do_hash(i) % HASH_SIZE as u64) as usize;
buckets[idx].push(i);
}
for i in 0..HASH_SIZE {
println!("{}: {:?}", i, buckets[i]);
}
}
Then, we can generate a sequence of e1, e2, e1, e2, ...
to get either oracle[e1]
or oracle[e2]
. Then, we can use e1, e3, e1, e3, ...
to get either oracle[e1]
or oracle[e3]
. The intersection will be oracle[e2]
. Continue the process until we find all elements.
Attack script:
from pwn import *
context(log_level="debug")
buckets = {
0: [5, 37, 69, 101, 133, 165, 197, 229],
1: [20, 52, 84, 116, 148, 180, 212, 244],
2: [7, 39, 71, 103, 135, 167, 199, 231],
3: [22, 54, 86, 118, 150, 182, 214, 246],
4: [1, 33, 65, 97, 129, 161, 193, 225],
5: [16, 48, 80, 112, 144, 176, 208, 240],
6: [3, 35, 67, 99, 131, 163, 195, 227],
7: [18, 50, 82, 114, 146, 178, 210, 242],
8: [13, 45, 77, 109, 141, 173, 205, 237],
9: [28, 60, 92, 124, 156, 188, 220, 252],
10: [15, 47, 79, 111, 143, 175, 207, 239],
11: [30, 62, 94, 126, 158, 190, 222, 254],
12: [9, 41, 73, 105, 137, 169, 201, 233],
13: [24, 56, 88, 120, 152, 184, 216, 248],
14: [11, 43, 75, 107, 139, 171, 203, 235],
15: [26, 58, 90, 122, 154, 186, 218, 250],
16: [21, 53, 85, 117, 149, 181, 213, 245],
17: [4, 36, 68, 100, 132, 164, 196, 228],
18: [23, 55, 87, 119, 151, 183, 215, 247],
19: [6, 38, 70, 102, 134, 166, 198, 230],
20: [17, 49, 81, 113, 145, 177, 209, 241],
21: [0, 32, 64, 96, 128, 160, 192, 224],
22: [19, 51, 83, 115, 147, 179, 211, 243],
23: [2, 34, 66, 98, 130, 162, 194, 226],
24: [29, 61, 93, 125, 157, 189, 221, 253],
25: [12, 44, 76, 108, 140, 172, 204, 236],
26: [31, 63, 95, 127, 159, 191, 223, 255],
27: [14, 46, 78, 110, 142, 174, 206, 238],
28: [25, 57, 89, 121, 153, 185, 217, 249],
29: [8, 40, 72, 104, 136, 168, 200, 232],
30: [27, 59, 91, 123, 155, 187, 219, 251],
31: [10, 42, 74, 106, 138, 170, 202, 234],
}
oracle = [0] * 256
p = remote("challs.watctf.org", 2333)
# p = process("./target/debug/permpress")
for i in buckets:
bucket = buckets[i]
# alternate between pairs
for j in range(0, len(bucket), 2):
e1 = bucket[j]
e2 = bucket[j + 1]
values = set()
while len(values) < 2:
p.recvuntil(b"Enter your choice:")
p.sendline(b"1")
p.recvuntil(b"spaces: ")
p.sendline(" ".join([str(e1), str(e2)] * 128).encode())
result = p.recvline()
value = int(result.split()[-1])
values.add(value)
print(value)
# we know that {oracle[e1], oracle[e2]} == values
# but we don't know the order
e3 = bucket[(j + 2) % len(bucket)]
values2 = set()
while len(values2) < 2:
p.recvuntil(b"Enter your choice:")
p.sendline(b"1")
p.recvuntil(b"spaces: ")
p.sendline(" ".join([str(e1), str(e3)] * 128).encode())
result = p.recvline()
value = int(result.split()[-1])
values2.add(value)
# the intersection of values and values2 is oracle[e1]
oracle[e1] = (values & values2).pop()
oracle[e2] = (values - set([oracle[e1]])).pop()
print(oracle)
p.recvuntil(b"Enter your choice:")
p.sendline(b"2")
p.recvuntil(b"spaces: ")
p.sendline(" ".join([str(e) for e in oracle]).encode())
p.interactive()
Flag: watctf{1nd1v1du4l_p3rm5_l0v3_uniqueness}
.