2 Шакала
2 Шакала
Platform: taipanbyte | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-02 | Status: Solved Techniques: manual_cipher_implementation, sha256_compression_inversion, shacal2_round_reversal
Summary
Task: Two base64 strings given with task name hinting at SHACAL-2 cipher. Solution: Manually implemented SHACAL-2 decryption by reversing SHA-256 compression function rounds.
2 Шакала — TaipanByte CTF
Description
The task description contains two base64 strings:
hQwbBESeCxL6kuxU3XkI9BIJoMkL5ZbusVuDtaTPU/c=
Y2hpdGloYXRlc2hha2Fscw==
The task name "2 Шакала" (2 Jackals) is a direct hint to the SHACAL-2 algorithm (шакал → SHACAL in Russian). We need to understand that this is the SHACAL-2 block cipher, determine which string is the key and which is the ciphertext, and decrypt it.
Analysis
What is SHACAL-2
SHACAL-2 is a block cipher built on the SHA-256 compression function:
- Block size: 256 bits (32 bytes)
- Key size: 128–512 bits (padded with zeros to 512 bits / 64 bytes)
- Number of rounds: 64 (same as SHA-256)
- Encryption: forward pass of 64 rounds of SHA-256 compression function
- Decryption: reverse pass — algebraic inversion of each round
Problem with PyCryptodome
The Crypto.Cipher.SHACAL2 module is missing from the standard PyCryptodome installation:
ImportError: cannot import name 'SHACAL2' from 'Crypto.Cipher'
This means we need to implement the algorithm manually.
Decoding input data
python read-only • copy allowedimport base64 key_b64 = "Y2hpdGloYXRlc2hha2Fscw==" ct_b64 = "hQwbBESeCxL6kuxU3XkI9BIJoMkL5ZbusVuDtaTPU/c=" key = base64.b64decode(key_b64) # b'chitihateshakals' (16 bytes) ct = base64.b64decode(ct_b64) # 32 bytes
Solution
Step 1: Implementing SHACAL-2
SHACAL-2 uses the same primitives as SHA-256:
- Round constants K (64 total) — first 32 bits of the fractional parts of the cube roots of the first 64 prime numbers
- Functions: Sigma0, Sigma1 (for state), sigma0, sigma1 (for key schedule), Ch, Maj
- Key schedule: 16-byte key is padded with zeros to 64 bytes, split into 16 words of 32 bits, expanded to 64 words via sigma0/sigma1
Step 2: Decryption (round inversion)
In each SHA-256 round, state [a, b, c, d, e, f, g, h] is updated:
T1 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i]
T2 = Sigma0(a) + Maj(a, b, c)
new_state = [T1 + T2, a, b, c, d + T1, e, f, g]
For decryption, we iterate through rounds in reverse order (from 63 to 0) and recover the previous state:
# From current state [a, b, c, d, e, f, g, h]:
# old_a = b, old_b = c, old_c = d
# T1 = a - Sigma0(b) - Maj(b, c, d) (mod 2^32)
# old_d = e - T1 (mod 2^32)
# old_e = f, old_f = g, old_g = h
# old_h = T1 - Sigma1(f) - Ch(f, g, h) - K[i] - W[i] (mod 2^32)
Full solve script
python read-only • copy allowed#!/usr/bin/env python3 """SHACAL-2 ECB decryption — manual implementation""" import base64 import struct # SHA-256 round constants K = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ] M32 = 0xFFFFFFFF def rotr(x, n): return ((x >> n) | (x << (32 - n))) & M32 def shr(x, n): return x >> n def Sigma0(x): return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22) def Sigma1(x): return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25) def sigma0(x): return rotr(x, 7) ^ rotr(x, 18) ^ shr(x, 3) def sigma1(x): return rotr(x, 17) ^ rotr(x, 19) ^ shr(x, 10) def Ch(e, f, g): return (e & f) ^ (~e & g) & M32 def Maj(a, b, c): return (a & b) ^ (a & c) ^ (b & c) def key_schedule(key_bytes): """Expand key to 64 x 32-bit words.""" padded = key_bytes.ljust(64, b'\x00') W = list(struct.unpack('>16I', padded)) for i in range(16, 64): W.append((sigma1(W[i-2]) + W[i-7] + sigma0(W[i-15]) + W[i-16]) & M32) return W def shacal2_decrypt_block(block, W): """Decrypt one 256-bit block using SHACAL-2.""" state = list(struct.unpack('>8I', block)) a, b, c, d, e, f, g, h = state for i in range(63, -1, -1): T2 = (Sigma0(b) + Maj(b, c, d)) & M32 T1 = (a - T2) & M32 new_h = (T1 - Sigma1(f) - Ch(f, g, h) - K[i] - W[i]) & M32 new_a = b new_b = c new_c = d new_d = (e - T1) & M32 new_e = f new_f = g new_g = h a, b, c, d, e, f, g, h = new_a, new_b, new_c, new_d, new_e, new_f, new_g, new_h return struct.pack('>8I', a, b, c, d, e, f, g, h) # Input data key_b64 = "Y2hpdGloYXRlc2hha2Fscw==" ct_b64 = "hQwbBESeCxL6kuxU3XkI9BIJoMkL5ZbusVuDtaTPU/c=" key = base64.b64decode(key_b64) ct = base64.b64decode(ct_b64) W = key_schedule(key) plaintext = shacal2_decrypt_block(ct, W) # Remove PKCS7 padding pad_len = plaintext[-1] plaintext = plaintext[:-pad_len] print(f"Decrypted: {plaintext.decode()}") # Output: CTF{REDACTED}