Skip to content

Instantly share code, notes, and snippets.

@Riatre
Last active July 18, 2022 22:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Riatre/7db0a0e0a32f92e6404e0d7dc740d39b to your computer and use it in GitHub Desktop.
Save Riatre/7db0a0e0a32f92e6404e0d7dc740d39b to your computer and use it in GitHub Desktop.
Google CTF 2022 Whitecube

Whitecube

Summary

Whitecube is a reversing challenge from Google CTF 2022. The challenge hands out a encryption tool whitecube.exe (AMD64 PE), along with an encrypted flag file. It is implied that the players should figure out what the algorithm is by looking at the binary, then break/reverse the encryption.

The challenge description strongly hints at something GPU related and indeed, the main algorithm is implemented as a SPIR-V shader and potentially runs on a GPU.

The challenge took us about 6 hours to solve.

CPU Part

The challenge binary prints a nice usage message when invoked without arguments:

Z:\>whitecube.exe
Usage: whitecube.exe <input_file> <output_file>

Running the binary with an input file containing 1024 'A'-s, it outputs a 1044 bytes file with visible repeating patterns starting at offset 0x14. We can make an educated guess that the encrypted file starts with a 20 bytes header. Moreover, the first 8 bytes is a 64-bit integer 0x400 in little endian, our original file size. This leaves 12 unknown and seemingly random bytes in the header.

Loading the binary into IDA Pro, we spotted a few interesting strings:

  • Failed to init GLFW
  • GLSL.std.450
  • f5(mf44[16]);l1;

Which suggested that the challenge uses the GLFW library and contains an embedded GLSL shader. Since the challenge statically linked against GLFW, it would be helpful to make FLAIR signatures of the GLFW library so that we can avoid reversing library functions. After some investigations we realized that GLFW releases precompiled Windows binaries. Using pcf+sigmake to prepare a signature, we get 378 matches in IDA Pro, which makes the analysis a lot easier:

$ ~/lib/flair77/bin/linux/pcf glfw-3.3.7.bin.WIN64/lib-vc2022/glfw3_mt.lib
/home/riatre/ctf/googlectf22/re/whitecube/glfw-3.3.7.bin.WIN64/lib-vc2022/glfw3_mt.lib: skipped 6, total 333
$ ~/lib/flair77/bin/linux/sigmake glfw3_mt.pat glfw3_mt.sig
glfw3_mt.sig: modules/leaves: 241/326, COLLISIONS: 4
See the documentation to learn how to resolve collisions.
# Edit glfw3_mt.enc, just remove the first few lines.
$ ~/lib/flair77/bin/linux/sigmake glfw3_mt.pat glfw3_mt.sig
$ 

The main() logic is quite simple:

  1. Read the input file into a buffer. Check that input file size is less than $800 \times 640 \times 4$ = $2048000$ bytes. Write input size as a 64-bit little endian integer to output file.
  2. Initialize GLFW and create a pair of vertex and fragment shaders.
  3. Generate 12 random bytes with std::mt19937 and std::random_device. The 12 bytes goes directly into the output file. It then concatenates [1, 1, 1, 1] to the 12 items and builds a 16x16 diagonal matrix out of it. Then binds the matrix to shader variable u_nonce.
  4. Transpose input in 4x4 blocks, bind the input file to a shader buffer and render.
  5. Copy the rendered pixels back to shader buffer and render. Repeat for 7 times.
  6. Write the final rendered pixels to the output file.

Note that the 16x16 matrix has a weird layout in memory, it was saved as 16 row-major 4x4 matrices, where the 16 matrices itself are also arranged in 4x4 row-major order. We'll see why later.

It is clear that actual transformation logic is in the shader code, time to dive in.

GPU Part

Dump the shader binary, file told me that they are SPIR-V bytecodes:

$ file fragment_shader.bin
fragment_shader.bin: Khronos SPIR-V binary, little-endian, version 0x010000, generator 0x08000a

Searching the Internet we found a nice SPIR-V decompiler, which gives very readable decompilation. With this, it is not hard to understand what happens in the shader. For data types, see the GLSL wiki. Here is a summary:

  • mat4: 4x4 float32 matrix in column-major order
  • vec4: 4-component float32 vector
  • uvec2: 2-component unsigned integer vector

The vertex shader is empty, it simply copies the input to the output. We can ignore it from now.

The fragment shader is a bit more complex, it contains a reasonably long main() and 8 helper functions. Let's start with the helper functions. In general, they work on 16x16 matrices in 16 mat4 block matrix layout. In our familiar numpy term, they are:

  1. f6(dst, src) simply copies 16 mat4 from mat4 src[16] to mat4 dst[16]. In other words it is just dst = src.
  2. f4(data, val) fills the matrix data with val, i.e. data = np.full((16, 16), val).
  3. f5(data) mods each element by 256.
  4. f3(res, a, b) is quite long, but after some cleanup it could look like:
void f3(inout mat4 res[16], mat4 a[16], mat4 b[16]) {
  res[ 0] = a[ 0] * b[ 0] + a[ 1] * b[ 4] + a[ 2] * b[ 8] + a[ 3] * b[12];
  res[ 1] = a[ 0] * b[ 1] + a[ 1] * b[ 5] + a[ 2] * b[ 9] + a[ 3] * b[13];
  res[ 2] = a[ 0] * b[ 2] + a[ 1] * b[ 6] + a[ 2] * b[10] + a[ 3] * b[14];
  // ...

it is a matrix multiplication on block matrices, i.e. res = a @ b. 5. f0(dst, m, k): dst = (intclip(126.0 * (1.0 + np.sin(m))) % 256) @ k 6. f1(dst, m, k): dst = (intclip(126.0 * (1.0 + np.cos(m))) % 256) @ k 7. f2(dst, m, k): dst = (intclip(126.0 * (1.0 + np.tan((m - 127.0) / 256.0))) % 256) @ k 8. f7(dst, src): dst += src

With these in mind, it isn't hard to figure out what the main() does. The "GPU computation model" applied here is simple: each main() call generates one single output pixel, written to FragColor, at coordinate (gl_FragCoord.x, gl_FragCoord.y). It actually does a lot of wasteful work, computing the entire 16x16 matrix the output pixel located in, only for a single 4 colors group of the current pixel. With that said, the logic reimplemented in Python looks like:

# Assume that block is in flatten bytestream order
def process(block: np.ndarray, block_idx: int, u_nonce):
    assert block.dtype == np.uint8
    assert block.shape == (1024,)

    # Whitecube.exe passes transposed matrix to the shader, so we are good to just reshape
    # in row-major order
    block = block.reshape(4, 16, 4, 4).astype(np.float32)
    nonce = u_nonce.copy()
    nonce[15, 0, 0] = 2 * (block_idx + 1) - 1
    nonce = convert_bmat_to_mat(nonce)

    FUNC = [
        lambda x: f0(x, PARAM_6),
        lambda x: f1(x, PARAM_9),
        lambda x: f2(x, PARAM_12),
        lambda x: x,
    ]
    result = []
    for subblock_idx in range(4):
        if subblock_idx != 3:
            bias = block[subblock_idx]
        else:
            bias = np.zeros_like(block[0])
        bias = convert_bmat_to_mat(bias)
        bias = FUNC[subblock_idx](bias)
        x = convert_bmat_to_mat(block[(subblock_idx+1)%4])
        x = (x + bias % 256) % 256
        x = (x @ nonce) % 256
        x = (x @ PARAM_23) % 256
        x = convert_mat_to_bmat(x)
        result.append(x.reshape(-1))
    return np.concatenate(result).astype(np.uint8)

Solution

With the u_nonce known and fixed, the process() function is clearly reversible. Simply run its inverse 8 times recovered the flag.

Note that there are more than 800*640*4 bytes in the encrypted flag file, which the Whitecube.exe refuses to encrypt. We can assume that the remaining blocks works the same.

See solve.ipynb for the full solution code.

import numpy as np
PARAM_6 = np.array(
[
(
(102.0, 148.0, 54.0, 11.0),
(113.0, 233.0, 219.0, 96.0),
(196.0, 70.0, 128.0, 97.0),
(7.0, 143.0, 123.0, 211.0),
),
(
(244.0, 136.0, 121.0, 170.0),
(112.0, 186.0, 107.0, 182.0),
(241.0, 255.0, 97.0, 18.0),
(233.0, 243.0, 247.0, 13.0),
),
(
(88.0, 127.0, 108.0, 173.0),
(31.0, 8.0, 220.0, 183.0),
(2.0, 167.0, 119.0, 47.0),
(14.0, 135.0, 210.0, 34.0),
),
(
(0.0, 162.0, 51.0, 235.0),
(36.0, 59.0, 108.0, 40.0),
(198.0, 130.0, 199.0, 106.0),
(127.0, 23.0, 35.0, 63.0),
),
(
(214.0, 22.0, 220.0, 102.0),
(195.0, 88.0, 98.0, 195.0),
(54.0, 194.0, 105.0, 161.0),
(211.0, 111.0, 122.0, 191.0),
),
(
(116.0, 29.0, 140.0, 27.0),
(120.0, 3.0, 90.0, 76.0),
(111.0, 86.0, 110.0, 83.0),
(129.0, 78.0, 71.0, 206.0),
),
(
(204.0, 57.0, 106.0, 196.0),
(180.0, 96.0, 139.0, 10.0),
(127.0, 52.0, 56.0, 91.0),
(130.0, 149.0, 200.0, 38.0),
),
(
(57.0, 138.0, 146.0, 127.0),
(254.0, 242.0, 98.0, 235.0),
(96.0, 115.0, 223.0, 233.0),
(214.0, 115.0, 8.0, 194.0),
),
(
(218.0, 11.0, 116.0, 135.0),
(150.0, 40.0, 241.0, 236.0),
(76.0, 128.0, 42.0, 87.0),
(9.0, 62.0, 32.0, 133.0),
),
(
(115.0, 73.0, 201.0, 105.0),
(138.0, 22.0, 234.0, 169.0),
(165.0, 221.0, 138.0, 225.0),
(112.0, 157.0, 200.0, 38.0),
),
(
(249.0, 80.0, 72.0, 162.0),
(84.0, 145.0, 190.0, 189.0),
(221.0, 143.0, 26.0, 114.0),
(18.0, 69.0, 211.0, 230.0),
),
(
(245.0, 158.0, 15.0, 44.0),
(168.0, 14.0, 248.0, 50.0),
(254.0, 8.0, 12.0, 192.0),
(24.0, 191.0, 174.0, 32.0),
),
(
(243.0, 166.0, 79.0, 32.0),
(198.0, 115.0, 254.0, 55.0),
(4.0, 160.0, 137.0, 201.0),
(13.0, 124.0, 57.0, 238.0),
),
(
(250.0, 75.0, 137.0, 218.0),
(27.0, 84.0, 10.0, 136.0),
(194.0, 102.0, 33.0, 98.0),
(236.0, 132.0, 28.0, 2.0),
),
(
(219.0, 219.0, 48.0, 232.0),
(73.0, 211.0, 252.0, 225.0),
(239.0, 229.0, 164.0, 246.0),
(179.0, 147.0, 139.0, 176.0),
),
(
(44.0, 155.0, 91.0, 224.0),
(10.0, 182.0, 78.0, 87.0),
(22.0, 255.0, 202.0, 149.0),
(84.0, 15.0, 47.0, 16.0),
),
],
dtype=np.float32,
)
PARAM_9 = np.array(
(
(
(102.0, 85.0, 19.0, 195.0),
(140.0, 247.0, 54.0, 25.0),
(9.0, 246.0, 179.0, 52.0),
(159.0, 186.0, 204.0, 57.0),
),
(
(146.0, 201.0, 231.0, 2.0),
(171.0, 161.0, 63.0, 78.0),
(34.0, 209.0, 147.0, 121.0),
(115.0, 206.0, 254.0, 112.0),
),
(
(44.0, 220.0, 249.0, 115.0),
(3.0, 57.0, 250.0, 38.0),
(165.0, 214.0, 62.0, 183.0),
(41.0, 223.0, 41.0, 194.0),
),
(
(149.0, 3.0, 230.0, 174.0),
(212.0, 115.0, 47.0, 244.0),
(189.0, 34.0, 50.0, 101.0),
(106.0, 169.0, 2.0, 130.0),
),
(
(203.0, 40.0, 210.0, 103.0),
(32.0, 174.0, 27.0, 100.0),
(231.0, 168.0, 203.0, 105.0),
(58.0, 129.0, 52.0, 110.0),
),
(
(231.0, 167.0, 103.0, 212.0),
(158.0, 125.0, 166.0, 164.0),
(156.0, 216.0, 72.0, 241.0),
(217.0, 234.0, 142.0, 62.0),
),
(
(49.0, 78.0, 76.0, 16.0),
(33.0, 143.0, 91.0, 176.0),
(90.0, 78.0, 150.0, 111.0),
(94.0, 129.0, 233.0, 221.0),
),
(
(14.0, 212.0, 128.0, 18.0),
(65.0, 246.0, 105.0, 41.0),
(176.0, 227.0, 96.0, 190.0),
(61.0, 204.0, 173.0, 201.0),
),
(
(17.0, 237.0, 24.0, 111.0),
(110.0, 169.0, 96.0, 146.0),
(143.0, 212.0, 199.0, 40.0),
(237.0, 124.0, 221.0, 36.0),
),
(
(125.0, 199.0, 26.0, 199.0),
(200.0, 11.0, 76.0, 39.0),
(159.0, 159.0, 109.0, 160.0),
(26.0, 201.0, 55.0, 26.0),
),
(
(76.0, 204.0, 245.0, 147.0),
(66.0, 213.0, 171.0, 245.0),
(10.0, 17.0, 230.0, 49.0),
(1.0, 23.0, 224.0, 151.0),
),
(
(144.0, 161.0, 7.0, 84.0),
(11.0, 27.0, 13.0, 32.0),
(136.0, 71.0, 28.0, 154.0),
(254.0, 167.0, 75.0, 12.0),
),
(
(123.0, 248.0, 247.0, 60.0),
(201.0, 90.0, 195.0, 245.0),
(191.0, 43.0, 225.0, 14.0),
(240.0, 137.0, 147.0, 192.0),
),
(
(65.0, 103.0, 198.0, 54.0),
(218.0, 174.0, 170.0, 134.0),
(71.0, 5.0, 113.0, 222.0),
(185.0, 239.0, 213.0, 201.0),
),
(
(214.0, 54.0, 42.0, 187.0),
(3.0, 68.0, 16.0, 146.0),
(182.0, 58.0, 32.0, 101.0),
(80.0, 50.0, 60.0, 57.0),
),
(
(1.0, 146.0, 133.0, 202.0),
(2.0, 243.0, 93.0, 45.0),
(27.0, 75.0, 4.0, 75.0),
(128.0, 175.0, 63.0, 247.0),
),
),
dtype=np.float32,
)
PARAM_12 = np.array(
(
(
(212.0, 51.0, 251.0, 31.0),
(136.0, 37.0, 62.0, 3.0),
(54.0, 242.0, 184.0, 241.0),
(35.0, 228.0, 61.0, 203.0),
),
(
(164.0, 217.0, 127.0, 198.0),
(44.0, 235.0, 143.0, 129.0),
(202.0, 154.0, 239.0, 95.0),
(26.0, 242.0, 110.0, 175.0),
),
(
(87.0, 220.0, 116.0, 20.0),
(50.0, 40.0, 147.0, 16.0),
(22.0, 2.0, 99.0, 232.0),
(190.0, 147.0, 185.0, 61.0),
),
(
(80.0, 99.0, 165.0, 180.0),
(248.0, 130.0, 48.0, 48.0),
(43.0, 116.0, 182.0, 73.0),
(96.0, 211.0, 61.0, 117.0),
),
(
(106.0, 160.0, 232.0, 156.0),
(177.0, 234.0, 3.0, 191.0),
(73.0, 113.0, 72.0, 34.0),
(154.0, 60.0, 94.0, 168.0),
),
(
(246.0, 83.0, 169.0, 21.0),
(180.0, 197.0, 92.0, 249.0),
(203.0, 39.0, 154.0, 91.0),
(131.0, 70.0, 109.0, 234.0),
),
(
(56.0, 245.0, 82.0, 175.0),
(133.0, 53.0, 141.0, 91.0),
(9.0, 123.0, 29.0, 145.0),
(105.0, 248.0, 183.0, 22.0),
),
(
(178.0, 206.0, 43.0, 128.0),
(45.0, 189.0, 36.0, 18.0),
(112.0, 175.0, 125.0, 252.0),
(213.0, 62.0, 113.0, 237.0),
),
(
(49.0, 0.0, 254.0, 128.0),
(120.0, 152.0, 100.0, 73.0),
(65.0, 23.0, 255.0, 44.0),
(68.0, 115.0, 9.0, 219.0),
),
(
(99.0, 250.0, 175.0, 18.0),
(30.0, 161.0, 7.0, 179.0),
(225.0, 142.0, 98.0, 92.0),
(200.0, 94.0, 212.0, 220.0),
),
(
(96.0, 156.0, 68.0, 49.0),
(215.0, 136.0, 67.0, 45.0),
(215.0, 186.0, 152.0, 47.0),
(140.0, 73.0, 176.0, 179.0),
),
(
(239.0, 206.0, 117.0, 93.0),
(177.0, 39.0, 194.0, 78.0),
(66.0, 221.0, 150.0, 147.0),
(78.0, 76.0, 25.0, 139.0),
),
(
(43.0, 90.0, 117.0, 187.0),
(117.0, 108.0, 232.0, 204.0),
(101.0, 62.0, 25.0, 214.0),
(50.0, 83.0, 43.0, 151.0),
),
(
(172.0, 96.0, 136.0, 109.0),
(167.0, 8.0, 33.0, 79.0),
(103.0, 251.0, 180.0, 214.0),
(33.0, 116.0, 141.0, 11.0),
),
(
(117.0, 123.0, 161.0, 60.0),
(79.0, 104.0, 82.0, 76.0),
(149.0, 32.0, 56.0, 160.0),
(173.0, 112.0, 174.0, 184.0),
),
(
(36.0, 81.0, 20.0, 81.0),
(165.0, 159.0, 126.0, 171.0),
(52.0, 14.0, 5.0, 126.0),
(176.0, 175.0, 99.0, 88.0),
),
),
dtype=np.float32,
)
PARAM_23 = np.array(
(
(
(97.0, 15.0, 52.0, 185.0),
(99.0, 64.0, 43.0, 201.0),
(230.0, 107.0, 122.0, 236.0),
(168.0, 189.0, 150.0, 145.0),
),
(
(121.0, 45.0, 186.0, 66.0),
(240.0, 74.0, 206.0, 29.0),
(47.0, 250.0, 37.0, 174.0),
(200.0, 253.0, 0.0, 53.0),
),
(
(161.0, 19.0, 128.0, 228.0),
(90.0, 170.0, 155.0, 169.0),
(131.0, 8.0, 122.0, 40.0),
(166.0, 187.0, 62.0, 167.0),
),
(
(230.0, 32.0, 19.0, 18.0),
(241.0, 213.0, 243.0, 81.0),
(25.0, 62.0, 171.0, 232.0),
(229.0, 152.0, 163.0, 71.0),
),
(
(66.0, 249.0, 233.0, 93.0),
(230.0, 166.0, 237.0, 203.0),
(63.0, 197.0, 230.0, 103.0),
(241.0, 197.0, 238.0, 85.0),
),
(
(38.0, 103.0, 159.0, 162.0),
(48.0, 157.0, 174.0, 218.0),
(99.0, 64.0, 119.0, 170.0),
(15.0, 65.0, 30.0, 75.0),
),
(
(9.0, 141.0, 21.0, 74.0),
(87.0, 223.0, 0.0, 176.0),
(218.0, 248.0, 1.0, 89.0),
(129.0, 4.0, 169.0, 6.0),
),
(
(155.0, 143.0, 94.0, 179.0),
(49.0, 102.0, 195.0, 35.0),
(127.0, 140.0, 55.0, 230.0),
(38.0, 83.0, 199.0, 159.0),
),
(
(32.0, 79.0, 97.0, 239.0),
(220.0, 107.0, 170.0, 220.0),
(26.0, 46.0, 84.0, 148.0),
(42.0, 107.0, 117.0, 50.0),
),
(
(174.0, 213.0, 71.0, 205.0),
(150.0, 19.0, 248.0, 198.0),
(139.0, 249.0, 64.0, 220.0),
(21.0, 96.0, 117.0, 218.0),
),
(
(150.0, 237.0, 154.0, 21.0),
(129.0, 60.0, 63.0, 208.0),
(159.0, 253.0, 153.0, 133.0),
(80.0, 110.0, 246.0, 174.0),
),
(
(250.0, 138.0, 193.0, 41.0),
(190.0, 15.0, 103.0, 64.0),
(185.0, 18.0, 4.0, 96.0),
(91.0, 208.0, 54.0, 176.0),
),
(
(219.0, 240.0, 70.0, 191.0),
(149.0, 203.0, 33.0, 255.0),
(251.0, 104.0, 34.0, 204.0),
(120.0, 165.0, 248.0, 54.0),
),
(
(143.0, 0.0, 218.0, 117.0),
(212.0, 146.0, 38.0, 102.0),
(45.0, 51.0, 194.0, 158.0),
(108.0, 25.0, 61.0, 149.0),
),
(
(157.0, 83.0, 170.0, 85.0),
(128.0, 18.0, 117.0, 154.0),
(77.0, 41.0, 178.0, 85.0),
(54.0, 96.0, 15.0, 118.0),
),
(
(191.0, 254.0, 38.0, 124.0),
(239.0, 246.0, 1.0, 16.0),
(51.0, 82.0, 53.0, 246.0),
(2.0, 181.0, 181.0, 196.0),
),
),
dtype=np.float32,
)
def intclip(arr):
return arr.astype(np.int32).astype(np.float32)
def convert_bmat_to_mat(bmat: np.ndarray):
assert bmat.shape == (16, 4, 4)
return np.block(
[
[bmat[0], bmat[1], bmat[2], bmat[3]],
[bmat[4], bmat[5], bmat[6], bmat[7]],
[bmat[8], bmat[9], bmat[10], bmat[11]],
[bmat[12], bmat[13], bmat[14], bmat[15]],
]
)
def convert_mat_to_bmat(mat: np.ndarray):
assert mat.shape == (16, 16)
return np.stack(
[
mat[:4, :4],
mat[:4, 4:8],
mat[:4, 8:12],
mat[:4, 12:16],
mat[4:8, :4],
mat[4:8, 4:8],
mat[4:8, 8:12],
mat[4:8, 12:16],
mat[8:12, :4],
mat[8:12, 4:8],
mat[8:12, 8:12],
mat[8:12, 12:16],
mat[12:16, :4],
mat[12:16, 4:8],
mat[12:16, 8:12],
mat[12:16, 12:16],
],
axis=0,
)
PARAM_6 = convert_bmat_to_mat(np.transpose(PARAM_6, axes=(0, 2, 1)))
PARAM_9 = convert_bmat_to_mat(np.transpose(PARAM_9, axes=(0, 2, 1)))
PARAM_12 = convert_bmat_to_mat(np.transpose(PARAM_12, axes=(0, 2, 1)))
PARAM_23 = convert_bmat_to_mat(np.transpose(PARAM_23, axes=(0, 2, 1)))
def f6(src):
return src
def f4(val):
return np.full((16, 16), val, dtype=np.float32)
def f5(mat):
assert mat.shape == (16, 16)
return mat % 256
def f3(a, b):
return a @ b
def f0(m, k):
# tmp = (126.0 * (1.0 + np.sin(m))).astype(np.int32).astype(np.float32)
# # tmp = f5(tmp)
# tmp = tmp % 256
# # return f3(tmp, k)
# return tmp @ k
assert m.shape == (16, 16)
assert k.shape == (16, 16)
return (intclip(126.0 * (1.0 + np.sin(m))) % 256) @ k
def f1(m, k):
assert m.shape == (16, 16)
assert k.shape == (16, 16)
return (intclip(126.0 * (1.0 + np.cos(m))) % 256) @ k
def f2(m, k):
assert m.shape == (16, 16)
assert k.shape == (16, 16)
return (intclip(126.0 * (1.0 + np.tan((m - 127.0) / 256.0))) % 256) @ k
def f7(dst, src):
assert dst.shape == (16, 16)
assert src.shape == (16, 16)
dst += src
from sage.all import *
import numpy as np
from common import *
# Assume that block is in flatten bytestream order
def process(block: np.ndarray, block_idx: int, u_nonce):
assert block.dtype == np.uint8
assert block.shape == (1024,)
# Whitecube.exe passes transposed matrix to the shader, so we are good to just reshape
# in row-major order
block = block.reshape(4, 16, 4, 4).astype(np.float32)
nonce = u_nonce.copy()
nonce[15, 0, 0] = 2 * (block_idx + 1) - 1
nonce = convert_bmat_to_mat(nonce)
FUNC = [
lambda x: f0(x, PARAM_6),
lambda x: f1(x, PARAM_9),
lambda x: f2(x, PARAM_12),
lambda x: x,
]
result = []
for subblock_idx in range(4):
if subblock_idx != 3:
bias = block[subblock_idx]
else:
bias = np.zeros_like(block[0])
bias = convert_bmat_to_mat(bias)
bias = FUNC[subblock_idx](bias)
x = convert_bmat_to_mat(block[(subblock_idx+1)%4])
x = (x + bias % 256) % 256
x = (x @ nonce) % 256
x = (x @ PARAM_23) % 256
x = convert_mat_to_bmat(x)
result.append(x.reshape(-1))
return np.concatenate(result).astype(np.uint8)
def inv_process(block: np.ndarray, block_idx: int, u_nonce):
assert block.dtype == np.uint8
assert block.shape == (1024,)
block = block.reshape(4, 16, 4, 4).astype(np.float32)
nonce = u_nonce.copy()
nonce[15, 0, 0] = 2 * (block_idx + 1) - 1
nonce = convert_bmat_to_mat(nonce)
FUNC = [
lambda x: f0(x, PARAM_6),
lambda x: f1(x, PARAM_9),
lambda x: f2(x, PARAM_12),
lambda x: x,
]
plain = np.zeros_like(block)
for subblock_idx in [3, 0, 1, 2]:
if subblock_idx != 3:
bias = plain[subblock_idx]
else:
bias = np.zeros_like(block[0])
bias = convert_bmat_to_mat(bias)
bias = FUNC[subblock_idx](bias)
x = matrix(Zmod(256), convert_bmat_to_mat(block[subblock_idx]))
x *= matrix(Zmod(256), PARAM_23).inverse()
x *= matrix(Zmod(256), nonce).inverse()
x -= matrix(Zmod(256), bias)
plain[(subblock_idx+1)%4] = convert_mat_to_bmat(np.array(x, dtype=np.float32))
return plain.flatten().astype(np.uint8)
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment