All Articles

dvCTF2022 Writeup Mini Game(Rev)

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

I participated in dvCTF, which started on 3/12, so I wrote a brief writeup.

Mini Game(Rev)

I tried to run the downloaded file, but it failed with a no such file or directory error.

According to the following article, binaries built on alpine may fail to run on glibc-based distributions such as Ubuntu because the required libraries are not available.

Reference: When a binary says “no such file or directory”—common pitfalls when running binaries built on Alpine on other Linux distros - Qiita

So I installed musl libc with the following command.

sudo apt install musl-dev

This allowed me to run the binary.

Next, I decompiled it with Ghidra.

For dynamic analysis, I changed Ghidra’s image base to 0x555555554000.

Below is an excerpt from the decompiled main function.

// main関数
check_input(input_text);
for (i = 0; i < 0xac; i = i + 1) {
iVar1 = *(int *)(&DAT_555555558420 + (long)i * 4);
change_value((long)iVar1 / (long)0xe & 0xffffffff,(long)iVar1 % (long)0xe & 0xffffffff,
                (long)iVar1 % (long)0xe & 0xffffffff);
}
iVar1 = is_clear?();

The function names were renamed manually.

When I looked at the is_clear function, which ultimately determines whether you get the flag, it looked like this.

image-20220314233543897

Since DAT_0010409c stores 0xe, the code above appears to check the values in DAT_555555558100 196 times (0xe * 0xe), four bytes at a time from the beginning. You only get the flag if all of those values are either 0x2d or 0x2a.

However, user input cannot manipulate the values in DAT_555555558100 directly.

Tracing backward through the main function, I found the following code where the user’s input changes the values in DAT_555555558420.

for (; (pcVar2 = token, token != (char *)0x0 && (i < 0xac)); i = i + 1) {
    *(undefined8 *)(acStack120 + lVar1 + -8) = 0x5555555559d2;
    iVar3 = atoi(pcVar2);
    *(int *)(&DAT_555555558420 + (long)i * 4) = iVar3;
    *(undefined8 *)(acStack120 + lVar1 + -8) = 0x5555555559fe;
    token = strtok((char *)0x0,";");
}

In the code above, the input is split on ;, and up to 0xac values are checked.

It then stores those ;-separated numbers as 4-byte integers, sequentially, in the first 0xac entries of DAT_555555558420.

for (i = 0; i < 0xac; i = i + 1) {
    iVar1 = *(int *)(&DAT_555555558420 + (long)i * 4);
    change_value((long)iVar1 / (long)0xe & 0xffffffff,(long)iVar1 % (long)0xe & 0xffffffff,
                    (long)iVar1 % (long)0xe & 0xffffffff);
}

The values stored in this DAT_555555558420 region can then indirectly rewrite DAT_555555558100 through the following code.

for (local_c = 0; local_c < 0xac; local_c = local_c + 1) {
  iVar1 = *(int *)(&DAT_00104420 + (long)local_c * 4);
  FUN_00101a29((long)iVar1 / (long)DAT_00104098 & 0xffffffff,
               (long)iVar1 % (long)DAT_0010409c & 0xffffffff,
               (long)iVar1 % (long)DAT_00104098 & 0xffffffff);
}

In this code, the DAT_00104420 region is traversed 0xac times in 4-byte steps, and transformed versions of those values are passed into FUN_00101a29.

Inside FUN_00101a29, as shown below, the address in DAT_555555558100 indicated by the incoming values is rewritten to 0x2d only if it is not already 0x2a.

if ((((-1 < div) && (div < 0xe)) && (-1 < mod)) && (mod < 0xe)) {
    if (*(int *)(&DAT_555555558100 + ((long)div * 0xe + (long)mod) * 4) == 0x2a) {
        BOOM();
    }
    else {
        *(undefined4 *)(&DAT_555555558100 + ((long)div * 0xe + (long)mod) * 4) = 0x2d;
    }
    return;
}

At initialization, the contents of 0x555555558100 looked like this.

$ x/197w 0x555555558100
0x555555558100: 0x0000002d      0x00000001      0x00000001      0x00000000
0x555555558110: 0x00000001      0x00000001      0x00000001      0x00000001
0x555555558120: 0x00000001      0x00000001      0x00000001      0x00000001
0x555555558130: 0x00000001      0x00000000      0x00000001      0x0000002a
0x555555558140: 0x00000001      0x00000000      0x00000001      0x0000002a
0x555555558150: 0x00000001      0x00000001      0x0000002a      0x00000001
0x555555558160: 0x00000001      0x0000002a      0x00000001      0x00000000
0x555555558170: 0x00000001      0x00000002      0x00000002      0x00000002

From there, if you extract every address other than the ones that originally contain 0x2a and 0x2d, then pass their relative offsets joined with ; as the input, all 196 four-byte entries in 0x555555558100 become either 0x2a or 0x2d. That clears the check and gives you the flag.

The final input that produced the flag was as follows.

0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;172;16;17;18;173;20;21;174;23;24;175;26;27;28;29;30;31;32;33;34;35;36;37;38;39;40;41;42;43;177;45;178;47;48;49;179;51;52;53;180;55;56;57;58;181;60;61;62;63;64;65;66;67;68;69;183;71;72;73;74;75;76;185;78;79;80;81;82;83;84;85;186;87;88;89;90;91;92;93;188;95;96;97;98;99;100;189;102;103;104;105;106;107;108;109;110;190;112;113;114;191;116;117;118;119;120;121;122;192;124;125;126;127;128;129;130;131;132;133;134;135;136;137;138;139;140;141;193;143;144;145;194;147;148;149;150;151;152;153;154;155;156;157;158;159;160;161;195;0;164;165;166;167;168;169;170;171

I generated this input with the following solver.

init_table = [0x00000001,0x00000001,0x00000001,0x00000000,
            0x00000001,0x00000001,0x00000001,0x00000001,
            0x00000001,0x00000001,0x00000001,0x00000001,
            0x00000001,0x00000000,0x00000001,0x0000002a,
            0x00000001,0x00000000,0x00000001,0x0000002a,
            0x00000001,0x00000001,0x0000002a,0x00000001,
            0x00000001,0x0000002a,0x00000001,0x00000000,
            0x00000001,0x00000002,0x00000002,0x00000002,
            0x00000002,0x00000002,0x00000001,0x00000002,
            0x00000002,0x00000002,0x00000001,0x00000002,
            0x00000002,0x00000001,0x00000000,0x00000001,
            0x0000002a,0x00000003,0x0000002a,0x00000001,
            0x00000000,0x00000001,0x0000002a,0x00000001,
            0x00000000,0x00000001,0x0000002a,0x00000002,
            0x00000001,0x00000002,0x00000002,0x0000002a,
            0x00000002,0x00000001,0x00000001,0x00000002,
            0x00000002,0x00000001,0x00000000,0x00000001,
            0x00000001,0x00000002,0x0000002a,0x00000002,
            0x00000002,0x00000002,0x00000001,0x00000000,
            0x00000001,0x0000002a,0x00000001,0x00000001,
            0x00000001,0x00000001,0x00000000,0x00000001,
            0x00000001,0x00000002,0x0000002a,0x00000002,
            0x00000001,0x00000000,0x00000001,0x00000001,
            0x00000001,0x00000001,0x0000002a,0x00000001,
            0x00000001,0x00000001,0x00000001,0x00000001,
            0x00000003,0x0000002a,0x00000002,0x00000000,
            0x00000000,0x00000000,0x00000000,0x00000001,
            0x00000002,0x00000002,0x00000002,0x0000002a,
            0x00000001,0x00000000,0x00000002,0x0000002a,
            0x00000002,0x00000000,0x00000000,0x00000000,
            0x00000000,0x00000000,0x00000001,0x0000002a,
            0x00000002,0x00000001,0x00000001,0x00000001,
            0x00000002,0x00000002,0x00000001,0x00000001,
            0x00000001,0x00000001,0x00000000,0x00000000,
            0x00000001,0x00000001,0x00000001,0x00000000,
            0x00000000,0x00000001,0x0000002a,0x00000001,
            0x00000000,0x00000001,0x0000002a,0x00000002,
            0x00000002,0x00000002,0x00000001,0x00000000,
            0x00000000,0x00000000,0x00000000,0x00000001,
            0x00000001,0x00000001,0x00000000,0x00000001,
            0x00000001,0x00000003,0x0000002a,0x0000002a,
            0x00000001,0x00000000,0x00000000,0x00000001,
            0x00000001,0x00000002,0x00000001,0x00000001]

arr = []
other = [172, 173, 174, 175, 177, 178, 179, 180, 181, 183, 
        185, 186, 188, 189, 190, 191, 192, 193, 194, 195, 0]
j = 0
for i, data in enumerate(init_table):
    if data != 0x0000002a:
        arr.append(str(i))
    else:
        arr.append(str(other[j]))
        j += 1

result = ";".join(arr)
print(result)

Summary

Sorry this was a rough writeup.