RE
SUDOKU
A small game for warm-up.
Author: dream02
Solution:
Đây là bài warmup nên chỉ cần nhìn kĩ là có thể giải được. Script của mình:
import struct
flag_enc = [0] * 86
cnt =0
rand_thing = [0x160e5b19, 0x67e9d105, 0x2e4a4f3a, 0x16b29bd3, 0x40969be4, 0x6f073098, 0x2a84b45d, 0x2065397c, 0x44aa7467, 0x350dc573, 0x431da129, 0x73220c12, 0x7878aa21, 0x7583038e, 0x8d47d33, 0x37134ed3, 0x68a7ee11, 0x17ae84e0, 0x25052828, 0x3153e88e, 0x426c6991, 0x6c130059, 0x45e69a01, 0x15057c4f, 0x7daea889, 0x59ecfe7b, 0x3a1d6705, 0x21081ec8, 0xfb63fd1, 0x5a4d3fad, 0x47644ec4, 0x771890c9, 0x6b948f4f, 0x79ef9e28, 0x4f0236dd, 0x31154387, 0x631004b3, 0x2d56fba9, 0x5789c355, 0x7165e499, 0x1caf2de2, 0x64055501, 0x3dee7436, 0x13edc81e, 0x11ac700c, 0x504a70e3, 0x167cd925, 0x252b409f, 0x3ed8ab8f, 0x5d9ee3c, 0xf86247f, 0x1b682edf, 0x51806b8a, 0x13889b53, 0x10c1b49d, 0x40c38862, 0x46ad0a1e, 0x6194ab2f, 0x2971c45c, 0x34c19984, 0x2b1d48b2, 0xd332270, 0xc93b900, 0x6cc85ef4, 0x1d0b3c83, 0x41df1296, 0x6a39490e, 0x692952b9, 0x40433d3c, 0x2580412c, 0x376d6485, 0x61e6f660, 0x497a82e9, 0x169d64d2, 0x55d59814, 0x52c80a72, 0x56debfeb, 0x6317de1e, 0x168d3af4, 0x3367e472, 0x142063f3, 0x4c111f7f, 0x834aeee, 0x10d30af1, 0x73cf152e]
target = [243, 17, 143, 220, 220, 75, 177, 0, 142, 223, 45, 89, 149, 83, 7, 96, 89, 41, 18, 74, 83, 225, 135, 7, 96, 181, 89, 159, 29, 46, 141, 197, 144, 20, 141, 97, 89, 109, 27, 247, 168, 95, 227, 215, 164, 109, 162, 94, 177, 255, 226, 225, 141, 161, 209, 103, 94, 243, 6, 16, 0, 54, 123, 164, 55, 135, 227, 135, 6, 89, 186, 123, 57, 190, 16, 115, 77, 225, 243, 179, 82, 65, 58, 179, 54]
with open('flag.enc', 'rb') as file:
while True:
data = file.read(4)
if not data:
break
result = struct.unpack('<I', data)[0]
flag_enc[cnt] = result
cnt+=1
rand = [0] * 85
for i in range(85):
rand[i] = flag_enc[i+1] ^ rand_thing[i]
for i in range(len(rand)):
print(chr(rand[i] ^ target[i]),end="")
EBPF
Note: Run with sudo (not infected).
Author: Jinn
Solution
Đây là chương trình ebpf, để tiếp cận được tới chương trình chính thì không thể chỉ đọc bằng ida.
The Just-in-Time (JIT) compilation step translates the generic bytecode of the program into the machine specific instruction set to optimize execution speed of the program. This makes eBPF programs run as efficiently as natively compiled kernel code or as code loaded as a kernel module.
Có hai cách đó là debug tới khi thấy bytecode của chương trình sau đó dùng objdump
để dump ra instructions. Ở đây thì mình sử dụng bpftool
để có thể disassemble instructions của chương trình.
Đầu tiên reverse sơ qua flow chương trình chưa tính mạch chính là của ebpf, ta thấy chương trình yêu cầu nhập flag độ dài 56 sau đó load từng 4 kí tự vào bpf_map_update_elem()
for ( i = 0; v19 > i; i += 4 )
{
key = 0;
if ( bpf_map_update_elem(fd, &key, &input[i], BPF_ANY) )
__assert_fail(
"bpf_map_update_elem(map_fd, &key, &buf[i], BPF_ANY) == 0",
"chall.c",
0x103u,
"chall");
key = 1;
if ( bpf_map_update_elem(fd, &key, &input[i + 1], BPF_ANY) )
__assert_fail(
"bpf_map_update_elem(map_fd, &key, &buf[i + 1], BPF_ANY) == 0",
"chall.c",
0x105u,
"chall");
key = 2;
if ( bpf_map_update_elem(fd, &key, &input[i + 2], BPF_ANY) )
__assert_fail(
"bpf_map_update_elem(map_fd, &key, &buf[i + 2], BPF_ANY) == 0",
"chall.c",
0x107u,
"chall");
key = 3;
if ( bpf_map_update_elem(fd, &key, &input[i + 3], BPF_ANY) )
__assert_fail(
"bpf_map_update_elem(map_fd, &key, &buf[i + 3], BPF_ANY) == 0",
"chall.c",
0x109u,
"chall");
trigger_bpf_program();
key = 0; // save
if ( bpf_map_lookup_elem(fd, &key, &input[i]) )
__assert_fail("bpf_map_lookup_elem(map_fd, &key, &buf[i]) == 0", "chall.c", 0x10Eu, "chall");
key = 1;
if ( bpf_map_lookup_elem(fd, &key, &input[i + 1]) )
__assert_fail("bpf_map_lookup_elem(map_fd, &key, &buf[i + 1]) == 0", "chall.c", 0x110u, "chall");
key = 2;
if ( bpf_map_lookup_elem(fd, &key, &input[i + 2]) )
__assert_fail("bpf_map_lookup_elem(map_fd, &key, &buf[i + 2]) == 0", "chall.c", 0x112u, "chall");
key = 3;
if ( bpf_map_lookup_elem(fd, &key, &input[i + 3]) )
__assert_fail("bpf_map_lookup_elem(map_fd, &key, &buf[i + 3]) == 0", "chall.c", 0x114u, "chall");
}
if ( !memcmp(input, &enc, 0xE0uLL) )
puts("That's flag!");
else
puts("Nope!");
Sau khi được thực hiện biến đổi thì nó check với mảng enc
. Nếu đúng thì sẽ in ra That's flag!
. Quay lại với mạch chính là làm sao để disassemble được instructions thì mình đặt breakpoint sau khi chương trình chạy hàm bpf_load_program
.
optval = bpf_load_program(1LL, &v22, 133LL, &off_556EE4F7A0D6, 0LL, &bpf_log_buf, 0xFFFFFFLL);
if ( optval >= 0 )
Trước khi debug thì kiểm tra xem có những chương trình ebpf nào đang chạy bằng sudo bpftool prog

Sau đó bắt đầu debug, ta thấy được có thêm một chương trình id : 14
, đó là chương trình ta đang cần tìm, dump disassemble code của chương trình nó bằng sudo bpftool prog dump xlated id 14

0: (bf) r9 = r1
1: (18) r1 = map[id:2]
3: (bf) r2 = r10
4: (07) r2 += -4
5: (62) *(u32 *)(r10 -4) = 0
6: (07) r1 += 272
7: (61) r0 = *(u32 *)(r2 +0)
8: (35) if r0 >= 0x100 goto pc+3
9: (67) r0 <<= 3
10: (0f) r0 += r1
11: (05) goto pc+1
12: (b7) r0 = 0
13: (15) if r0 == 0x0 goto pc+14
14: (61) r5 = *(u32 *)(r0 +0)
15: (63) *(u32 *)(r10 -20) = r5
16: (18) r1 = map[id:2]
18: (bf) r2 = r10
19: (07) r2 += -4
20: (62) *(u32 *)(r10 -4) = 1
21: (07) r1 += 272
22: (61) r0 = *(u32 *)(r2 +0)
23: (35) if r0 >= 0x100 goto pc+3
24: (67) r0 <<= 3
25: (0f) r0 += r1
26: (05) goto pc+1
27: (b7) r0 = 0
28: (15) if r0 == 0x0 goto pc+14
29: (61) r5 = *(u32 *)(r0 +0)
30: (63) *(u32 *)(r10 -24) = r5
31: (18) r1 = map[id:2]
33: (bf) r2 = r10
34: (07) r2 += -4
35: (62) *(u32 *)(r10 -4) = 2
36: (07) r1 += 272
37: (61) r0 = *(u32 *)(r2 +0)
38: (35) if r0 >= 0x100 goto pc+3
39: (67) r0 <<= 3
40: (0f) r0 += r1
41: (05) goto pc+1
42: (b7) r0 = 0
43: (15) if r0 == 0x0 goto pc+14
44: (61) r5 = *(u32 *)(r0 +0)
45: (63) *(u32 *)(r10 -28) = r5
46: (18) r1 = map[id:2]
48: (bf) r2 = r10
49: (07) r2 += -4
50: (62) *(u32 *)(r10 -4) = 3
51: (07) r1 += 272
52: (61) r0 = *(u32 *)(r2 +0)
53: (35) if r0 >= 0x100 goto pc+3
54: (67) r0 <<= 3
55: (0f) r0 += r1
56: (05) goto pc+1
57: (b7) r0 = 0
58: (15) if r0 == 0x0 goto pc+96
59: (61) r5 = *(u32 *)(r0 +0)
60: (63) *(u32 *)(r10 -32) = r5
61: (61) r0 = *(u32 *)(r10 -20)
62: (61) r1 = *(u32 *)(r10 -24)
63: (61) r2 = *(u32 *)(r10 -28)
64: (61) r3 = *(u32 *)(r10 -32)
65: (af) r0 ^= r1
66: (5f) r2 &= r3
67: (0f) r0 += r2
68: (57) r0 &= 255
69: (63) *(u32 *)(r10 -36) = r0
70: (61) r0 = *(u32 *)(r10 -20)
71: (61) r2 = *(u32 *)(r10 -28)
72: (af) r2 ^= r3
73: (af) r0 ^= r1
74: (0f) r0 += r2
75: (57) r0 &= 255
76: (63) *(u32 *)(r10 -48) = r0
77: (61) r0 = *(u32 *)(r10 -20)
78: (61) r2 = *(u32 *)(r10 -28)
79: (bf) r4 = r0
80: (1f) r4 -= r2
81: (2f) r2 *= r3
82: (af) r4 ^= r2
83: (0f) r0 += r1
84: (bf) r5 = r0
85: (bf) r6 = r0
86: (67) r5 <<= 5
87: (57) r5 &= 255
88: (57) r6 &= 255
89: (77) r6 >>= 3
90: (4f) r5 |= r6
91: (af) r4 ^= r5
92: (57) r4 &= 255
93: (63) *(u32 *)(r10 -40) = r4
94: (61) r0 = *(u32 *)(r10 -20)
95: (61) r2 = *(u32 *)(r10 -28)
96: (bf) r4 = r1
97: (2f) r4 *= r3
98: (0f) r1 += r2
99: (af) r4 ^= r1
100: (0f) r0 += r2
101: (bf) r5 = r0
102: (bf) r6 = r0
103: (67) r5 <<= 4
104: (57) r5 &= 255
105: (57) r6 &= 255
106: (77) r6 >>= 4
107: (4f) r5 |= r6
108: (af) r4 ^= r5
109: (57) r4 &= 255
110: (63) *(u32 *)(r10 -44) = r4
111: (18) r1 = map[id:2]
113: (62) *(u32 *)(r10 -12) = 0
114: (bf) r2 = r10
115: (07) r2 += -12
116: (61) r5 = *(u32 *)(r10 -36)
117: (7b) *(u64 *)(r10 -8) = r5
118: (bf) r3 = r10
119: (07) r3 += -8
120: (b7) r4 = 0
121: (85) call array_map_update_elem#175408
122: (18) r1 = map[id:2]
124: (62) *(u32 *)(r10 -12) = 1
125: (bf) r2 = r10
126: (07) r2 += -12
127: (61) r5 = *(u32 *)(r10 -40)
128: (7b) *(u64 *)(r10 -8) = r5
129: (bf) r3 = r10
130: (07) r3 += -8
131: (b7) r4 = 0
132: (85) call array_map_update_elem#175408
133: (18) r1 = map[id:2]
135: (62) *(u32 *)(r10 -12) = 2
136: (bf) r2 = r10
137: (07) r2 += -12
138: (61) r5 = *(u32 *)(r10 -44)
139: (7b) *(u64 *)(r10 -8) = r5
140: (bf) r3 = r10
141: (07) r3 += -8
142: (b7) r4 = 0
143: (85) call array_map_update_elem#175408
144: (18) r1 = map[id:2]
146: (62) *(u32 *)(r10 -12) = 3
147: (bf) r2 = r10
148: (07) r2 += -12
149: (61) r5 = *(u32 *)(r10 -48)
150: (7b) *(u64 *)(r10 -8) = r5
151: (bf) r3 = r10
152: (07) r3 += -8
153: (b7) r4 = 0
154: (85) call array_map_update_elem#175408
155: (b7) r0 = 0
156: (95) exit
Có thể thấy nó có liên quan tới map[id:2]
, ta có thể xem giá trị nó là gì bằng sudo bpftool map dump id 2
, tuy nhiên lúc này tất cả đều là 0. Lý do là ta phải debug qua hàm bpf_map_update_elem()
, sau đó ta thấy nó load 4 kí tự vào chương trình.

Sau đó, nó được lưu lại mảng input
sau khi thực hiện chương trình.

Vậy thì lúc này ta đã hiểu sơ nó thực hiện nhận và cần biết nó làm gì đó để có thể kiếm được flag. Quay lại với đống disassmble instructions, ta lấy ra được và thực hiện reverse. Từ dòng đầu tới vòng 60 ta thấy chương trình nhận 4 giá trị từ chương trình gốc sau đó lưu vào stack với offset là 20->32. Sau đó thực hiện biến đổi và lưu lại 4 giá trị đó vào stack với offset từ 36->48 và trả về chương trình gốc. Vậy thì cụ thể nó đã biến đổi ra sao, ta sẽ bắt đầu từ dòng 65 đổ đi. Đây là note của mình cách nó implement
r0 ^= r1
r2 &= r3
r0 += r2
r0 &= 255
-> ((r0 ^ r1 ) + (r2 & r3)) & 255 // first element
r2 ^= r3
r0 ^= r1
r0 += r2
r0 &= 255
-> ((r0 ^ r1) + (r2 ^ r3) & 255) // fourth element
r4 = r0
r4 -= r2
r2 *= r3
r4 ^= r2
r0 += r1
r5 = r0
r6 = r0
r5 <<= 5
r5 &= 255
r6 &= 255
r6 >>= 3
r5 |= r6
r4 ^= r5
r4 &= 255
-> r4 = ((r0 - r2) ^ (r2 * r3))
-> ((((r0 + r1) << 5 ) & 255) | (((r0+r1) & 255) >> 3))
-> ((((r0 - r2) ^ (r2 * r3)) ^ ((((r0 + r1) << 5 ) & 255) | (((r0+r1) & 255) >> 3))) & 255) // second element
r4 = r1
r4 *= r3
r1 += r2
r4 ^= r1
r0 += r2
r5 = r0
r6 = r0
r5 <<= 4
r5 &= 255
r6 &= 255
r6 >>= 4
r5 |= r6
r4 ^= r5
r4 &= 255
-> r4 = ((r1 * r3) ^ (r1 + r2))
-> ((((r0 + r2) << 4 ) & 255) | (((r0+r2) & 255) >> 4))
-> (((((r1 * r3) ^ (r1 + r2)) ^ ((( (r0 + r2) << 4 ) & 255) | (((r0+r2) & 255) >> 4)))) & 255) // third element
Như thế thì ta có thể thấy đây là hệ phương trình 4 ẩn. Lúc này quá rõ ràng rồi ta chỉ cần xài z3 để recover mảng enc 4 kí tự một. Đây là script của mình:
from z3 import *
from pwn import *
e = ELF("./ebpf-w1playground")
data = e.read(0x6020, 0xe0)[::4]
flag = b""
while data:
results = data[:4]
s = Solver()
r0 = BitVec("a", 32)
r1 = BitVec("b", 32)
r2 = BitVec("c", 32)
r3 = BitVec("d", 32)
for x in [r0,r1,r2,r3]:
s.add(x >= 0x20)
s.add(x < 0x80)
s.add(((r0 ^ r1) + (r2 & r3)) & 0xff == results[0])
s.add(((((r0 - r2) ^ (r2 * r3)) ^ ((((r0 + r1) << 5 ) & 255) | (((r0+r1) & 255) >> 3))) & 255) == results[1])
s.add((((((r1 * r3) ^ (r1 + r2)) ^ ((( (r0 + r2) << 4 ) & 255) | (((r0+r2) & 255) >> 4)))) & 255) == results[2])
s.add(((r2 ^ r3) + (r0 ^ r1)) & 0xff == results[3])
if s.check() == sat:
flag += bytes([s.model()[i].as_long() for i in [r0,r1,r2,r3]])
data = data[4:]
print(flag)
Shadows of Encryption
Một bài mới do anh Jinn ra nên mình quyết định sẽ update lên blog luôn vì sau này chắc hẳn cần để nhìn lại. Tải file về và nhận ra đây là rust và tệ hơn là rust bị stripped, mình như muốn treo cổ vì trước giờ mình rất yếu khi đụng golang hay rust. Nhưng chuyện gì đến cũng phải đến, lets go…
Như mọi lần thì mình sẽ bắt đầu với việc chạy thử xem file làm gì.

Hmm có vẻ bị lỗi gì đó mình bắt đầu đi tìm kiếm tại sao lại bị lỗi như trên. Vậy là nó k thể đọc được file nào đó. Tới đây thì việc tiếp theo là mở ida và analyze. Các bạn hãy xài file res.i64 vì mình đã khôi phục gần như tất cả các hàm và có comment. Vậy là chương trình cần đọc file censored.png

Tạo một file ảnh censored.png
bất kì ở đây mình tạo như sau

Tiếp tục debug thì mình phát hiện giá trị mà mình đặt tên là randomkey
luôn thay đổi và nó luôn là 16 bytes. Tới đây rồi mình đã nghĩ ngay tới đây là một dạng mã hóa kiểu dữ liệu mà đúng hơn nó sẽ là AES
. Tuy nhiên nó chỉ là phỏng đoán ban đầu, mình tiếp tục debug, tới hàm EXPANDKEY
sau đó từ 16 bytes random đầu nó thành 176 bytes. Tới đây không nghi ngờ gì nữa đây là khúc expand key trong AES. ( Sau khi có những phỏng đoán mình đã phải dành thời gian làm cryptohack và học về AES nên wu có vẻ sẽ rất trơn trượt nhưng khi làm mình không hề như vậy =))) ) . Lúc này tưởng ngon ăn, mình tưởng bài này anh Jinn chắc chỉ cho AES ECB 128 thôi nhỉ?? Chạy thử và so sánh với kết quả encryption trên cyberchef với kết quả chương trình. Oh…

Không giống tí nào… Vậy là sao? Nếu mà vậy thì sẽ padding key ở đâu vì nó dùng random_chacha
mà nhỉ? Tới đây thì mình quyết định phải rev vào core của encrypt chứ không thể như này nữa.

void *__fastcall ENCRYPT(void *a1, char *expand, char *padding)
{
char *v3; // rax
unsigned __int64 v4; // rdx
__int64 v5; // rdx
__int64 v6; // rax
__int64 v7; // rdx
char *v8; // rax
unsigned __int64 v9; // rdx
char *v11; // rax
unsigned __int64 v12; // rdx
unsigned __int64 v13; // [rsp+8h] [rbp-C0h]
unsigned __int64 v14; // [rsp+10h] [rbp-B8h]
char xor_STATE[16]; // [rsp+40h] [rbp-88h] BYREF
__int64 v16; // [rsp+50h] [rbp-78h]
__int64 v17; // [rsp+58h] [rbp-70h]
__int64 v18; // [rsp+60h] [rbp-68h]
__int64 v19; // [rsp+68h] [rbp-60h]
__int64 v20[3]; // [rsp+70h] [rbp-58h] BYREF
__int64 v21; // [rsp+88h] [rbp-40h]
unsigned __int64 v22; // [rsp+90h] [rbp-38h]
unsigned __int64 v23; // [rsp+98h] [rbp-30h]
__int64 v24; // [rsp+A0h] [rbp-28h]
__int64 v25; // [rsp+A8h] [rbp-20h]
char *v26; // [rsp+B0h] [rbp-18h]
char *v27; // [rsp+B8h] [rbp-10h]
__int64 v28; // [rsp+C0h] [rbp-8h]
v26 = expand;
v27 = padding;
sub_55644CDE3220(xor_STATE, (__int64)padding);
v16 = 0LL;
v17 = 4LL;
v3 = (char *)take_EXPANDKEY();
addroundkey(xor_STATE, v3, v4); // xor state with sbox
v18 = 1LL;
v19 = 10LL;
v20[0] = sub_55644CDE3D80(1LL);
v20[1] = v5;
while ( 1 )
{
v6 = sub_55644CDE3D70(v20);
v21 = v7;
v20[2] = v6;
if ( !v6 )
break;
v14 = v21;
v28 = v21;
shift_rows(xor_STATE); // not that SUS
mix_column(xor_STATE); // last round skip
if ( !is_mul_ok(4uLL, v14) )
sub_55644CDE02E0((__int64)"attempt to multiply with overflow", 33LL, (__int64)&off_55644CE6FCE8);
v13 = 4 * v14;
if ( !is_mul_ok(4uLL, v14) )
sub_55644CDE02E0((__int64)"attempt to multiply with overflow", 33LL, (__int64)&off_55644CE6FD00);
if ( v13 >= 0xFFFFFFFFFFFFFFFCLL )
sub_55644CDE02E0((__int64)"attempt to add with overflow", 28LL, (__int64)&off_55644CE6FD18);
v22 = 4 * v14;
v23 = v13 + 4;
v11 = (char *)take_EXPANDKEY(); // use subkey
addroundkey(xor_STATE, v11, v12);
}
shift_rows(xor_STATE);
v24 = 40LL;
v25 = 44LL;
v8 = (char *)take_EXPANDKEY();
addroundkey(xor_STATE, v8, v9);
sub_55644CDE34B0(a1, xor_STATE); // AES WITHOUT SUBBYTES
return a1;
}
Sau khi đọc code + debug miệt mài mình khôi phục được như sau vậy là rõ rồi AES ECB without sub bytes. Vậy thì chắc chắn sẽ có cách crack nhỉ. Sau khi research và tất nhiên mình cũng hỏi các anh, các bạn chơi crypto thì mình được những link rất hữu dụng.
https://medium.com/@wrth/cracking-aes-without-any-one-of-its-operations-c42cdfc0452f https://crypto.stackexchange.com/questions/20228/consequences-of-aes-without-any-one-of-its-operations https://hackmd.io/@vishiswoz/r10P7knwj
Vậy nó có thể crack nhưng nhất định phải có một cặp block plain - cipher. Lúc này quay lại vấn đề làm sao để kiếm plain đây? Lúc này mình suy nghĩ là hmm nếu là file png thì nó sẽ có header bytes giống nhau vậy thì lúc này thỏa với 16 bytes plaintext rồi. Header bytes: 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52
. Final script:
from sage.all import *
def bytes2mat(b):
a = []
for i in b:
tmp = bin(i)[2:].zfill(8)
for j in tmp:
a.append(int(j))
return Matrix(GF(2), a)
def mat2bytes(m):
a = ""
for i in range(128):
a += str(m[0, i])
a = [a[i:i+8] for i in range(0, 128, 8)]
a = [int(i, 2) for i in a]
return bytes(a)
I = identity_matrix(GF(2), 8)
X = Matrix(GF(2), 8, 8)
for i in range(7):
X[i, i+1] = 1
X[3, 0] = 1
X[4, 0] = 1
X[6, 0] = 1
X[7, 0] = 1
C = block_matrix([
[X, X+I, I, I],
[I, X, X+I, I],
[I, I, X, X+I],
[X+I, I, I, X]
])
zeros = Matrix(GF(2), 8, 8)
zeros2 = Matrix(GF(2), 32, 32)
o0 = block_matrix([
[I, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros]
])
o1 = block_matrix([
[zeros, zeros, zeros, zeros],
[zeros, I, zeros, zeros],
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros]
])
o2 = block_matrix([
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros],
[zeros, zeros, I, zeros],
[zeros, zeros, zeros, zeros]
])
o3 = block_matrix([
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, zeros],
[zeros, zeros, zeros, I]
])
S = block_matrix([
[o0, o1, o2, o3],
[o3, o0, o1, o2],
[o2, o3, o0, o1],
[o1, o2, o3, o0]
])
M = block_matrix([
[C, zeros2, zeros2, zeros2],
[zeros2, C, zeros2, zeros2],
[zeros2, zeros2, C, zeros2],
[zeros2, zeros2, zeros2, C]
])
R = M*S
A = S*(R**9) # sorry for the inconsistency in the variable name, this is supposed to be SA^9 that I talked about
p = open("censored.png.enc", "rb").read()
p2 = "89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52"
p2 = bytes.fromhex(p2)
ct2 = "2ff89e3a6c9a3747cab74b9300ebcdc8"
ct2 = bytes.fromhex(ct2)
p2 = bytes2mat(p2).transpose()
ct2 = bytes2mat(ct2).transpose()
K = ct2 - A * p2
recovered_plaintext = b""
for i in range(0, len(p), 16):
block = p[i:i+16]
block = bytes2mat(block)
block = (A.inverse() * (block.transpose() - K)).transpose()
recovered_plaintext += mat2bytes(block)
open("recovered.png", "wb").write(recovered_plaintext)

Mình sẽ không thể solve nếu không có sự giúp đỡ của các anh, các bạn chơi crypto. Shout out for crypto players !
p/s: https://legacy.cryptool.org/en/cto/aes-step-by-step
mình sử dụng web này kết hợp trong lúc debug.
Lời cuối: em xin cảm ơn anh dream02 và anh Jinn vì đã tạo ra những challenge thú vị.