<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Khavid</title><description>Khavid&apos;s blog</description><link>https://khavidbap.github.io/</link><language>en</language><item><title>(EN) PatriotCTF 2025 - Reverse</title><link>https://khavidbap.github.io/posts/patriot2025-rev/</link><guid isPermaLink="true">https://khavidbap.github.io/posts/patriot2025-rev/</guid><description>Writeup for all Reverse Engineering challenge in PatriotCTF 2025 by VGU_Cypher</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Space Pirates&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;...

#define FLAG_LEN 30
const uint8_t TARGET[FLAG_LEN] = {...};

// The pirate&apos;s rotating XOR key
const uint8_t XOR_KEY[5] = {0x42, 0x73, 0x21, 0x69, 0x37};

// The magic addition constant
const uint8_t MAGIC_ADD = 0x2A;
// PCTF{0x_M4rks_tH3_sp0t_M4t3y}

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yea, they wrote the right flag inside the file, how hilarious it is.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[pctf{0x_M4rks_tH3_sp0t_M4t3y}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Are You Pylingual?&lt;/h1&gt;
&lt;p&gt;We&apos;re giving &lt;code&gt;.pyc&lt;/code&gt; file. Decompile it and we have this code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pyfiglet
file = open(&apos;flag.txt&apos;, &apos;r&apos;)
flag = file.read()
font = &apos;slant&apos;
words = &apos;MASONCC IS THE BEST CLUB EVER&apos;
flag_track = 0
art = list(pyfiglet.figlet_format(words, font=font))
i = len(art) % 10
for ind in range(len(art)):
    if ind == i and flag_track &amp;lt; len(flag):
        art[ind] = flag[flag_track]
        i += 28
        flag_track += 1
art_str = &apos;&apos;.join(art)
first_val = 5
second_val = 6
first_half = art_str[:len(art_str) // 2]
second_half = art_str[len(art_str) // 2:]
first = [~ord(char) ^ first_val for char in first_half]
second = [~ord(char) ^ second_val for char in second_half]
output = second + first
print(output)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basically, the generator split the figlet output &lt;code&gt;art_str&lt;/code&gt; into 2 parts, &lt;code&gt;first_half&lt;/code&gt; (&lt;code&gt;floor(L / 2)&lt;/code&gt;) and &lt;code&gt;second_half&lt;/code&gt;. It encoded as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;first = [ord(char) ^ first_val for char in first_half]
second = [ord(char) ^ second_val for char in second_half]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then printed &lt;code&gt;output = second + first&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So to decode this, we need to split the provided &lt;code&gt;output&lt;/code&gt; list into &lt;code&gt;second&lt;/code&gt; (length = &lt;code&gt;ceil(len(output) / 2)&lt;/code&gt;) and &lt;code&gt;first&lt;/code&gt; (length = &lt;code&gt;floor(len(output) / 2)&lt;/code&gt;). Then, recover bytes with &lt;code&gt;ord = (~(v ^ val)) &amp;amp; 0xFF&lt;/code&gt; and &lt;code&gt;chr(ord)&lt;/code&gt; to print it out.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pyfiglet import figlet_format

output = [...]
first_val = 5
second_val = 6

L = len(output)
first_len = L // 2         # = floor(L / 2)
second_len = L - first_len # = ceil(L / 2)
second_enc = output[:second_len]
first_enc  = output[second_len:]

def decode_list(enc_list, val):
    chars = []
    for v in enc_list:
        o = (~(v ^ val)) &amp;amp; 0xFF
        chars.append(chr(o))
    return &apos;&apos;.join(chars)

first_half = decode_list(first_enc, first_val)
second_half = decode_list(second_enc, second_val)
art_str = first_half + second_half
print(art_str[:1000])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this and it would print out the ASCII art with some characters from the flag.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  p __  ______   _____ ____  _c  ______________   ________t
   /  |/  /   | / ___// __f\/ | / / ____/ ____/  /  _/{___/
  / /|_/ / /| | \__ \/o/ / /  |/ / /   / /       /b/ \__ \
 / /  / / ___ |___F / /_/ / /|  / /___/ /___  u_/ / ___/ /
/_/  /_/_/  |_s____/\____/_/ |_/\____/\___c/  /___//____/
          4                           t
  ____i___  ________   ____  _____0___________   ________    _n  ______
 /_  __/ / / / ____/  / __ )/ ____/ ___/_  __i  / ____/ /   / / / / __ )
5 / / / /_/ / __/    / __  /n__/  \__ \ / /    / /   / /&apos;  / / / / __  |
 / / / __  t /___   / /_/ / /___ ___/ /_ /    / /___/ /___/ /_/ / /E/ /
/_/ /_/ /_/_____/  /__n__/_____//____//_/     \___c/_____/\____/_____/
     R                           y                           p
    _______    _t__________
   / ____/ |  /1/ ____/ __ \
  / __/  | | /o/ __/ / /_/ /
 / /___  | |/N/ /___/ _, _/
/_____/  |__}/_____/_/ |_|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[pctf{obFusc4ti0n_i5n&apos;t_EncRypt1oN}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Space Pirates 2&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;use std::env;
use std::process;

const TARGET: [u8; 32] = [0x15, 0x5A, 0xAC, 0xF6, 0x36, 0x22, 0x3B, 0x52, 0x6C, 0x4F, 0x90, 0xD9, 0x35, 0x63, 0xF8, 0x0E, 0x02, 0x33, 0xB0, 0xF1, 0xB7, 0x69, 0x42, 0x67, 0x25, 0xEA, 0x96, 0x63, 0x1B, 0xA7, 0x03, 0x0B];
const XOR_KEY: [u8; 5] = [0x7E, 0x33, 0x91, 0x4C, 0xA5];
const ROTATION_PATTERN: [u32; 7] = [1, 3, 5, 7, 2, 4, 6];
const MAGIC_SUB: u8 = 0x5D;

fn print_flag(buffer: &amp;amp;str) {
    println!(&quot;Flag: {0}\n&quot;, buffer);
}

/// Rotate a byte left by n positions
/// This is a bijection because all 8 bit rotations of a byte are unique
/// ROL(ROL(x, n), 8-n) = x, proving invertibility
fn rotate_left(byte: u8, n: u32) -&amp;gt; u8 {
    byte.rotate_left(n % 8)
}

/// OPERATION 1: XOR with NEW rotating key
/// Each byte is XORed with one of 5 NEW key bytes (cycling through them)
/// Bijection proof: (x ⊕ k) ⊕ k = x (XOR involution)
fn apply_quantum_cipher_v2(buffer: &amp;amp;mut [u8]) {
    for (i, byte) in buffer.iter_mut().enumerate() {
        *byte ^= XOR_KEY[i % 5];
    }
}

/// OPERATION 2 (NEW!): Rotate Left with varying amounts
/// Each byte is rotated left by an amount determined by its position
/// Bijection proof: ROL⁻¹ = ROR with same amount
/// The rotation amount varies: position mod 7 selects from ROTATION_PATTERN
fn apply_stellar_rotation(buffer: &amp;amp;mut [u8]) {
    for (i, byte) in buffer.iter_mut().enumerate() {
        let rotation = ROTATION_PATTERN[i % 7];
        *byte = rotate_left(*byte, rotation);
    }
}

/// OPERATION 3: Swap adjacent byte pairs
/// Bytes at positions (0,1), (2,3), (4,5), etc. are swapped
/// Bijection proof: Swapping twice returns original (f ∘ f = identity)
fn apply_spatial_transposition(buffer: &amp;amp;mut [u8]) {
    for i in (0..buffer.len()).step_by(2) {
        buffer.swap(i, i + 1);
    }
}

/// OPERATION 4: Subtract magic constant (mod 256) - CHANGED FROM ADDITION!
/// Each byte has MAGIC_SUB subtracted from it (wrapping at 256)
/// Bijection proof: (x - k) + k ≡ x (mod 256)
/// Subtraction forms a group, every element has unique inverse
fn apply_gravitational_shift_v2(buffer: &amp;amp;mut [u8]) {
    for byte in buffer.iter_mut() {
        *byte = byte.wrapping_sub(MAGIC_SUB);
    }
}

/// OPERATION 5 (NEW!): Reverse bytes in chunks of 5
/// Splits the 30-byte buffer into 6 chunks of 5, reverses each chunk
/// Chunk 0: [0,1,2,3,4] -&amp;gt; [4,3,2,1,0]
/// Chunk 1: [5,6,7,8,9] -&amp;gt; [9,8,7,6,5], etc.
/// Bijection proof: Reversal is self-inverse, f(f(x)) = x
fn apply_temporal_inversion(buffer: &amp;amp;mut [u8]) {
    const CHUNK_SIZE: usize = 5;
    for chunk_start in (0..buffer.len()).step_by(CHUNK_SIZE) {
        let chunk_end = (chunk_start + CHUNK_SIZE).min(buffer.len());
        buffer[chunk_start..chunk_end].reverse();
    }
}

/// OPERATION 6 (NEW!): XOR each byte with its position SQUARED (mod 256)
/// Byte at position i is XORed with i² mod 256
/// Bijection proof: (x ⊕ k) ⊕ k = x (XOR involution)
/// While i² grows, mod 256 keeps values in range, and XOR remains invertible
fn apply_coordinate_calibration_v2(buffer: &amp;amp;mut [u8]) {
    for (i, byte) in buffer.iter_mut().enumerate() {
        let position_squared = ((i * i) % 256) as u8;
        *byte ^= position_squared;
    }
}

fn process_transmission(input: &amp;amp;str) -&amp;gt; Result&amp;lt;[u8; 32], String&amp;gt; {
    if input.len() != 32 {
        return Err(format!(input.len()));
    }
    let mut buffer = [0u8; 32];
    buffer.copy_from_slice(input.as_bytes());
    apply_quantum_cipher_v2(&amp;amp;mut buffer);
    apply_stellar_rotation(&amp;amp;mut buffer);
    apply_spatial_transposition(&amp;amp;mut buffer);
    apply_gravitational_shift_v2(&amp;amp;mut buffer);
    apply_temporal_inversion(&amp;amp;mut buffer);
    apply_coordinate_calibration_v2(&amp;amp;mut buffer);
    Ok(buffer)
}

fn main() {
    let args: Vec&amp;lt;String&amp;gt; = env::args().collect();
    if args.len() != 2 {
        process::exit(1);
    }

    let input = &amp;amp;args[1];
    match process_transmission(input) {
        Ok(buffer) =&amp;gt; {
            if buffer == TARGET {
                print_flag(input);
            } else {
                println!(&quot;\n&quot;);
                process::exit(1);
            }
        }
        Err(err) =&amp;gt; {
            eprintln!(&quot;{}&quot;, err);
            process::exit(1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pipeline for the encryption, from input to flag, is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;XOR with rotating key&lt;/li&gt;
&lt;li&gt;Rotate left with varying amounts&lt;/li&gt;
&lt;li&gt;Swap adjacent byte pairs&lt;/li&gt;
&lt;li&gt;Subtract magic constant&lt;/li&gt;
&lt;li&gt;Reverse bytes in chunks of 5&lt;/li&gt;
&lt;li&gt;XOR each byte with its position squared&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, we need to undo these steps, from operation 6 back to 1, to get the orginal flag. Simple.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET = [0x15, 0x5A, 0xAC, 0xF6, 0x36, 0x22, 0x3B, 0x52, 0x6C, 0x4F, 0x90, 0xD9, 0x35, 0x63, 0xF8, 0x0E, 0x02, 0x33, 0xB0, 0xF1, 0xB7, 0x69, 0x42, 0x67, 0x25, 0xEA, 0x96, 0x63, 0x1B, 0xA7, 0x03, 0x0B]
XOR_KEY = [0x7E, 0x33, 0x91, 0x4C, 0xA5]
ROTATION_PATTERN = [1, 3, 5, 7, 2, 4, 6]
MAGIC_SUB = 0x5D

def ror(byte, n):
    n %= 8
    return ((byte &amp;gt;&amp;gt; n) | ((byte &amp;lt;&amp;lt; (8 - n)) &amp;amp; 0xFF)) &amp;amp; 0xFF

def solve():
    buf = TARGET.copy()
    # Operation 6
    for i in range(32):
        buf[i] ^= (i * i) % 256
    # Operation 5
    for cs in range(0, 32, 5):
        buf[cs:cs+5] = reversed(buf[cs:cs+5])
    # Operation 4
    for i in range(32):
        buf[i] = (buf[i] + MAGIC_SUB) &amp;amp; 0xFF
    # Operation 3
    for i in range(0, 32, 2):
        buf[i], buf[i+1] = buf[i+1], buf[i]
    # Operation 2
    for i in range(32):
        rot = ROTATION_PATTERN[i % 7]
        buf[i] = ror(buf[i], rot)
    # Operation 1
    for i in range(32):
        buf[i] ^= XOR_KEY[i % 5]
    s = bytes(buf).decode(&quot;utf-8&quot;)
    print(&quot;Recovered input:&quot;)
    print(s)

if __name__ == &quot;__main__&quot;:
    solve()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[pctf{Y0U_F0UND_TH3_P1R4T3_B00TY}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Space Pirates 3&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;package main
import (
	&quot;fmt&quot;
	&quot;os&quot;
)
var target = [30]byte{
	0x60, 0x6D, 0x5D, 0x97, 0x2C, 0x04, 0xAF, 0x7C, 0xE2, 0x9E, 0x77, 0x85, 0xD1, 0x0F, 0x1D, 0x17, 0xD4, 0x30, 0xB7, 0x48, 0xDC, 0x48, 0x36, 0xC1, 0xCA, 0x28, 0xE1, 0x37, 0x58, 0x0F,
}
var xorKey = [7]byte{0xC7, 0x2E, 0x89, 0x51, 0xB4, 0x6D, 0x1F}
var rotationPattern = [8]uint{7, 5, 3, 1, 6, 4, 2, 0}
const magicSub byte = 0x93
const chunkSize = 6

func printFlag(input string) {
	fmt.Println(&quot;Flag: &quot;, input)
}
func rotateLeft(b byte, n uint) byte {
	n = n % 8
	return (b &amp;lt;&amp;lt; n) | (b &amp;gt;&amp;gt; (8 - n))
}
func applyUltimateQuantumCipher(buffer []byte) {
	for i := range buffer {
		buffer[i] ^= xorKey[i%len(xorKey)]
	}
}
func applyStellarRotationV2(buffer []byte) {
	for i := range buffer {
		rotation := rotationPattern[i%len(rotationPattern)]
		buffer[i] = rotateLeft(buffer[i], rotation)
	}
}
func applySpatialTransposition(buffer []byte) {
	for i := 0; i &amp;lt; len(buffer)-1; i += 2 {
		buffer[i], buffer[i+1] = buffer[i+1], buffer[i]
	}
}
func applyGravitationalShiftV3(buffer []byte) {
	for i := range buffer {
		buffer[i] -= magicSub
	}
}
func applyTemporalInversionV2(buffer []byte) {
	for chunkStart := 0; chunkStart &amp;lt; len(buffer); chunkStart += chunkSize {
		chunkEnd := chunkStart + chunkSize
		if chunkEnd &amp;gt; len(buffer) {
			chunkEnd = len(buffer)
		}
		for i, j := chunkStart, chunkEnd-1; i &amp;lt; j; i, j = i+1, j-1 {
			buffer[i], buffer[j] = buffer[j], buffer[i]
		}
	}
}
func applyCoordinateCalibrationV3(buffer []byte) {
	for i := range buffer {
		positionValue := ((i * i) + i) % 256
		buffer[i] ^= byte(positionValue)
	}
}

func processVault(input string) ([30]byte, error) {
	var result [30]byte
	if len(input) != 30 {
		return result, fmt.Errorf(
			&quot;Invalid vault combination&quot;,
		)
	}
	copy(result[:], input)
	buffer := result[:]
	applyUltimateQuantumCipher(buffer)
	applyStellarRotationV2(buffer)
	applySpatialTransposition(buffer)
	applyGravitationalShiftV3(buffer)
	applyTemporalInversionV2(buffer)
	applyCoordinateCalibrationV3(buffer)
	return result, nil
}

func main() {
	if len(os.Args) != 2 {
		os.Exit(1)
	}
	input := os.Args[1]
	result, err := processVault(input)
	if err != nil {
		fmt.Fprintf(os.Stderr, &quot; %v\n&quot;, err)
		os.Exit(1)
	}
	if result == target {
		printFlag(input)
	} else {
		fmt.Println(&quot;\n&quot;)
		os.Exit(1)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Different programming language, but have the same meaning in each operation, the same as &lt;a href=&quot;#space-pirates-2&quot;&gt;Sapce Pirates 2&lt;/a&gt;. So, to reverse it, we need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;XOR each byte with &lt;code&gt;(i^2 + i) mod 256&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Reverse each 6-byte chunk&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;magicSub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Swap adjacent pairs&lt;/li&gt;
&lt;li&gt;Rotate right by the rotation pattern&lt;/li&gt;
&lt;li&gt;XOR with 7-byte &lt;code&gt;xorKey&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;TARGET = [0x60, 0x6D, 0x5D, 0x97, 0x2C, 0x04, 0xAF, 0x7C, 0xE2, 0x9E, 0x77, 0x85, 0xD1, 0x0F, 0x1D, 0x17, 0xD4, 0x30, 0xB7, 0x48, 0xDC, 0x48, 0x36, 0xC1, 0xCA, 0x28, 0xE1, 0x37, 0x58, 0x0F]
XOR_KEY = [0xC7, 0x2E, 0x89, 0x51, 0xB4, 0x6D, 0x1F]
ROTATION_PATTERN = [7, 5, 3, 1, 6, 4, 2, 0]
MAGIC_SUB = 0x93

def ror(byte, n):
    n %= 8
    return ((byte &amp;gt;&amp;gt; n) | ((byte &amp;lt;&amp;lt; (8 - n)) &amp;amp; 0xFF)) &amp;amp; 0xFF

def solve():
    buf = TARGET.copy()
    # Operation 6
    for i in range(30):
        buf[i] ^= ((i * i) + i) % 256
    # Operation 5
    for cs in range(0, 30, 6):
        buf[cs:cs+6] = reversed(buf[cs:cs+6])
    # Operation 4
    for i in range(30):
        buf[i] = (buf[i] + MAGIC_SUB) &amp;amp; 0xFF
    # Operation 3
    for i in range(0, 30, 2):
        buf[i], buf[i+1] = buf[i+1], buf[i]
    # Operation 2
    for i in range(30):
        rot = ROTATION_PATTERN[i % 8]
        buf[i] = ror(buf[i], rot)
    # Operation 1
    for i in range(30):
        buf[i] ^= XOR_KEY[i % 7]
    s = bytes(buf).decode(&quot;utf-8&quot;)
    print(&quot;Recovered input:&quot;)
    print(s)

if __name__ == &quot;__main__&quot;:
    solve()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[pctf{M4ST3R_0F_TH3_S3V3N_S34S}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Vorpal Masters&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;void main(void) {
  int iVar1;
  int local_20;
  char local_1c [11];
  char local_11;
  char local_10;
  char local_f;
  char local_e;
  int local_c;
  
  puts(&quot;Welcome to {insert game here}\nPlease enter the license key from the 3rd page of the booklet.&quot;);
  local_c = __isoc99_scanf(&quot;%4s-%d-%10s&quot;, &amp;amp;local_11, &amp;amp;local_20, local_1c);
  if (local_c != 3) {
    puts(&quot;Please enter you key in the format xxxx-xxxx-xxxx&quot;);
    exit(0);
  }
  if ((((local_11 != &apos;C&apos;) || (local_f != &apos;C&apos;)) || (local_e != &apos;I&apos;)) || (local_10 != &apos;A&apos;)) {
    womp_womp();
  }
  if ((-0x1389 &amp;lt; local_20) &amp;amp;&amp;amp; (local_20 &amp;lt; 0x2711)) {
    if ((local_20 + 0x16) % 0x6ca == ((local_20 * 2) % 2000) * 6 + 9) goto LAB_00101286;
  }
  womp_womp();
LAB_00101286:
  iVar1 = strcmp(local_1c, &quot;PatriotCTF&quot;);
  if (iVar1 != 0) womp_womp();
  puts(&quot;Lisence key registered, you may play the game now!&quot;);
  return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key is in the form &lt;code&gt;xxxx-xxxx-xxxx&lt;/code&gt;. In &lt;code&gt;local_c&lt;/code&gt;, it decleares that &lt;code&gt;local_c = __isoc99_scanf(&quot;%4s-%d-%10s&quot;, &amp;amp;local_11, &amp;amp;local_20, local_1c);&lt;/code&gt;, meaning there are 3 parts in the key:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First part is a string having 4 characters;&lt;/li&gt;
&lt;li&gt;Second part is a number;&lt;/li&gt;
&lt;li&gt;Third part is a string having 10 characters.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;For the first part, although the arrangement in the code was wrong (&lt;code&gt;CCIA&lt;/code&gt;), but based on the memory of each variable, we know that the first part is &lt;code&gt;CACI&lt;/code&gt; (like what the challenge statement said).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For the second part, we know that:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  if ((-0x1389 &amp;lt; local_20) &amp;amp;&amp;amp; (local_20 &amp;lt; 0x2711)) {
    if ((local_20 + 0x16) % 0x6ca == ((local_20 * 2) % 2000) * 6 + 9) goto LAB_00101286;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which means:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for n in range(-4999, 10000):
    if (n + 22) % 1738 == ((n * 2) % 2000) * 6 + 9:
        print(n) # goto LAB_00101286;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the second part is &lt;code&gt;2025&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For the third part, we already know it is &lt;code&gt;PatriotCTF&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[CACI{2025-PatriotCTF}]&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>(VN) TryHackMe - Reversing ELF</title><link>https://khavidbap.github.io/posts/tryhackme-reverselfiles/</link><guid isPermaLink="true">https://khavidbap.github.io/posts/tryhackme-reverselfiles/</guid><description>Writeup cho tất cả các task trong room &quot;Reversing ELF&quot; trên platform TryHackMe.</description><pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://tryhackme.com/room/reverselfiles&quot;&gt;Link to the room&lt;/a&gt;&lt;/h1&gt;
&lt;h1&gt;Crackme1&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;//...
  char local_98 [32];
  uint local_78 [28];
  
  local_78[0] = 0x25;
  local_78[1] = 0x2b;
  //...
  local_78[0x18] = 0x25;
  local_78[0x19] = 0x3c;
  local_78[0x1a] = 0xffffffbf;

  memset(local_98, 0x41, 0x1b);
  for (local_78[0x1b] = 0; local_78[0x1b] &amp;lt; 0x1b; local_78[0x1b] = local_78[0x1b] + 1) {
    local_98[(int)local_78[0x1b]] =
        (char)local_78[(int)local_78[0x1b]] + local_98[(int)local_78[0x1b]];
  }
  puts(local_98);
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;local_98&lt;/code&gt; = 32-byte buffer, cài đặt &lt;code&gt;A [0x41]&lt;/code&gt; lập lại &lt;code&gt;27 [0x1B]&lt;/code&gt; lần.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local_78&lt;/code&gt; = array gồm 27 số (từ &lt;code&gt;0&lt;/code&gt; đến &lt;code&gt;0x1A&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local_98[i] = (char)local_78[i] + local_98[i]&lt;/code&gt;, hoặc &lt;code&gt;output[i] = &apos;A&apos; + local_78[i]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;local_78 = [0x25, 0x2b, 0x20, 0x26, 0x3a, 0x2d, 0x2e, 0x33, 0x1e, 0x33,
            0x27, 0x20, 0x33, 0x1e, 0x2a, 0x28, 0x2d, 0x23, 0x1e, 0x2e,
            0x25, 0x1e, 0x24, 0x2b, 0x25, 0x3c]

print(&quot;&quot;.join(chr(0x41 + i) for i in local_78))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What is the flag? - :spoiler[flag{not_that_kind_of_elf}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme2&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;//...
  if (param_1 == 2)
    iVar2 = strcmp((char *)param_2[1],&quot;&amp;lt;password&amp;gt;&quot;);
    if (iVar2 == 0) {
      puts(&quot;Access granted.&quot;);
      giveFlag();
      uVar1 = 0;
    }
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What is the super secret password? - :spoiler[super_secret_password]&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
  int iVar1;
  undefined4 *puVar2;
  undefined4 *puVar3;
  char local_11f [51];
  undefined4 local_ec [51];
  uint local_20;

  puVar2 = &amp;amp;DAT_080486c0;
  puVar3 = local_ec;
  for (iVar1 = 0x33; iVar1 != 0; iVar1 = iVar1 + -1) {
    *puVar3 = *puVar2;
    puVar2 = puVar2 + 1;
    puVar3 = puVar3 + 1;
  }
  memset(local_11f, 0x41, 0x33);
  for (local_20 = 0; local_20 &amp;lt; 0x33; local_20 = local_20 + 1) {
    local_11f[local_20] = (char)local_ec[local_20] + local_11f[local_20];
  }
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dump data ở &lt;code&gt;DAT_080486c0&lt;/code&gt; để lấy data cần được xử lý.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/204xb 0x080486c0
0x80486c0:      0x25    0x00    0x00    0x00    0x2b    0x00    0x00    0x00
0x80486c8:      0x20    0x00    0x00    0x00    0x26    0x00    0x00    0x00
0x80486d0:      0x3a    0x00    0x00    0x00    0x28    0x00    0x00    0x00
0x80486d8:      0x25    0x00    0x00    0x00    0x1e    0x00    0x00    0x00
0x80486e0:      0x28    0x00    0x00    0x00    0x1e    0x00    0x00    0x00
0x80486e8:      0x32    0x00    0x00    0x00    0x34    0x00    0x00    0x00
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vì mỗi giá trị ở &lt;code&gt;local_ec&lt;/code&gt; được định dạng bằng &lt;code&gt;undefined4&lt;/code&gt;, tức là mỗi phần tử đều có giá trị là 4 byte. Nên chúng ta chỉ cần lấy byte thấp nhất của mỗi giá trị integer này.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte = [0x25, 0x2b, 0x20, 0x26, 0x3a, 0x28, 0x25, 0x1e, 0x28, 0x1e, 
        0x32, 0x34, 0x21, 0x2c, 0x28, 0x33, 0x1e, 0x33, 0x27, 0x28, 
        0x32, 0x1e, 0x25, 0x2b, 0x20, 0x26, 0x1e, 0x33, 0x27, 0x24, 
        0x2d, 0x1e, 0x28, 0x1e, 0x36, 0x28, 0x2b, 0x2b, 0x1e, 0x26, 
        0x24, 0x33, 0x1e, 0x2f, 0x2e, 0x28, 0x2d, 0x33, 0x32, 0x3c]

print(&apos;&apos;.join(chr(i + 0x41) for i in byte))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What is the flag? - :spoiler[flag{if_i_submit_this_flag_then_i_will_get_points}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme3&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;void processEntry entry(undefined4 param_1,undefined4 param_2) {
  undefined auStack_4 [4];
  __libc_start_main(FUN_080484f4, param_2,&amp;amp;stack0x00000004, FUN_08048d90, 
                    FUN_08048e00, param_1, auStack_4);
  do {} while( true );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đọc function &lt;code&gt;FUN_080484f4&lt;/code&gt; để lấy thêm thông tin.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;undefined4 FUN_080484f4(int param_1,undefined4 *param_2) {
    //...
    if ((sVar1 == 0x40) &amp;amp;&amp;amp; (iVar2 = strcmp(__s_00,&quot;&amp;lt;flag&amp;gt;&quot;), iVar2 == 0)) {
        puts(&quot;Correct password!&quot;);
        return 0;
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What is the flag? - :spoiler[f0r_y0ur_5ec0nd_le55on_unbase64_4ll_7h3_7h1ng5]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme4&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;undefined8 main(int param_1,undefined8 *param_2) {
  //...
  if (param_1 == 2) {
    compare_pwd((char *)param_2[1]);
  }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void compare_pwd(char *param_1) {
    //...
    builtin_strncpy(local_28,&quot;I]{I\x14V\x17{WAGQV\x17{TS@&quot;,0x13);
    get_pwd((long)local_28);
    iVar1 = strcmp(local_28,param_1);
    if (iVar1 == 0) {
        puts(&quot;password OK&quot;);
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;String ở &lt;code&gt;local_28&lt;/code&gt; bị mã hóa, đọc function &lt;code&gt;get_pwd()&lt;/code&gt; để hiểu cách để đưa lại string ban đầu.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void get_pwd(long param_1) {
  undefined4 local_c; 
  local_c = -1;
  while (local_c = local_c + 1, *(char *)(param_1 + local_c) != &apos;\0&apos;) {
    *(byte *)(local_c + param_1) = *(byte *)(param_1 + local_c) ^ 0x24;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ở đây, mỗi kí tự trong &lt;code&gt;local_28&lt;/code&gt; sẽ XOR với ký tự khác (&lt;code&gt;0x24&lt;/code&gt;), từ đây viết lại code để in ra password.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;local_28 = b&quot;I]{I\x14V\x17{WAGQV\x17{TS@&quot;
decoded = bytes([i ^ 0x24 for i in local_28])
print(decoded.decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What is the password? - :spoiler[my_m0r3_secur3_pwd]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme5&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;undefined8 main(void) {
  //...
  builtin_strncpy(local_38,&quot;OfdlDSA|3tXb32~X3tX@sX`4tXtz&quot;,0x1c);
  puts(&quot;Enter your input:&quot;);
  __isoc99_scanf(&amp;amp;DAT_00400966,local_58);
  iVar1 = strcmp_(local_58,local_38);
  if (iVar1 == 0) {
    puts(&quot;Good game&quot;);
  }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Có một bug trong chương trình này, thay vì &lt;code&gt;strcmp()&lt;/code&gt; thì họ đã sử dụng &lt;code&gt;strcmp_()&lt;/code&gt;. Mặc dù vô hại nhưng cách xử lý của 2 function này là hoàn toàn khác nhau. Đọc thử function &lt;code&gt;strcmp_()&lt;/code&gt; để hiểu cách hoạt động.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void strcmp_(char *param_1, char *param_2) {
  //...
  while( true ) {
    sVar1 = strlen(param_1);
    if (sVar1 &amp;lt;= (ulong)(long)local_1c) break;
    param_1[local_1c] = (byte)key ^ param_1[local_1c];
    local_1c = local_1c + 1;
  }
  strncmp(param_1, param_2, 0x1c);
  return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Có thể để ý ở đây mỗi ký tự được XOR với &lt;code&gt;key&lt;/code&gt;. Xem thử &lt;code&gt;key&lt;/code&gt; ở global có giá trị nào không.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/wx 0x601064
0x601064 &amp;lt;key&amp;gt;: 0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vậy là xem như dãy kí tự ở &lt;code&gt;local_38&lt;/code&gt; không bị mã hóa như kí tự ở input ban đầu.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the input? - :spoiler[OfdlDSA|3tXb32~X3tX@sX`4tXtz]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme6&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;undefined8 main(int param_1,undefined8 *param_2) {
  if (param_1 == 2) {
    compare_pwd((char *)param_2[1]);
  }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void compare_pwd(char *param_1) {
  //...
  uVar1 = my_secure_test(param_1);
  if ((int)uVar1 == 0) {
    puts(&quot;password OK&quot;);
  }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;undefined8 my_secure_test(char *param_1) {
  undefined8 uVar1;
  
  if ((*param_1 == &apos;\0&apos;) || (*param_1 != &apos;1&apos;)) {
    uVar1 = 0xffffffff;
  }
  else if ((param_1[1] == &apos;\0&apos;) || (param_1[1] != &apos;3&apos;)) {
    uVar1 = 0xffffffff;
  }
  //...
  else if ((param_1[7] == &apos;\0&apos;) || (param_1[7] != &apos;d&apos;)) {
    uVar1 = 0xffffffff;
  }
  else if (param_1[8] == &apos;\0&apos;) {
    uVar1 = 0;
  }
  else {
    uVar1 = 0xffffffff;
  }
  return uVar1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mỗi ký tự ở password là parameter cần nhập vào input để check xem password đó có đúng hay chưa.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the password? - :spoiler[1337_pwd]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme7&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;undefined4 main(undefined param_1) {
    //...
    else if (local_14 == 0x7a69) {
        puts(&quot;Wow such h4x0r!&quot;);
        giveFlag();
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void giveFlag(void) {
  //...
  puVar2 = &amp;amp;DAT_080488e0;
  puVar3 = local_a8;
  for (iVar1 = 0x22; iVar1 != 0; iVar1 = iVar1 + -1) {
    *puVar3 = *puVar2;
    puVar2 = puVar2 + 1;
    puVar3 = puVar3 + 1;
  }
  memset(local_ca,0x41,0x22);
  for (local_20 = 0; local_20 &amp;lt; 0x22; local_20 = local_20 + 1) {
    local_ca[local_20] = (char)local_a8[local_20] + local_ca[local_20];
  }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tới đây, cách xử lý quay về như bài &lt;a href=&quot;#crackme2&quot;&gt;Crackme2&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/136xb 0x080488e0
0x80488e0:      0x25    0x00    0x00    0x00    0x2b    0x00    0x00    0x00
0x80488e8:      0x20    0x00    0x00    0x00    0x26    0x00    0x00    0x00
0x80488f0:      0x3a    0x00    0x00    0x00    0x2c    0x00    0x00    0x00
0x80488f8:      0x34    0x00    0x00    0x00    0x22    0x00    0x00    0x00
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What is the flag? - :spoiler[flag{much_reversing_very_ida_wow}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Crackme8&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;undefined4 main(int param_1,undefined4 *param_2) {
  //...
  if (param_1 == 2) {
    iVar2 = atoi((char *)param_2[1]);
    if (iVar2 == -0x35010ff3) {
      puts(&quot;Access granted.&quot;);
      giveFlag();
      uVar1 = 0;
    }
  }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Function &lt;code&gt;giveFlag()&lt;/code&gt; hoàn toàn y chang với bài &lt;a href=&quot;#crackme2&quot;&gt;Crackme2&lt;/a&gt; và &lt;a href=&quot;#crackme7&quot;&gt;Crackme7&lt;/a&gt;. Áp dụng các kỹ thuật đã được nêu ở 2 bài trên để ra được flag.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the flag? - :spoiler[flag{at_least_this_cafe_wont_leak_your_credit_card_numbers}]&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>(EN) osu!gaming CTF 2025 - Crypto</title><link>https://khavidbap.github.io/posts/osuctf2025-crypto/</link><guid isPermaLink="true">https://khavidbap.github.io/posts/osuctf2025-crypto/</guid><description>Writeup for all solved Crypto challenge in osu!gaming CTF 2025 by #MogamiShizuka</description><pubDate>Fri, 31 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;rot727&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;aeg{at_imuf_nussqd_zgynqd_paqezf_yqmz_yadq_eqogdq}&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As the title mentioned, we can see that ROT727 is almost similar to ROT13. So we can put that in some decoder to brute-force the ROT, and the flag is encrypted in ROT14.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[osu{oh_wait_bigger_number_doesnt_mean_more_secure}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;beyond-wood&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image
import random

FLAG = Image.open(&quot;flag.png&quot;)
width, height = FLAG.size

key = [random.randrange(0, 256) for _ in range(width+height+3)]

out = FLAG.copy()
for i in range(width):
    for j in range(height):
        pixel = FLAG.getpixel((i, j))
        pixel = tuple(x ^ k for x, k in zip(pixel, key))
        newi, newj = (2134266 + i * 727) % width, (4501511 + j * 727) % height 
        out.putpixel((newi, newj), pixel)

out.save(&quot;output.png&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;key&lt;/code&gt; is built with random bytes of length &lt;code&gt;width + height + 3&lt;/code&gt;. For each &lt;code&gt;(i, j)&lt;/code&gt; it XORS the pixel channels with &lt;code&gt;key&lt;/code&gt; starting from &lt;code&gt;key[0]&lt;/code&gt;. Meaning that XOR key takes the first 3 bytes of &lt;code&gt;key&lt;/code&gt;, and it&apos;s constant per channel (one byte for &lt;code&gt;R&lt;/code&gt;, one byte for &lt;code&gt;G&lt;/code&gt; and one byte for &lt;code&gt;B&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Then it write that XORed pixel to a new permuted location.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;newi = (2134266 + i * 727) % width
newj = (4501511 + j * 727) % height
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a linear (affine) permutation on each axis, so it&apos;s reversible.&lt;/p&gt;
&lt;p&gt;To solve this challenge, we first invert the permutation, by using the formula, and place it at the original pixel.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inv_i[(2134266 + i * 727) % width] = i
inv_j[(4501511 + j * 727) % height] = j
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then recover the 3 key bytes. Because XOR key is global, so that we can use some frequency assumptions such as black &lt;code&gt;(0, 0, 0)&lt;/code&gt; or white &lt;code&gt;(255, 255, 255)&lt;/code&gt;, to XOR the unshuffled image with the recovered key to get the original.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image
from collections import Counter

def build_inverse_map(length, offset, mult):
    inv = [None] * length
    for i in range(length):
        new = (offset + i * mult) % length
        inv[new] = i
    return inv

def unshuffle(img):
    width, height = img.size
    inv_i = build_inverse_map(width, 2134266, 727)
    inv_j = build_inverse_map(height, 4501511, 727)
    out = Image.new(img.mode, (width, height))
    src = img.load()
    dst = out.load()
    for x in range(width):
        for y in range(height):
            dst[inv_i[x], inv_j[y]] = src[x, y]
    return out

def xor_pixel(pixel, key):
    return tuple(p ^ k for p, k in zip(pixel, key))

def apply_key(img, key):
    width, height = img.size
    out = Image.new(img.mode, (width, height))
    src = img.load()
    dst = out.load()
    for x in range(width):
        for y in range(height):
            dst[x, y] = xor_pixel(src[x, y], key)
    return out

def main():
    img = Image.open(&quot;output.png&quot;).convert(&quot;RGB&quot;)
    width, height = img.size
    unshuffled = unshuffle(img)

    for bg in [(0,0,0), (255,255,255)]:
        key = tuple(mc[i] ^ bg[i] for i in range(3))
        print(f&quot;Recovering {bg}, key = {key}&quot;)
        recovered = apply_key(unshuffled, key)
        recovered.save(f&quot;recovered_{bg[0]}_{bg[1]}_{bg[2]}.png&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[osu{h1_05u_d351gn_t34m}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;xnor-xnor-xnor&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import os
flag = open(&quot;flag.txt&quot;, &quot;rb&quot;).read()

def xnor_gate(a, b):
    if a == 0 and b == 0:
        return 1
    elif a == 0 and b == 1:
        return 0
    elif a == 1 and b == 0:
        return 0
    else:
        return 1

def str_to_bits(s):
    bits = []
    for x in s:
        bits += [(x &amp;gt;&amp;gt; i) &amp;amp; 1 for i in range(8)][::-1]
    return bits

def bits_to_str(bits):
    return bytes([sum(x * 2 ** j for j, x in enumerate(bits[i:i+8][::-1])) for i in range(0, len(bits), 8)])

def xnor(pt_bits, key_bits):
    return [xnor_gate(pt_bit, key_bit) for pt_bit, key_bit in zip(pt_bits, key_bits)]

key = os.urandom(4) * (1 + len(flag) // 4)
key_bits = str_to_bits(key)
flag_bits = str_to_bits(flag)
enc_flag = xnor(xnor(xnor(flag_bits, key_bits), key_bits), key_bits)

print(bits_to_str(enc_flag).hex())
# 7e5fa0f2731fb9b9671fb1d62254b6e5645fe4ff2273b8f04e4ee6e5215ae6ed6c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have &lt;code&gt;XNOR(a, b) = 1 - (a XOR b)&lt;/code&gt;, so if we have &lt;code&gt;t = XNOR(XNOR(XNOR(a, k), k), k)&lt;/code&gt;, meaning that if &lt;code&gt;k = 1&lt;/code&gt; then &lt;code&gt;t = a&lt;/code&gt;, and &lt;code&gt;k = 0&lt;/code&gt; then &lt;code&gt;t = NOT a&lt;/code&gt;. Therefore the whole triple-XNOR is just a bitwise XOR with the bitwise complement of the key. In bytes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mask_byte = 0xff - key_byte
cipher = plaintext XOR mask
plaintext = cipher XOR mask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;key&lt;/code&gt; is &lt;code&gt;os.urandom(4)&lt;/code&gt; repeated, &lt;code&gt;mask&lt;/code&gt; repeats every 4 bytes. So if we know the what the plaintext is, it&apos;s reversible.&lt;/p&gt;
&lt;p&gt;We know the flag format is &lt;code&gt;osu{&lt;/code&gt;, it&apos;s &lt;code&gt;0x6f 0x73 0x75 0x7b&lt;/code&gt;. Calculate the first 4 mask bytes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mask0 = 0x7e XOR 0x6f = 0x11
mask1 = 0x5f XOR 0x73 = 0x2c
mask2 = 0xa0 XOR 0x75 = 0xd5
mask3 = 0xf2 XOR 0x7b = 0x89
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have the 4-byte repeated &lt;code&gt;mask&lt;/code&gt; as &lt;code&gt;0x11 0x2c 0xd5 0x89&lt;/code&gt;, so the &lt;code&gt;key&lt;/code&gt; is &lt;code&gt;0xff - mask_byte&lt;/code&gt;, meaning &lt;code&gt;0xee 0xd3 0x2a 0x76&lt;/code&gt;. XOR it with the cipher text, and we have the flag.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c = bytes.fromhex(&quot;7e5fa0f2731fb9b9671fb1d62254b6e5645fe4ff2273b8f04e4ee6e5215ae6ed6c&quot;)
known = b&quot;osu{&quot;
mask = [c[i] ^ known[i] for i in range(4)]
t = bytes(c[i] ^ mask[i % 4] for i in range(len(c)))
print(t.decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[osu{b3l0v3d_3xclus1v3_my_b3l0v3d}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;pls-nominate&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;from Crypto.Util.number import * 
FLAG = open(&quot;flag.txt&quot;, &quot;rb&quot;).read() 
message = bytes_to_long( b&quot;hello there can you pls nominate my map https://osu.ppy.sh/beatmapsets/2436259 :steamhappy: i can bribe you with a flag if you do: &quot; + FLAG ) 

ns = [getPrime(727) * getPrime(727) for _ in range(5)] 
e = 5 
print(len(FLAG)) 
print(ns) 
print([pow(message, e, n) for n in ns])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This challenge is a simple Hastad-style broadcast RSA with &lt;code&gt;e = 5&lt;/code&gt;. So we just need to combine 5 ciphertexts with the Chinese Remainder Theorem and search for the small multiple &lt;code&gt;k&lt;/code&gt; so that &lt;code&gt;M_e + k * N&lt;/code&gt; is a perfect 5th power.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ns = [...]
ciphers = [...]
e = 5

def prod(a):
    p = 1
    for x in a: p *= x
    return p

def ith_root(x, n):
    if x &amp;lt; 2: return x, True
    lo, hi = 0, 1 &amp;lt;&amp;lt; ((x.bit_length() + n - 1) // n + 1)
    while lo + 1 &amp;lt; hi:
        m = (lo + hi) // 2
        if m ** n == x: return m, True
        if m ** n &amp;lt; x: lo = m
        else: hi = m
    return (lo, lo ** n == x)

# CRT
N = prod(ns)
Me = 0
for n, c in zip(ns, ciphers):
    Ni = N // n
    inv = pow(Ni, -1, n)
    Me += c * Ni * inv
Me %= N

# Search small k
r, exact = ith_root(Me, e)
if not exact:
    for k in range(0, 500000):
        r, exact = ith_root(Me + k*N, e)
        if exact:
            break

m = r
b = m.to_bytes((m.bit_length() + 7) // 8, &apos;big&apos;)
i = b.find(b&quot;osu{&quot;)
print(b[i:i + 200].decode() if i != -1 else b[:200])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[osu{pr3tty_pl3453_w1th_4_ch3rry_0n_t0p!?:pleading:}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;linear-feedback&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;from secrets import randbits 
from math import floor 
from hashlib import sha256 

class LFSR: 
    def __init__(self, key, taps, format): 
        self.key = key 
        self.taps = taps 
        self.state = list(map(int, list(format.format(key)))) 
        
    def _clock(self): 
        ob = self.state[0] 
        self.state = self.state[1:] + [sum([self.state[t] for t in self.taps]) % 2] 
        return ob 
        
def xnor_gate(a, b): 
    if a == 0 and b == 0: return 1 
    elif a == 0 and b == 1: return 0 
    elif a == 1 and b == 0: return 0 
    else: return 1 
        
key1 = randbits(21) 
key2 = randbits(29) 
L1 = LFSR(key1, [2, 4, 5, 1, 7, 9, 8], &quot;{:021b}&quot;) 
L2 = LFSR(key2, [5, 3, 5, 5, 9, 9, 7], &quot;{:029b}&quot;) 

bits = [xnor_gate(L1._clock(), L2._clock()) for _ in range(floor(72.7))] 
print(bits) 

FLAG = open(&quot;flag.txt&quot;, &quot;rb&quot;).read() 
keystream = sha256((str(key1) + str(key2)).encode()).digest() * 2 
print(bytes([b1 ^ b2 for b1, b2 in zip(FLAG, keystream)]).hex())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the challenge where turning the LFSRs into a linear system over GF(2) and solving the initial register bits.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The two LFSRs are linear over GF(2), where each output bit at &lt;code&gt;t&lt;/code&gt; is a linear function of the initial state bits.&lt;/li&gt;
&lt;li&gt;The program prints &lt;code&gt;bits = xnor(L1._clock(), L2._clock())&lt;/code&gt;. Since &lt;code&gt;xnor(a, b) = 1&lt;/code&gt; when &lt;code&gt;a == b&lt;/code&gt;, we have: &lt;code&gt;L1_t XOR L2_t = 1 ^ bits_t&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build coefficient vectors for each LFSR output as linear combinations of their initial state bits, for every observed time step. That gives a linear system with 50 unknowns (21 + 29 initial bits) and 72 equations (observed outputs).&lt;/li&gt;
&lt;li&gt;Solve the GF(2) linear system (Gaussian elimination). The system had 2 free variables; try 4 assignments, check which produces ASCII flag when decrypting the provided ciphertext.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Based on these steps, we can have the recovered values as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key1 = 776071    #binary 10111101101001000111 as a 21-bit string
key2 = 340835109 #binary 0101000100010010101100010110101 as a 29-bit string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[osu{th1s_hr1_i5_th3_m0st_fun_m4p_3v3r_1n_0wc}]&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;ssss&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/local/bin/python3 
from Crypto.Util.number import * 
import random 

p = 2**255 - 19 
k = 15 
SECRET = random.randrange(0, p) 

def lcg(x, a, b, p): 
    return (a * x + b) % p 
    
a = random.randrange(0, p) 
b = random.randrange(0, p) 
poly = [SECRET] 
while len(poly) != k: poly.append(lcg(poly[-1], a, b, p)) 

def evaluate_poly(f, x): 
    return sum(c * pow(x, i, p) for i, c in enumerate(f)) % p 
    
print(&quot;welcome to ssss&quot;, flush=True) 
for _ in range(k - 1): 
    x = int(input()) 
    assert 0 &amp;lt; x &amp;lt; p, &quot;no cheating!&quot; 
    print(evaluate_poly(poly, x), flush=True) 
    
if int(input(&quot;secret? &quot;)) == SECRET: 
    FLAG = open(&quot;flag.txt&quot;).read() 
    print(FLAG, flush=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Server returns
$$y = \sum_{i=0}^{k-1} c_i x^i$$ with $$c_{i+1} = ac_i + b, c_0 = SECRET$$.&lt;/li&gt;
&lt;li&gt;Because $$c_i = C_1a^i + C_2$$, then $$y(x) = C_1S(x, a) + C_2U(x)$$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;where $$S(x, a) = \frac{1 - (ax)^k}{1 - ax}$$ and $$U(x) = \frac{1 - x^k}{1 - x}$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send 3 values &lt;code&gt;x&lt;/code&gt; (e.x. 2, 3, 5), receive 3 values &lt;code&gt;y&lt;/code&gt; and calculate 3 nonlinear system for $$a, C_1, C_2$$.&lt;/li&gt;
&lt;li&gt;Eliminate $$C_1, C_2$$ to get one polynomial equation in $$a$$, solve it modulo $$p$$, then recover $$C_1, C_2$$  by linear solve.&lt;/li&gt;
&lt;li&gt;Compute $$SECRET = C_1 + C_2 \pmod{b}$$, send this &lt;code&gt;SECRET&lt;/code&gt; to the server to receive the flag.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import socket, re, sys, time
from sympy import symbols, together, expand, Poly, factor_list, Integer
from Crypto.Util.number import inverse, long_to_bytes

HOST = &quot;ssss.challs.sekai.team&quot;
PORT = 1337

p = 2**255 - 19
k = 15
xs_to_send = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]  # length 14

def recv_line(sock, timeout=10):
    sock.settimeout(timeout)
    data = b&quot;&quot;
    try:
        while True:
            ch = sock.recv(1)
            if not ch:
                break
            data += ch
            if ch == b&quot;\n&quot;:
                break
    except socket.timeout:
        pass
    return data

def extract_last_int_from_bytes(b: bytes):
    m = re.search(rb&quot;(-?\d+)\s*$&quot;, b.strip())
    if not m:
        return None
    return int(m.group(1))

def query_server_and_get_results(host, port, xs):
    s = socket.create_connection((host, port))
    s.settimeout(5)
    for _ in range(4):
        line = recv_line(s, timeout=0.5)
        if not line:
            break

    outputs = []
    for x in xs:
        # send x
        s.sendall(f&quot;{x}\n&quot;.encode())
        val = None
        start = time.time()
        while True:
            line = recv_line(s, timeout=5)
            if not line:
                break
            val = extract_last_int_from_bytes(line)
            if val is not None:
                break
            if time.time() - start &amp;gt; 8:
                break
        if val is None:
            remaining = line.decode(errors=&apos;ignore&apos;) if line else &quot;&amp;lt;no data&amp;gt;&quot;
            print(remaining)
            s.close()
            sys.exit(1)
        outputs.append((x, val))
    return s, outputs

def recover_secret_from_three_pairs(pairs):
    (x1, y1), (x2, y2), (x3, y3) = pairs
    a = symbols(&apos;a&apos;)
    def S_sym(x):
        return (1 - (a * Integer(x))**k) / (1 - a * Integer(x))
    def U_sym(x):
        xI = Integer(x)
        return (1 - xI**k) / (1 - xI)
    S1, S2, S3 = S_sym(x1), S_sym(x2), S_sym(x3)
    U1, U2, U3 = U_sym(x1), U_sym(x2), U_sym(x3)
    Y1, Y2, Y3 = map(Integer, [y1, y2, y3])
    expr = (Y1 * U2 - Y2 * U1) * S3 + (-Y1 * S2 + Y2 * S1) * U3 - Y3 * (S1 * U2 - S2 * U1)
    expr_s = together(expr)
    num, den = expr_s.as_numer_denom()
    poly_expr = expand(num)
    P = Poly(poly_expr, a)
    print(&quot;[*] Integer polynomial degree:&quot;, P.degree())
    coeffs_mod_p = [int(c % p) for c in P.all_coeffs()]
    Pp = Poly(coeffs_mod_p, a, modulus=p)
    print(&quot;[*] Degree over GF(p):&quot;, Pp.degree())
    print(&quot;[*] Factoring polynomial mod p (may take a moment)...&quot;)
    try:
        fact = factor_list(Pp.as_expr(), modulus=p)
        factors = fact[1]
    except Exception as e:
        print(&quot;[!] factor_list failed:&quot;, e)
        factors = []

    candidates = set()
    for fpol, mult in factors:
        fpoly = Poly(fpol, a, modulus=p)
        if fpoly.degree() == 1:
            coefs = fpoly.all_coeffs()
            Acoef, Bcoef = int(coefs[0]) % p, int(coefs[1]) % p
            root = (-Bcoef * inverse(Acoef, p)) % p
            candidates.add(root)

    if not candidates:
        print(&quot;[*] No linear factors found&quot;)
        for guess in range(0, 200000):
            if Pp.eval(guess) % p == 0:
                candidates.add(guess)
                print(&quot;[*] Found a by scanning:&quot;, guess)
                break

    if not candidates:
        raise RuntimeError(&quot;No candidate &apos;a&apos; found.&quot;)

    def compute_S_num(x, a_val):
        ax = (a_val * x) % p
        if (1 - ax) % p == 0:
            return None
        num = (1 - pow(ax, k, p)) % p
        den = (1 - ax) % p
        return (num * inverse(den, p)) % p

    def compute_U_num(x):
        if (1 - x) % p == 0:
            return None
        num = (1 - pow(x, k, p)) % p
        den = (1 - x) % p
        return (num * inverse(den, p)) % p

    for a_cand in candidates:
        S1v = compute_S_num(x1, a_cand)
        S2v = compute_S_num(x2, a_cand)
        S3v = compute_S_num(x3, a_cand)
        U1v = compute_U_num(x1)
        U2v = compute_U_num(x2)
        U3v = compute_U_num(x3)
        if None in (S1v, S2v, S3v, U1v, U2v, U3v):
            continue
        det = (S1v * U2v - S2v * U1v) % p
        if det == 0:
            continue
        inv_det = inverse(det, p)
        C1 = ((Integer(y1) * U2v - Integer(y2) * U1v) * inv_det) % p
        C2 = ((-Integer(y1) * S2v + Integer(y2) * S1v) * inv_det) % p
        y3calc = (C1 * S3v + C2 * U3v) % p
        if y3calc == Integer(y3) % p:
            SECRET = int((C1 + C2) % p)
            return a_cand, SECRET
    raise RuntimeError(&quot;No candidate &apos;a&apos; produced valid C1/C2.&quot;)

def send_secret_and_get_flag(sock, secret):
    sock.sendall(f&quot;{secret}\n&quot;.encode())
    data = b&quot;&quot;
    try:
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            data += chunk
    except socket.timeout:
        pass
    return data.decode(errors=&apos;ignore&apos;)

def main():
    print(&quot;Connecting server&quot;)
    s, outputs = query_server_and_get_results(HOST, PORT, xs_to_send)
    print(&quot;Queried pairs:&quot;, outputs[:3], &quot;... (total returned {})&quot;.format(len(outputs)))
    three = outputs[:3]
    try:
        a_val, secret = recover_secret_from_three_pairs(three)
    except Exception as e:
        print(&quot;Recovery failed:&quot;, e)
        s.close()
        sys.exit(1)

    print(&quot;Recovered a =&quot;, a_val)
    print(&quot;Recovered SECRET (int):&quot;, secret)
    try:
        print(&quot;SECRET (bytes):&quot;, long_to_bytes(secret))
    except Exception:
        pass

    print(&quot;Sending secret&quot;)
    flag_text = send_secret_and_get_flag(s, secret)
    s.close()
    print(&quot;---- SERVER OUTPUT ----&quot;)
    print(flag_text)
    print(&quot;-----------------------&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Flag: :spoiler[osu{0n3_hundr3d_p3rc3nt_4ccur4cy!}]&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>