← Back to Writeups
CUSTOMMediumCrypto

2 Шакала

XESXOR9/2/20264 min read
#crypto#custom#medium#manual_cipher_implementation#sha256_compression_inversion#shacal2_round_reversal

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
import 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
#!/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}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍