1413 words
7 minutes
PicoCTF Reverse Challenge - Vault Door 1 - 6.

Lời mở đầu#

Phần này sẽ giải thích cách để giải các bài Vault-door được viết dưới dạng .java. Để giải được các bài này chỉ cần có kiến thức cơ bản về java language thì mọi thứ sẽ khá là đơn giản.

alt text

#Vault-door-1#

alt text

NOTE

Trước đây sẽ có 2 bài nữa là vault-door-0 và vault-door-2. Nhưng vì một lí do nào đó thì chúng bị xóa đi - có vẻ là do quá dễ để được coi là Reverse Engineering.

Bắt đầu với vault-door-1 thì ta có một đoạn in flag (Thứ mình sẽ bỏ qua) và một đoạn check_password. Ta có thể phân tích từ đây

alt text

Nhìn vào thì có lẽ ta đã có flag trong đầu rồi nhưng khá vất vả khi chúng bị xáo trộn chứ không được theo như thứ tự ban đầu.

Bạn có thể chọn viết tay - nhưng bài này mình sẽ chọn viết bằng script python.

Sử dụng list của python để nhằm phục vụ cho việc sắp xếp nhanh hơn. Ta sẽ cho các dòng password theo dạng sau:

  • flag[0] = ‘a’
  • flag[1] = ‘b’
  • …
  • flag[32] = ‘c’

alt text

Sau khi viết xong nó sẽ có dạng như sau:

flag = [None] * 32
flag[0]  = 'd' 
flag[29] = '9' 
flag[4]  = 'r' 
flag[2]  = '5' 
flag[23] = 'r' 
flag[3]  = 'c' 
flag[17] = '4' 
flag[1]  = '3' 
flag[7]  = 'b' 
flag[10] = '_' 
flag[5]  = '4' 
flag[9]  = '3' 
flag[11] = 't' 
flag[15] = 'c' 
flag[8]  = 'l' 
flag[12] = 'H' 
flag[20] = 'c' 
flag[14] = '_' 
flag[6]  = 'm' 
flag[24] = '5' 
flag[18] = 'r' 
flag[13] = '3' 
flag[19] = '4' 
flag[21] = 'T' 
flag[16] = 'H' 
flag[27] = '5' 
flag[30] = '2' 
flag[25] = '_' 
flag[22] = '3' 
flag[28] = '0' 
flag[26] = '7' 
flag[31] = 'e';

print("picoCTF{" + ''.join(flag[i] for i in range(32)) + "}")'

Vậy là giải quyết được phần sắp xếp và viết tất cả trong một dòng code duy nhất ^_o.

Flag: picoCTF{d35cr4mbl3_tH3_cH4r4cT3r5_75092e}

#Vault-door-3#

alt text

Mô tả đề chỉ ngắn gọn: “This vault uses for-loops and byte arrays.” Nhìn vào checkPassword thì thấy ngay đây là bài hoán vị vị trí ký tự (giống vault-door-1) nhưng phức tạp hơn — chia buffer 32 ký tự thành 4 đoạn, mỗi đoạn một kiểu sắp xếp khác nhau:

public boolean checkPassword(String password) {
    if (password.length() != 32) {
        return false;
    }
    char[] buffer = new char[32];
    int i;
    for (i=0; i<8; i++) {
        buffer[i] = password.charAt(i);
    }
    for (; i<16; i++) {
        buffer[i] = password.charAt(23-i);
    }
    for (; i<32; i+=2) {
        buffer[i] = password.charAt(46-i);
    }
    for (i=31; i>=17; i-=2) {
        buffer[i] = password.charAt(i);
    }
    String s = new String(buffer);
    return s.equals("jU5t_a_sna_3lpm10g747_u_4_m7re49");
}

Phân tích từng vòng lặp để biết buffer[i] map tới password[?] ở đâu:

  • i = 0..7: buffer[i] = password[i] — giữ nguyên vị trí.
  • i = 8..15: buffer[i] = password[23-i] — đảo ngược đoạn 8-15.
  • i = 16,18,...,30 (bước 2): buffer[i] = password[46-i] — các vị trí chẵn lấy từ phía đối xứng.
  • i = 31,29,...,17 (bước -2): buffer[i] = password[i] — các vị trí lẻ giữ nguyên.

Vì mình biết buffer phải bằng chuỗi target, nên chỉ cần đảo ngược từng công thức để suy ra password từ target — không cần brute-force gì cả, thuần là đại số.

target = "jU5t_a_sna_3lpm10g747_u_4_m7re49"
password = [None] * 32

for i in range(8):
    password[i] = target[i]
for i in range(8, 16):
    password[23 - i] = target[i]
for i in range(16, 32, 2):
    password[46 - i] = target[i]
for i in range(31, 16, -2):
    password[i] = target[i]

print("academy{" + "".join(password) + "}")

Chạy ra jU5t_a_s1mpl3_an4gr4m_4_u_777e09 — đọc lái đi thì đúng là “just a simple anagram”. Mình có viết thêm một đoạn mô phỏng lại y hệt checkPassword trong Python rồi chạy thử trên kết quả này để chắc chắn nó thật sự khớp target, tránh đoán nhầm dấu +/- trong công thức đảo ngược.

Flag: academy{jU5t_a_s1mpl3_an4gr4m_4_u_777e09}

alt text

#Vault-door-4#

alt text

Đề bài troll nhẹ: “I made myself dizzy converting all of these numbers into different bases…” — mở file ra thì thấy ngay lý do:

byte[] myBytes = {
    106 , 85  , 53  , 116 , 95  , 52  , 95  , 98  ,
    0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f,
    0142, 0131, 0164, 063 , 0163, 0137, 070 , 070 ,
    'a' , '3' , '1' , 'd' , 'a' , 'd' , '6' , 'c' ,
};

4 dòng là 4 hệ số khác nhau trong cùng một mảng byte: dòng 1 số thập phân, dòng 2 hex (0x), dòng 3 octal (số bắt đầu bằng 0 không có x), dòng 4 ký tự literal. Java cho phép trộn vậy vì tất cả đều resolve về cùng kiểu byte lúc compile. checkPassword chỉ so sánh từng byte của password với mảng này — không có hoán vị gì thêm, nên chỉ cần decode đúng hệ số là ra ngay password, không cần đoán.

my_bytes = [
    106, 85, 53, 116, 95, 52, 95, 98,
    0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f,
    0o142, 0o131, 0o164, 0o63, 0o163, 0o137, 0o70, 0o70,
    ord('a'), ord('3'), ord('1'), ord('d'), ord('a'), ord('d'), ord('6'), ord('c'),
]
password = bytes(my_bytes).decode()
print("academy{" + password + "}")

Ra jU5t_4_bUnCh_0f_bYt3s_88a31dad6c (“just a bunch of bytes”). Mình có check lại bằng cách encode ngược password này về byte rồi so từng phần tử với myBytes gốc — khớp 100%.

Flag: academy{jU5t_4_bUnCh_0f_bYt3s_88a31dad6c}

alt text

#Vault-door-5#

alt text

Đề này troll còn lộ liễu hơn — comment trong code viết thẳng: “URL encoding is meant for web pages… defintely not vault door! Oh wait, should I have not said that in a source code comment?” Đây gần như là spoiler cho cách giải:

public boolean checkPassword(String password) {
    String urlEncoded = urlEncode(password.getBytes());
    String base64Encoded = base64Encode(urlEncoded.getBytes());
    String expected = "JTYzJTMwJTZlJTc2..."; // (rút gọn)
    return base64Encoded.equals(expected);
}

Luồng xử lý là password → urlEncode → base64Encode → so với expected. Vậy muốn lấy lại password chỉ cần đi ngược: expected → base64-decode → urlDecode.

import base64

expected_b64 = (
    "JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVm"
    "JTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2"
    "JTM0JTVmJTMzJTYzJTYzJTM4JTYxJTM5JTY0JTM2"
)

url_encoded = base64.b64decode(expected_b64).decode()   # "%63%30%6e%76..."
parts = url_encoded.split("%")[1:]
password = bytes(int(p, 16) for p in parts).decode()

print("academy{" + password + "}")

Ra c0nv3rt1ng_fr0m_ba5e_64_3cc8a9d6 (“converting from base 64”). Mình có encode lại đúng chiều thuận (urlEncode rồi base64Encode) trên password này để so với expected gốc — khớp, chắc chắn không phải trùng hợp.

Flag: academy{c0nv3rt1ng_fr0m_ba5e_64_3cc8a9d6}

alt text

#Vault-door-6#

alt text

Đề trích dẫn cuốn Applied Cryptography của Bruce Schneier nhưng kết quả chỉ là XOR — vui nhất series:

public boolean checkPassword(String password) {
    byte[] passBytes = password.getBytes();
    byte[] myBytes = {
        0x3b, 0x65, 0x21, 0xa , 0x38, 0x0 , 0x36, 0x1d,
        0xa , 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0xa ,
        0x21, 0x1d, 0x61, 0x3b, 0xa , 0x2d, 0x65, 0x27,
        0xa , 0x6d, 0x65, 0x6d, 0x66, 0x63, 0x62, 0x37,
    };
    for (int i=0; i<32; i++) {
        if (((passBytes[i] ^ 0x55) - myBytes[i]) != 0) {
            return false;
        }
    }
    return true;
}

Điều kiện (passBytes[i] ^ 0x55) - myBytes[i] == 0 tương đương passBytes[i] ^ 0x55 == myBytes[i]. Vì XOR có tính chất tự nghịch đảo (A ^ B = C thì C ^ B = A), nên chỉ cần XOR ngược lại với 0x55 là ra từng byte password:

my_bytes = [
    0x3b, 0x65, 0x21, 0x0a, 0x38, 0x00, 0x36, 0x1d,
    0x0a, 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0x0a,
    0x21, 0x1d, 0x61, 0x3b, 0x0a, 0x2d, 0x65, 0x27,
    0x0a, 0x6d, 0x65, 0x6d, 0x66, 0x63, 0x62, 0x37,
]
password = bytes(b ^ 0x55 for b in my_bytes).decode()
print("academy{" + password + "}")

# verify đúng chiều thuận của check
assert all(((b ^ 0x55) - my_bytes[i]) == 0 for i, b in enumerate(password.encode()))

Ra n0t_mUcH_h4rD3r_tH4n_x0r_808367b (“not much harder than xor” — chính bài tự nhận xét về độ khó của mình). Script có kèm luôn dòng assert để verify lại điều kiện gốc, chạy không lỗi tức là đúng.

Flag: academy{n0t_mUcH_h4rD3r_tH4n_x0r_808367b}

alt text

Cả 4 flag mình nộp thử trực tiếp lên CyLab Academy đều ra “Correct flag!” — khớp với kết quả script tính được.

Tổng kết#

6/6 door xong. Vài điều rút ra:

  • Series này về bản chất chỉ xoay quanh 2 kỹ thuật: hoán vị vị trí ký tự (door 1, 3) và encode/transform rồi so sánh (door 4: multi-base byte array, door 5: url-encode + base64, door 6: XOR). Không có brute-force nào trong cả 6 bài — tất cả đều giải được bằng đại số/đảo ngược hàm thuần túy.
  • Luôn viết script verify lại theo chiều thuận (chạy đúng checkPassword gốc với password mình suy ra) thay vì tin luôn kết quả đảo ngược — mình bị sai số index một lần lúc làm door 3 (nhầm 46-i thành sai vị trí) và script simulate bắt lỗi ngay lập tức, đỡ phải nộp sai flag lên hệ thống.
PicoCTF Reverse Challenge - Vault Door 1 - 6.
https://stewmalwarehunter.id.vn/posts/vaultdoor-1-6/vault_door/
Author
Stew
Published at
2025-07-26
License
CC BY-NC-SA 4.0