Post

EHAX CTF 2026 Writeup

EHAX CTF 2026 Writeup

Reverse Engineering

1. I guess bro

Mức độ: MediumKiến trúc: RISC-V 64-bitCông cụ: Ghidra, Python

Khảo sát ban đầu (Initial Recon)

Challenge cung cấp một file thực thi duy nhất i guess bro. Việc đầu tiên luôn là kiểm tra thông tin file và chạy thử.

Kiểm tra thông tin file

Phân tích: Nhìn vào output, thấy ngay nguyên nhân gây ra lỗi exec format error:

  • File được build cho kiến trúc UCB RISC-V 64-bit, chứ không phải x86_64 thông dụng trên máy tính của chúng ta.
  • File ở trạng thái stripped (đã bị xóa bỏ bảng thông tin symbol/tên hàm nguyên bản).

Do IDA Freeware thường không hỗ trợ tốt hoặc không có sẵn plugin cho RISC-V, Ghidra sẽ là vũ khí hoàn hảo nhất cho bài này nhờ khả năng decompile đa kiến trúc cực mạnh.

Tìm kiếm điểm neo (Strings & XREF)

Phân tích: Vì file bị stripped (không có hàm main), ta tìm điểm bắt đầu bằng cửa sổ Defined Strings. Chuỗi "Wrong length! Keep guessing..." là một thông báo báo lỗi kinh điển. Click đúp vào chuỗi này và theo dấu Cross-Reference (XREF), Ghidra đưa ta đến hàm xử lý logic chính của chương trình.

Khôi phục pseudo code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
undefined8 FUN_ram_0001037e(void)
{
  long lVar1;
  undefined8 uVar2;
  undefined1 auStack_90 [128];

  FUN_ram_00010800();
  FUN_ram_0001085c();
  lVar1 = FUN_ram_00014f24(auStack_90,0x80,PTR_DAT_ram_0007e3b0_ram_0007e588);
  if (lVar1 == 0) {
    FUN_ram_000151c4("Input error!");
    uVar2 = 1;
  }
  else {
    lVar1 = FUN_ram_0001ed44(auStack_90,"\n");
    auStack_90[lVar1] = 0;
    lVar1 = FUN_ram_0001ee70(auStack_90);
    if (lVar1 == 0x23) {
      lVar1 = FUN_ram_00010732(auStack_90);
      if (lVar1 == 0) {
        FUN_ram_000151c4(&DAT_ram_00051440);
        uVar2 = 1;
      }
      else {
        FUN_ram_000151c4(&DAT_ram_00051410);
        FUN_ram_000110d4("Flag: %s\n",auStack_90);
        uVar2 = 0;
      }
    }
    else {
      FUN_ram_000151c4("Wrong length! Keep guessing...");
      uVar2 = 1;
    }
  }
  return uVar2;
}

Bật tab Decompiler của Ghidra lên, ta sẽ thấy một đoạn mã giả (pseudo-C) ban đầu khá rối mắt. Tuy nhiên, bằng kỹ năng C cơ bản, ta có thể đổi tên để lột mặt nạ đoạn code này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
undefined8 FUN_ram_0001037e(void)

{
  long lVar1;
  undefined8 uVar2;
  undefined1 user_input [128];

  FUN_ram_00010800();
  FUN_ram_0001085c();
  lVar1 = fgets(user_input,0x80,PTR_DAT_ram_0007e3b0_ram_0007e588);
  if (lVar1 == 0) {
    FUN_ram_000151c4("Input error!");
    uVar2 = 1;
  }
  else {
    lVar1 = FUN_ram_0001ed44(user_input,"\n");
    user_input[lVar1] = 0;
    lVar1 = FUN_ram_0001ee70(user_input);
    if (lVar1 == 0x23) {
      lVar1 = check_flag(user_input);
      if (lVar1 == 0) {
        FUN_ram_000151c4(&DAT_ram_00051440);
        uVar2 = 1;
      }
      else {
        FUN_ram_000151c4(&DAT_ram_00051410);
        FUN_ram_000110d4("Flag: %s\n",user_input);
        uVar2 = 0;
      }
    }
    else {
      FUN_ram_000151c4("Wrong length! Keep guessing...");
      uVar2 = 1;
    }
  }
  return uVar2;
}

Phân tích logic:

  1. Chương trình đọc input của người dùng (tối đa 128 bytes) thông qua một hàm có cấu trúc y hệt fgets.
  2. Tìm và thay thế ký tự xuống dòng \n thành \0 (tương đương strcspn).
  3. Kiểm tra độ dài: if (strlen(user_input) == 0x23). Vậy chiều dài bắt buộc của Flag là 35 ký tự. Nếu sai, chương trình ném ra lỗi “Wrong length…“.
  4. Nếu đúng 35 ký tự, nó truyền input vào hàm lõi check_flag.

Phân tích Lõi kiểm tra & Bẫy Anti-Debug

Tiếp tục double click để đi sâu vào hàm check_flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
undefined8 check_flag(undefined8 param_1)

{
  long lVar1;
  long lVar2;
  undefined8 uVar3;
  int iStack_28;

  lVar1 = FUN_ram_0001eaf0(param_1,"EH4X{nOt_th3_r34l_fl4g}");
  if (lVar1 == 0) {
    FUN_ram_000151c4("Nice try, but that\'s not it!");
    return 0;
  }
  lVar1 = FUN_ram_0001eaf0(param_1,"EH4X{try_h4rd3r_buddy}");
  if (lVar1 == 0) {
    FUN_ram_000151c4("Getting warmer... but nope!");
    return 0;
  }
  lVar1 = FUN_ram_0002022e();
  iStack_28 = 0;
  do {
    iStack_28 = iStack_28 + 1;
  } while (iStack_28 < 100000);
  lVar2 = FUN_ram_0002022e();
  if (lVar2 - lVar1 < 0xc351) {
    lVar1 = FUN_ram_000105cc(param_1);
    if ((lVar1 != 0) && (lVar1 = FUN_ram_00010622(param_1), lVar1 != 0)) {
      uVar3 = FUN_ram_00010700(param_1);
      return uVar3;
    }
  }
  else {
    FUN_ram_000151c4("Debugger detected! Exiting...");
    FUN_ram_000110c0(1);
  }
  return 0;
}

Phân tích: Tác giả đã giăng ra khá nhiều bẫy trong căn phòng này:

  • Bẫy Fake Flag: Chương trình so sánh input với các chuỗi "EH4X{n0t_th3_r34l_fl4g}" để đánh lừa người chơi.
  • Bẫy Anti-Debug: Nó gọi 2 lần hàm lấy thời gian hệ thống (get_time) kẹp giữa một vòng lặp 100.000 lần. Nếu thời gian thực thi lớn hơn mức quy định, nó in ra “Debugger detected! Exiting…“. Khẳng định lại việc chọn Phân tích tĩnh từ đầu là chính xác!
  • Lõi 3 lớp (3-Stage Check): Để lấy được chữ “Correct!”, input phải lọt qua 3 khe cửa. Cả 3 hàm đều kiểm tra cùng 1 chuỗi input:
    • Cửa 2: Một mớ hỗn độn các phương trình toán học và chia lấy dư (Z3 Solver). Cua2
    • Cửa 3: Một hàm băm Hash một chiều vô phương cứu chữa. Cua3
    • Cửa 1: Jack-pot! (Thuật toán đối xứng). Cua1

Khai thác mắt xích yếu nhất (XOR Decryption)

Phân tích thuật toán: Thuật toán ở cửa 1 này cực kỳ sơ hở:

  1. Trỏ đến vùng nhớ DAT_ram_00057bc8 để lấy từng byte mã hóa.
  2. Đưa qua vòng lặp giải mã: Lấy byte đó XOR với bVar4 (biến đếm tăng thêm 7 sau mỗi vòng) và XOR tiếp với hằng số 0xa5.
  3. So sánh kết quả giải mã với input của người dùng.

Phép toán XOR có tính đối xứng. Do đó, ta chỉ cần vào vùng nhớ DAT_ram_00057bc8, copy 35 byte bản mã (ciphertext) và viết script mô phỏng lại đúng thao tác trên là cờ sẽ tự rớt ra.

Chuỗi Hex lấy từ Ghidra: e0ea9fe8c2ffbfe1c2fd96db828df4a88aa6b3145d694d357e694c7b135a1417287136

Kịch bản giải mã (Solve Script)

Tạo file solve.py với nội dung sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Mảng 35 bytes trích xuất từ Ghidra
hex_data = "e0ea9fe8c2ffbfe1c2fd96db828df4a88aa6b3145d694d357e694c7b135a1417287136"
data = bytes.fromhex(hex_data)

flag = ""
bVar4 = 0

for byte in data:
    # Lõi giải mã: byte ^ bVar4 ^ 0xa5
    decrypted_char = chr(byte ^ bVar4 ^ 0xa5)
    flag += decrypted_char

    # Ép kiểu giới hạn trong 1 byte (0-255)
    bVar4 = (bVar4 + 7) & 0xFF

print("[+] Flag:", flag)

flag

Chạy script và thu được flag: EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}

2. Pathfinder

Mức độ: MediumKiến trúc: ELF x86_64Công cụ: IDA Freeware, Python BFS

Khảo sát ban đầu & Cái bẫy “Patching” (Fake Flag)

Chạy thử chương trình, nó hỏi có phải là “pathfinder” không: [y/n]. Nhập y, nó yêu cầu ta nhập đường đi tốt nhất (best path).

Mở file bằng IDA Freeware và nhảy đến hàm main, ta thấy một đoạn kiểm tra luồng rẽ nhánh quen thuộc:

Luồng kiểm tra nhập y/n và đường đi

Thử patching jz thanh 2 lệnh nop chương trình sẽ in ra một Flag có dạng EHAX{abcd...} nhưng nộp hệ thống sẽ báo Sai. Tại sao? Vì đây là Bẫy Data Flow (Luồng dữ liệu)! Cờ được tạo ra bằng cách lấy chính chuỗi đường đi (input) đem đi nén lại. Nếu nhập sai và ép nó in cờ, nó sẽ nén cái input sai đó thành một cái Cờ Giả (Fake Flag). Kết luận: Bắt buộc phải dịch ngược thuật toán để tìm ra chuỗi input chính xác tuyệt đối.

Đọc vị luật chơi Mê cung (The Maze Logic)

Nhấp đúp vào hàm kiểm tra chính sub_1444 và nhấn F5 để xem mã giả C.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_BOOL8 __fastcall sub_1444(_BYTE *a1)
{
  __int64 v1; // rdx
  unsigned __int16 v3; // [rsp+Ch] [rbp-2Ch]
  unsigned __int16 v4; // [rsp+Eh] [rbp-2Ah]
  unsigned int v5; // [rsp+10h] [rbp-28h]
  unsigned int v6; // [rsp+14h] [rbp-24h]
  unsigned int v7; // [rsp+1Ch] [rbp-1Ch]
  _BYTE *i; // [rsp+20h] [rbp-18h]
  __int64 v9; // [rsp+2Ch] [rbp-Ch]
  int v10; // [rsp+34h] [rbp-4h]

  v5 = 0;
  v6 = 0;
  for ( i = a1; *i; ++i )
  {
    v1 = 12LL * (unsigned __int8)*i;
    v9 = *(_QWORD *)((char *)&unk_4120 + v1);
    v10 = *(_DWORD *)((char *)&unk_4120 + v1 + 8);
    LOBYTE(v3) = v10 ^ (107 * *i) ^ 0x3C;
    HIBYTE(v3) = BYTE1(v10) ^ (107 * *i) ^ 0x3C;
    if ( !BYTE2(v10) )
      return 0;
    v7 = HIDWORD(v9) + v6;
    if ( (unsigned int)v9 + v5 > 9 || v7 > 9 )
      return 0;
    LOBYTE(v4) = sub_123F(v5, v6);
    HIBYTE(v4) = sub_123F((unsigned int)v9 + v5, v7);
    if ( (v3 & v4) == 0 )
      return 0;
    v5 += v9;
    v6 += HIDWORD(v9);
  }
  return v5 == 9 && v6 == 9 && (unsigned int)sub_126B(a1) == -2034609652;
}

Phân tích logic:

  • v5 = 0; v6 = 0;: Khởi tạo tọa độ xuất phát (X = 0, Y = 0).
  • Vòng lặp for duyệt qua từng ký tự ta nhập vào. Mã ASCII của ký tự được nhân với 12 (v1 = 12LL * (unsigned __int8)*i) để trỏ tới bảng tra cứu unk_4120 nhằm lấy ra bước dịch chuyển trên trục X và Y.
  • Nếu đi ra ngoài giới hạn 0-9 (> 9), lập tức return 0 (Thua).
  • Điều kiện chiến thắng: Kết thúc ở tọa độ (9, 9) và chuỗi input đi qua một hàm mã băm Checksum (sub_126B) phải trả về đúng -2034609652 (0x86BA520C).
  • Kiểm tra tường (sub_123F): Nó gọi sub_123F kiểm tra ô hiện tại và ô sắp bước tới, sau đó thực hiện phép AND Bitwise: if ((v3 & v4) == 0) return 0;

Đi sâu vào sub_123F, ta thấy nó trả về giá trị tại byte_40A0[10 * X + Y]. Đây chính là mảng 2 chiều 10x10 lưu trữ bản đồ mê cung!

Truy tìm Dữ liệu ẩn (Constructors)

Khi nhảy đến vùng nhớ unk_4120 (Bảng di chuyển) và byte_40A0 (Bản đồ) trong IDA, ta phát hiện chúng hoàn toàn trống rỗng (nằm ở vùng .bss toàn byte 0). Ai đã nạp dữ liệu vào đây?

Hãy nhìn vào cột Cross-Reference (XREF) bên phải của IDA:

Vùng nhớ .bss và các XREF chỉ đến Constructor

💡 Note: > Vùng .bss dùng để chứa các biến chưa khởi tạo. Tuy nhiên, IDA phát hiện có 2 hàm sub_11EEsub_12CE tác động vào vùng này. Đây là các hàm Constructor (được khai báo với __attribute__((constructor)) trong C). Chúng được hệ điều hành âm thầm gọi chạy TRƯỚC CẢ KHI HÀM MAIN BẮT ĐẦU, dùng để giải mã và nạp bản đồ vào bộ nhớ.

Khảo sát 2 hàm này bằng F5:

1. Bảng điều khiển (sub_12CE): Hàm này gán các giá trị dịch chuyển tọa độ (X, Y) vào các offset của bảng unk_4120. Tính toán mã ASCII ngược lại, ta tìm được 4 phím di chuyển hợp lệ (chữ IN HOA):

  • N (North): X = -1, Y = 0
  • S (South): X = 1, Y = 0
  • E (East): X = 0, Y = 1
  • W (West): X = 0, Y = -1

2. Giải mã Bản đồ (sub_11EE): Một vòng lặp 100 lần để nạp bản đồ 10x10.

1
2
3
4
for ( i = 0; i <= 99; ++i ) {
    v0 = byte_2020[i];
    byte_40A0[i] = sub_11C9((unsigned int)i) ^ v0;
}

Nó lấy mảng 100 bytes mã hóa ở byte_2020 đem XOR với bộ tạo khóa sub_11C9 (có công thức: (31 * i + 17) ^ (8 * i) ^ 0xA5).

Đến đây, ta lấy toàn bộ 100 bytes tĩnh từ byte_2020 ra để chuẩn bị viết kịch bản.

Dữ liệu thô 100 bytes tại byte_2020

Thuật toán Dò đường (BFS with Bitmask Doors)

Bài này không dùng mê cung tường/đường (0/1) thông thường. Nhớ lại lệnh if ((v3 & v4) == 0) trong hàm kiểm tra sub_123F:

Bản đồ dùng Bitmask làm các cửa một chiều. Để đi qua lại giữa 2 ô, ô hiện tại phải có bit “Mở Cửa Ra” HOẶC ô sắp tới phải có bit “Mở Cửa Vào” khớp với hướng đi:

  • Đi Bắc (N): Cần bit 0x04 ở ô hiện tại hoặc 0x01 ở ô đích.
  • Đi Nam (S): Cần bit 0x01 ở ô hiện tại hoặc 0x04 ở ô đích.
  • Đi Đông (E): Cần bit 0x02 ở ô hiện tại hoặc 0x08 ở ô đích.
  • Đi Tây (W): Cần bit 0x08 ở ô hiện tại hoặc 0x02 ở ô đích.

Dưới đây là kịch bản BFS (Tìm kiếm theo chiều rộng) đầy đủ để giải mã bản đồ và tự động tìm đường ngắn nhất thỏa mãn cả luật Mở cửa khắt khe này.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from collections import deque

# 100 bytes dữ liệu thô (byte_2020) lấy từ IDA
byte_2020 = [
    0xBC, 0x97, 0xF6, 0xD3, 0x08, 0x21, 0x5E, 0x77, 0xEC, 0xC5,
    0xB2, 0x9B, 0x45, 0x69, 0x16, 0x37, 0x2E, 0x07, 0x06, 0x63, 0x78,
    0x91, 0xAD, 0xC7, 0x9C, 0x70, 0x42, 0x2B, 0x35, 0xD9, 0xEE,
    0x85, 0x5E, 0xB7, 0x90, 0xF2, 0xE8, 0x01, 0x3B, 0x57, 0x09, 0xE5,
    0xD2, 0xBB, 0xA0, 0x49, 0x7E, 0x15, 0xC5, 0x2D, 0x2F, 0x03,
    0x54, 0x7B, 0x82, 0xA7, 0xB9, 0x95, 0x62, 0x4B, 0x15, 0x39,
    0xC3, 0xEF, 0x71, 0x5D, 0xBF, 0x93, 0xC8, 0xE1, 0x1B, 0x37,
    0x2D, 0x05, 0xF1, 0xD1, 0x89, 0xA9, 0x5E, 0x73, 0xE1, 0xCD,
    0xCA, 0x23, 0x38, 0x51, 0x6E, 0x87, 0xD9, 0xB0, 0x81, 0x61,
    0x7A, 0x13, 0x2C, 0xC5, 0x1E, 0x77, 0x5B, 0xB0
]

# 1. Giải mã Bản đồ (XOR)
maze = []
for i in range(100):
    key = ((31 * i + 17) ^ (8 * i) ^ 0xA5) & 0xFF
    maze.append(byte_2020[i] ^ key)

grid = [maze[i:i+10] for i in range(0, 100, 10)]

# 2. Logic kiểm tra Cửa (Bitmasks)
def can_move(x, y, nx, ny, move):
    curr_cell = grid[x][y]
    next_cell = grid[nx][ny]
    
    if move == 'N': return (curr_cell & 0x04) != 0 or (next_cell & 0x01) != 0
    if move == 'S': return (curr_cell & 0x01) != 0 or (next_cell & 0x04) != 0
    if move == 'E': return (curr_cell & 0x02) != 0 or (next_cell & 0x08) != 0
    if move == 'W': return (curr_cell & 0x08) != 0 or (next_cell & 0x02) != 0
    return False

# 3. BFS Dò đường
queue = deque([(0, 0, "")])
visited = set([(0, 0)])
directions = {'N': (-1, 0), 'S': (1, 0), 'E': (0, 1), 'W': (0, -1)}

while queue:
    x, y, path = queue.popleft()
    
    if x == 9 and y == 9:
        print("[+] CHUỖI ĐƯỜNG ĐI:", path)
        break
        
    for move, (dx, dy) in directions.items():
        nx, ny = x + dx, y + dy
        if 0 <= nx < 10 and 0 <= ny < 10 and (nx, ny) not in visited:
            if can_move(x, y, nx, ny, move):
                visited.add((nx, ny))
                queue.append((nx, ny, path + move))

Kết quả & Lấy Flag thật

Khi chạy kịch bản trên, thuật toán BFS sẽ in ra một chuỗi đường đi chính xác, lách qua toàn bộ các cánh cửa gài bẫy.

Kết quả, màn hình sẽ in ra thông báo “You have what it takes” kèm theo Flag thật!

Flag

Flag: Flag: EHAX{2E3S2W6S8E2NE2S}

3. Compute it

Mức độ: HardKiến trúc: ELF x86_64Công cụ:

Misc

1. Inferno Sprint

Mức độ: MediumThể loại: Scripting, AlgorithmCông cụ: Python (pwntools), Thuật toán BFS

Khảo sát ban đầu (Initial Recon)

Challenge cung cấp một địa chỉ netcat. Khi kết nối vào, server trả về luật chơi của một tựa game sinh tồn dạng lưới (grid-based survival game) với các đặc điểm sau:

  • Mục tiêu: Sống sót qua 5 vòng (rounds) mê cung đang cháy bằng cách di chuyển nhân vật (M) ra đến bất kỳ rìa nào của ma trận.
  • Mã hóa: Các hàng của bản đồ không hiển thị trực tiếp mà bị mã hóa dưới dạng chuỗi Hex.
  • Chướng ngại vật & Cơ chế:
    • Tường (#).
    • Lửa lan với tốc độ khác nhau (1, 2, 3). Lửa tốc độ K sẽ mất K lượt để lan sang ô lân cận.
    • Cổng dịch chuyển (Portals) ký hiệu bằng các cặp chữ cái a-e. Đứng lên và bấm P sẽ dịch chuyển tốn 1 lượt.
  • Áp lực thời gian: Chỉ có 90 giây để hoàn thành cả 5 vòng, và mỗi vòng đều có giới hạn số bước di chuyển (LIMIT).

Luật chơi Inferno Sprint và Map Round 1

Tư duy Tự động hóa & Xây dựng “Interpreter” (Automation Mindset)

Với áp lực 90 giây cho 5 ma trận ngày càng phình to, việc giải tay (manual) và tự chuyển đổi Hex sang ASCII là bất khả thi. Thay vào đó, bài toán yêu cầu phải chuyển sang tư duy Tự động hóa (Automation).

Cần viết một kịch bản Python đóng vai trò như một “Interpreter” thu nhỏ:

  1. Đọc dữ liệu thô (Raw Data): Dùng pwntools hứng luồng I/O từ server.
  2. Tiền xử lý (Preprocessing): Bóc tách các thông số SIZE, START, LIMIT và tự động decode các dòng Hex thành một ma trận ASCII 2 chiều để thuật toán dễ dàng thao tác.

Thuật toán Dò đường (BFS Algorithm)

Thay vì phải mô phỏng lại toàn bộ quá trình cháy phức tạp của các loại lửa khác nhau (điều này sẽ làm logic trở nên nặng nề), có thể áp dụng chiến thuật “Điểm mù”: Lửa cần thời gian để lan, nếu ta luôn tìm được đường đi ngắn nhất (Shortest Path) thoát ra rìa mê cung nằm trong giới hạn LIMIT, ta gần như chắc chắn sẽ chạy thoát trước khi lửa đuổi kịp.

Thuật toán BFS (Breadth-First Search) là ứng cử viên hoàn hảo:

  • Khởi tạo một Hàng đợi (Queue) chứa tọa độ bắt đầu (StartX, StartY) và chuỗi đường đi hiện tại (rỗng).
  • Mỗi bước, duyệt 4 hướng di chuyển: Lên (W), Xuống (S), Trái (A), Phải (D).
  • Nếu ô tiếp theo hợp lệ (nằm trong map, không phải Tường # hay Lửa 1,2,3) và chưa đi qua, đẩy vào Queue.
  • Xử lý Cổng (Portal): Nếu đang đứng ở một cổng, thêm một nhánh rẻ (branch) cho hành động bấm P để dịch chuyển sang tọa độ của cổng đích.
  • Điểm dừng (Win Condition): Ngay khi tọa độ chạm vào tọa độ 0 hoặc SIZE - 1 của bất kỳ trục nào.

Kịch bản Giải quyết (Solve Script)

Dưới đây là kịch bản hoàn chỉnh kết hợp pwntools và thuật toán BFS để dọn sạch 5 round trong chưa đầy 10 giây.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
from pwn import *
from collections import deque

# Thông tin kết nối server
HOST = 'chall.ehax.in'
PORT = 31337

def solve():
    # Khởi tạo kết nối
    io = remote(HOST, PORT)
    io.recvuntil(b'BEGIN\n')

    # Xử lý tự động 5 vòng
    for round_num in range(1, 6):
        log.info(f"--- Đang giải Round {round_num}/5 ---")
        
        # 1. Bóc tách thông số giới hạn của Map
        io.recvuntil(b'SIZE ')
        w, h = map(int, io.recvline().strip().split())
        
        io.recvuntil(b'START ')
        sr, sc = map(int, io.recvline().strip().split())
        
        io.recvuntil(b'LIMIT ')
        limit = int(io.recvline().strip())
        
        # 2. Đọc và giải mã Hex sang ASCII, lập bản đồ cổng dịch chuyển
        grid = []
        portal_locs = {}
        portals = {}
        
        for r in range(h):
            hex_line = io.recvline().strip().decode()
            ascii_line = bytes.fromhex(hex_line).decode()
            grid.append(ascii_line)
            
            for c, char in enumerate(ascii_line):
                if char in 'abcde':
                    if char not in portal_locs:
                        portal_locs[char] = []
                    portal_locs[char].append((r, c))

        # Nối các cổng có cùng ký tự lại với nhau
        for char, locs in portal_locs.items():
            if len(locs) == 2:
                portals[locs[0]] = locs[1]
                portals[locs[1]] = locs[0]

        io.recvuntil(b'PATH>')

        # 3. Thuật toán BFS tìm đường thoát ngắn nhất
        q = deque([(sr, sc, "")])
        visited = set([(sr, sc)])
        path_found = ""
        
        while q:
            r, c, current_path = q.popleft()
            
            # Điều kiện thắng: Chạm vào bất kỳ rìa nào của ma trận
            if r == 0 or r == h - 1 or c == 0 or c == w - 1:
                path_found = current_path
                break
                
            # Bỏ qua nếu đường đi chạm ngưỡng LIMIT nhưng chưa thoát
            if len(current_path) >= limit:
                continue
                
            # Duyệt 4 hướng (Lên, Xuống, Trái, Phải)
            moves = [(-1, 0, 'W'), (1, 0, 'S'), (0, -1, 'A'), (0, 1, 'D')]
            for dr, dc, move_char in moves:
                nr, nc = r + dr, c + dc
                
                if 0 <= nr < h and 0 <= nc < w:
                    # Bỏ qua tường và các loại lửa
                    if grid[nr][nc] not in ['#', '1', '2', '3'] and (nr, nc) not in visited:
                        visited.add((nr, nc))
                        q.append((nr, nc, current_path + move_char))
                        
            # Dịch chuyển nếu đứng trên cổng
            if (r, c) in portals:
                pr, pc = portals[(r, c)]
                if (pr, pc) not in visited:
                    visited.add((pr, pc))
                    q.append((pr, pc, current_path + 'P'))

        # 4. Tương tác gửi đáp án về server
        if path_found:
            log.success(f"Tìm thấy đường đi: {path_found}")
            io.sendline(path_found.encode())
        else:
            log.error("Không tìm thấy đường đi hợp lệ!")
            break

    # Trả terminal về trạng thái tương tác để lấy cờ
    io.interactive()

if __name__ == '__main__':
    solve()

Kết quả

Chạy script, Interpreter của chúng ta tự động bóc tách map, tính toán đường đi thần tốc và gửi trả kết quả cho server. Vượt qua 5 round, server nhả cờ.

Kết quả chạy script và lấy Flag

Flag: EH4X{1nf3rn0_spr1n7_bl4z3_runn3r_m4573r}

This post is licensed under CC BY 4.0 by the author.

Trending Tags