Home IrisCTF 2024
Post
Cancel

IrisCTF 2024

Introduction

전역 전 마지막 주말에 잠깐 CTF를 했다!

대회 기간에 얼마 안 봤어서 다시 풀어보기로 했다

Reversing

Secure Computing - 484pts

Description

1
Your own secure computer can check the flag! Might have forgotten to add the logic to the program, but I think if you guess enough, you can figure it out. Not sure

문제 이름이 seccomp다.

Dockerfile, chal, snippet.c가 주어진다.

1
2
$ file chal
chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=14f708039846d9aa20c56627866551a92a387633, stripped

snippet.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Here's a snippet of the source code for you

int main() {
    printf("Guess: ");
    char flag[49+8+1] = {0};
    if(scanf("%57s", flag) != 1 || strlen(flag) != 57 || strncmp(flag, "irisctf{", 8) != 0 || strncmp(flag + 56, "}", 1)) {
        printf("Guess harder\n");
        return 0;
    }
#define flg(n) *((__uint64_t*)((flag+8))+n)
    syscall(0x1337, flg(0), flg(1), flg(2), flg(3), flg(4), flg(5));
    printf("Maybe? idk bro\n");

    return 0;
}

irisctf{...} 형식으로 입력을 줘야 한다.

1
2
3
$ ./chal
Guess: irisctf{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}
Bad system call

형식을 맞춰서 입력해보면 Bad system call을 출력하고 종료된다.

1
2
3
$ seccomp-tools dump ./chal
Guess: irisctf{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}
Maybe? idk bro

seccomp-tools로 dump를 해보면 Maybe? idk bro가 출력되고 종료된다.

sub_870

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
__int64 sub_870()
{
  char **v0;  rsi
  int v1;  edi
  char **v2;  rdx
  __int64 v3;  rbx
  __int64 v4;  rax
  __int64 result;  rax
  __int16 v6;  [rsp+0h] [rbp-48h] BYREF
  __int64 v7;  [rsp+8h] [rbp-40h]
  unsigned __int64 v8;  [rsp+18h] [rbp-30h]

  v0 = 0LL;
  v1 = 0;
  v8 = __readfsqword(0x28u);
  if ( ptrace(PTRACE_TRACEME, 0LL) >= 0 )
  {
    v3 = 0LL;
    prctl(38, 1LL, 0LL, 0LL, 0LL);
    do
    {
      v0 = (char **)(&dword_0 + 1);
      v1 = SYS_seccomp;
      v6 = *(_QWORD *)((char *)&unk_202020 + v3);
      v4 = *(__int64 *)((char *)&off_23D560 + v3);
      v3 += 8LL;
      v7 = v4;
      syscall(SYS_seccomp, 1LL, 0LL, &v6);
    }
    while ( v3 != 64 );
  }
  result = __readfsqword(0x28u) ^ v8;
  if ( result )
    return main(v1, v0, v2);
  return result;
}

.init_array section을 확인해보면 위 함수를 호출하는데

ptrace로 디버깅을 탐지하여 seccomp rule을 설정한다

ptrace(PTRACE_TRACEME, 0LL) >= 0에서 js short loc_8FAjns short loc_8FA로 패치하고 seccomp-tools로 다시 dump해보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ seccomp-tools dump ./chal_patched
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x15 0x01 0x00 0x00001337  if (A == 0x1337) goto 0003
 0002: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0003: 0x03 0x00 0x00 0x0000000b  mem[11] = X
 0004: 0x04 0x00 0x00 0x9a0b31d4  A += 0x9a0b31d4
 0005: 0x04 0x00 0x00 0x5245d02a  A += 0x5245d02a
 0006: 0x1c 0x00 0x00 0x00000000  A -= X
 0007: 0x04 0x00 0x00 0x7d5a280a  A += 0x7d5a280a
 0008: 0x1c 0x00 0x00 0x00000000  A -= X
 0009: 0x24 0x00 0x00 0x000081af  A *= 0x81af
 0010: 0x03 0x00 0x00 0x00000003  mem[3] = X
 0011: 0xa4 0x00 0x00 0xd3400e8e  A ^= 0xd3400e8e
 ...
 3791: 0x60 0x00 0x00 0x0000000f  A = mem[15]
 3792: 0x15 0x00 0x01 0xd101957e  if (A != 3506541950) goto 3794
 3793: 0x06 0x00 0x00 0x00050000  return ERRNO(0)
 3794: 0x06 0x00 0x00 0x00000000  return KILL

엄청 긴 seccomp rule을 볼 수 있다

삽질

1
2
3
#     -l, --limit LIMIT                Limit the number of calling "prctl(PR_SET_SECCOMP)".
#                                      The target process will be killed whenever its calling times reaches LIMIT.
#                                      Default: 1

sub_870 함수를 보면 8번으로 나눠서 seccomp filter를 설정해주는데 seccomp-tools는 기본적으로 prctl 호출 LIMIT이 1로 설정되어 있다..!

그래서 seccomp-tools dump ./chal_patched -l 8을 실행해야 8개의 필터가 모두 적용된다.

이 점만 유의해서 seccomp filter를 dump한 뒤에 z3로 풀어주면 플래그를 획득할 수 있다

sol.py

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
import re
from z3 import *

# seccomp-tools dump ./chal_patched -l 8 > seccomp.txt
with open('./seccomp.txt', 'r') as f:
    code = f.read().split('\n')[2:]

s = Solver()
A = BitVec('A', 64)
X = BitVec('X', 64)
mem = [BitVec(f'mem_{i}', 64) for i in range(16)]
args = [BitVec(f'args_{i}', 64) for i in range(6)]
orig_args = [_ for _ in args]
arch = 0xc000003e # AUDIT_ARCH_X86_64
sys_number = 0x1337
offset = 34

for i in range(6):
    for j in range(8):
        c = Extract((j+1)*8 - 1, j*8, orig_args[i])
        s.add(And(0x20 < c, c < 0x7f))

for c in code:
    line = c[offset:]
    res = re.search('if \((.*)\) goto (\d+)', line)
    if res:
        comp, n = res.groups()
        if 'KILL' in code[int(n)]:
            exec(f's.add(Not({comp}))')
        else:
            exec(f's.add({comp})')
    elif re.search('return', line):
        continue
    else:
        res = re.search('(.*) = (.*) >> (.*)', line)
        if res:
            lst = res.groups()
            exec(f'{lst[0]} = LShR({lst[1]}, {lst[2]})')
        else:
            exec(line)

    A = A & 0xFFFFFFFF
    X = X & 0xFFFFFFFF

while True:
    if s.check() != sat:
        break

    m = s.model()
    res = [int(m[i].as_long()) for i in orig_args]

    print(''.join(map(lambda x: x.to_bytes(64, 'little').decode(), res)))
    check = And([orig_args[i] == res[i] for i in range(6)])
    s.add(check == False)
1
2
3
4
5
6
7
8
9
$ python3 sol.py
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_1nstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBQF_1nstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_qnstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBQF_qnstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_9nstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBQF_9nstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBQF_ynstruct10ns!
1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_ynstruct10ns!

flag : irisctf{1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_1nstruct10ns!}

The Maze - 484pts

Description

1
you all remember sickscigames.com, right? there were some bangers on there.

파일이 안 주어진다.

문제 링크에 들어가보면 미로찾기 게임이 나온다.

개발자 도구로 보면 js/tfg-min.js를 로드하는 부분이 있다.

tfg-min.js

1
2
3
eval(Function("eval(Function(\"[M='tB__mWRL@RlXI_MItVPf_vvw]bdUwytzW`\x1cOgON\x1cTwf`hv`m|zhfb_XXK~czS\x1cnttRISdCYiBUcBuNwkUUXHIcWNL\x1c^UM~J}C|Dl[QugvaWwqkLsp}}QulTES[sJknBcNLKdPbAzW~KwRZ?TkIyAUqUetZEDrHJlnGselozxOWhBjnpTlEI}DDqq}FVSL?`maN_a@HsWH`dkwD~bguL}NP_Y|mgHXFGayuBag{lw~EBqzB}ovcwCA~ladQJR]ClxbOF~wTWzJVru}qjThvBg?|xz?XXeEKJ@rpvAuUcpeQWa]?|Zwdk^ASfANamVBEXWCThpNGIF_AmubVuqghov^TlDkEzEIQsUE{btQrAaBYJLJZxFGtV_g{fD\x1c]Ef[{E[uzYSCQLUVOfgFuPoGHRMjN~HjDJGJtDeRtCPpz}Euod}iS_Rx?~j]ZoQ@TjK~ODioveynx?HisQKkEGf@_SkcKvT}fAR}XcKigQ~kUW]ecvdd||vnizqcUf^VjX?uf_nWpUjZporSnnhWjQSf}z?RsPtWnExPkuYZ\x1cSRj?rD_`navK]mB?nYacfyUoIDB_dE|GdekRIbFREf]eHvhKznrALBF_Jy}`JTp?}cfUB?DRaPfHMiGUhq@HfudQglfRwj`qjaPjoXjxix{PXYDQ[c~HRlsynIQ}ZSBg_AKQPtQbvEcTVecxlpU?KLp{AfKjlpCemI]KPXyk]mFdjjy?~]wXI[LiMgJRBR{ZyqUy[?Cjgn?y`|mkRLeLWEhK`UFSsMzl[wmplLzJGl\x1cNpnHOKTjqG}ayE}znbGmN|XynnRkN|pZ_m|UANMSPW@nZX_\x1crerI_eCQ@sNXQcD{`Q_XdKJbkwiOL|LR{j\x1cDvUYH@gG]KmzhQIPCGK^BPJMsn[IsTidDoQLR~MXKR@~s[@UpB~zsyClCJNYIpxzP]m`~NXc|St{GRONAqjVYklvgBv@YxlJi^E^VzRM@`T_Qg}pGs\x1cIjzO@Su@QT@{{RXxzmkpMAIggWwxeNnnmM[LYSKILYUUmZyxhbjPpXizjZhOaVCk{HEBpWFdf`Br|VjBg\x1cgMN[~|IERmOWigKeTsAVYDSN_oNWdaYL}]adU~]AbsZ}VNOem[hgWOBceugpicywKBALBpvj@gkcz]Ezmuj|tMubU_YdQT~G^hViJsz`BtTVXzttmi\x1cK]m}Pr^cecwqpCggkcwm\x1cqc|ljw{ZuGOxTvTKbJXpiTDBQuy^~YWnGhE[DSGeH[chUZxvY^RBbaMd~Pi^W}YMYHCfDJBwlRal_@w]dXw?kbKDNq]?JNHf[wByX@drpzx_}rZeEfk?q^XzZmLZutEovAp?XlPkC?lGWVYhxPde?IxYVlf^@mS~fS@YIBZPLZrkXXlDMRsKf~Ya[WX?}[xuPDV[FaS_o\x1cqb}vdoVPdPso?X]brFWVGqnVSn?d^th\x1cJJV@pzRLBrqVgblVVPV{cHkUrvZn_NgcfViTPEFVbnNDTIZmCStJizDANgBvYLcEaUomUfAJ]TbDQ?OO?P|eKjNtYr]SmkVTAtVCfzqblp?hQ[ys{BJJ\x1cPvyDbX]CccrIfUUrxWhN\x1c`DXRRklaLiut^~fvJw[Ig^kEllVA\x1cQzDKN~UVeddJxb]vTIQVgvmS}~vqcfOip_vmSHNwCG|x|YWsTiBKhTVv[VD[nVH`bEHcSIPi_YrAUsijbYagVk{OV\x1cLQNqbuLt?EB[PelmYn?pYHpKoQRvkkKD~BmjrzMyyphNCJswaoJ|W|N{N@bKEHUC@ZY]}pozRdbXotHHnybjD`zK`X\x1cToF{@lN\x1c\x1ciow@u?SNjpiBAuWUFsI}aLpTUZWfOQ^hfyLGN?JCE~Kq`^TW{YE[@dru{~PoBLyVPXA@f\x1cu]LBi}wGEFxmXP^hbVL^e~{FvkaRCgVrDBD`n?iD~ay`IuEpT?t[WW@ly]pDpyFo`Fu?N}sfxeOl\x1c{^U}EAd\x1cWSVSIFiGaKdVG}qTvdRpBabFVsmV\x1cxFVfsvyzbliPSoVQpsLjpBQMdojewsu^mUk`PrXUdfnlD{WFLcoFAAGNIZBeFVcUPIEvYdV|skfZuPGrCdfBOPXUiV~zDzW?ctpEROCTJAaYqWngSdYy^asrIwARXsFPHI[OvWacAlfrT\x1cI^rQ?KYlCwV||mY{COF[kIPRnz@^TlP~PHFqUnTV_UL`jyggd|E~EKy[n]^EDGufMO_UV}GrWSVc~[_CDDAQNbIvYVM~c?XTDBkUaVunYaZ]vQognB^]\x1cntw_vOWpK~VksyVg\x1cvjkO[]N~LXWOeeF_]Sl?x[[Q_bwv}RFtFvyw][@azsFuVjH?zvLJTfsn~gNUtizn`ble\x1c^viIgZ^nM@GAQDIu@qFkJ[NAbbF|R|kMenjcRhLrhP[@|cEVqV[slWVNYInVPqpLsWIa\x1ce]wIxLJGYpISPZoWfkjs@?\'\", ...']charCodeAtUinyxpf\', `for (; e < 3656; c[e++] = p -= 128, A = A ? p - A && A : (p == 34 | p == 96) && p) for (p = 1; p < 128; y = f.map((n, x) => (U = r[n] * 2 + 1, U = Math.log(U / (h - U)), t -= a[x] * U, U / 500)), t = ~-h / (1 + Math.exp(t)) | 1, i = o % h < t, o = o % h + (i ? t : h - t) * (o >> 17) - !i * t, f.map((n, x) => (U = r[n] += (i * h / 2 - r[n] << 13) / ((C[n] += C[n] < 5) + 1 / 20) >> 13, a[x] += y[x] * (i - t / h))), p = p * 2 + i) for (f = \'010202103203210431053105410642065206541\'.split(t = 0).map((n, x) => (U = 0, [...n].map((n, x) => (U = U * 997 + (c[e - n] | 0) | 0)), h * 32 - 1 & U * 997 + p + !!A * 129) * 12 + x); o < h * 32; o = o * 64 | M.charCodeAt(d++) & 63); for (C = String.fromCharCode(...c); r = /[\\0-\x1e]/.exec(C);) with(C.split(r)) C = join(shift()); console.log(C)return C")([], [], 1 << 17, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], new Uint16Array(51e6).fill(1 << 15), new Uint8Array(51e6), 0, 0, 0, 0))

일단 sources 탭에서 모든 코드를 복사해서 로컬에서 분석했다.

먼저 eval로 호출되는 함수 코드를 보기위해 return C 이전에 console.log(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
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
99
100
101
102
103
104
105
106
107
108
109
110
111
const e = document.getElementById("c"),
    t = e.getContext("2d"),
    a = [
        [...Array(4).keys()].map((e => [-17969, -16540, 11745, -12783, 3226, 15010, 10940, 3387, -5306, -4100, -21425, 10338, -16904, -355, 13485, -25858].map((e => e / 503155)).slice(4 * e, 4 * e + 4))), [...Array(4).keys()].map((e => [24356, 12443, -34624, -20408, 7719, 2169, -12039, -4767, -11817, -10941, 24441, 12396, -17878, -8011, 28295, 19198].map((e => e / 138081)).slice(4 * e, 4 * e + 4))), [...Array(4).keys()].map((e => [-14826, 3464, 5822, -13182, 51761, -11669, -19467, 45292, 29097, -6763, -10919, 25324, -11126, 2364, 4412, -9672].map((e => e / 10270)).slice(4 * e, 4 * e + 4))), [...Array(4).keys()].map((e => [-10870, 13314, 3852, 6736, 8930, -9852, -1980, -5468, -982, 3891, 1980, 3481, 7174, -9705, -4194, -6127].map((e => e / 35766)).slice(4 * e, 4 * e + 4)))
    ],
    n = "Dugd8DbBCXnrEF1kKd2Hg4lsRQ1eV/6gQ+NfwsVhtr4UgeXQFq1m6WctmIljEG7PZg==",
    r = ["toy cube", "laser pointer", "large axle", "gift box", "dust pan", "tea kettle", "v-type engine", "stop sign"],
    i = {
        a: 0,
        b: 0,
        c: [0, 0, 0, 0],
        d: [38, 40, 37, 39],
        e: [],
        f: {
            x: 40,
            y: 40,
            z: []
        },
        g: {
            x: -7,
            y: -7
        },
        h: [],
        i: [],
        j: 1,
        k: 0,
        l: ""
    };

function c(e) {
    let t = e + 1831565813;
    return t = Math.imul(t ^ t >>> 15, 1 | t), t ^= t + Math.imul(t ^ t >>> 7, 61 | t), ((t ^ t >>> 14) >>> 0) / 4294967296
}

function o(e, a, n, r, c = 1) {
    t.fillStyle = `rgba(${[0].reduce(((e,t)=>e.slice((c-1)%3).concat(e.slice(0,(c-1)%3))),[25,255*s(i.a,a/500),25])}, 255)`, t.fillRect(e, a, n, r)
}

function y() {
    if (i.g = i.c.reduce(((e, t, a) => 1 === t ? {
            x: e.x + (a < 2 || 1 & i.f.z[i.g.y + 7][i.g.x + 7 + (a - 2)] ? 0 : 2 * (a - 2) - 1),
            y: e.y + (a >= 2 || 2 & i.f.z[i.g.y + 7 + a][i.g.x + 7] ? 0 : 2 * a - 1)
        } : {
            x: e.x,
            y: e.y
        }), {
            x: i.g.x,
            y: i.g.y
        }), i.c.some((e => 1 === e))) {
        i.j <<= 2, i.j |= 3 & i.d[i.c.findIndex((e => 1 === e))], i.j >= 64 && (i.i.push((63 & i.j) - 32), i.j = 1), i.k = i.k + 211 * (i.g.x + 9) * (i.g.y + 9) * 239 & 4294967295, 16 == i.i.length && 1 === i.j && i.e.push("1,6,11,16" == (e = [...Array(4).keys()].map((e => i.i.slice(4 * e, 4 * e + 4))), t = a[i.b], e.map((e => t[0].map(((e, a) => t.map((e => e[a])))).map((t => e.map(((a, n) => e[n] * t[n])).reduce(((e, t) => e + t))))))).flatMap((e => e)).map((e => Math.round(100 * e) / 100)).map(((e, t) => 1 === e ? t + 1 : e)).filter((e => e)) ? i.k : -1);
        const r = i.h.findIndex((e => e.x === i.g.x && e.y === i.g.y));
        if (-1 !== r) {
            const e = i.h[r].name;
            i.h.splice(r, 1), i.l = `found ${e}!`, setTimeout((() => {
                i.l = ""
            }), 4e3)
        }
        i.g.x === i.f.x - 9 && i.g.y === i.f.y - 9 && (i.g.x = -7, i.g.y = -7, i.i = [], i.j = 1, i.k = 0, f(++i.b), 4 !== i.b || 4 !== i.e.length || i.e.some((e => -1 === e)) || async function(e) {
            const t = i.e.map((e => e.toString(16).padStart(8, "0"))).join(""),
                a = new Uint8Array(atob(e).split("").map((e => e.charCodeAt(0)))),
                n = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
                r = (new TextEncoder).encode(t),
                c = await crypto.subtle.importKey("raw", r, {
                    name: "AES-GCM"
                }, !1, ["decrypt"]),
                o = await crypto.subtle.decrypt({
                    name: "AES-GCM",
                    iv: n
                }, c, a);
            return (new TextDecoder).decode(o)
        }(n).then((e => i.l = e)))
    }
    var e, t;
    i.c = i.c.map((e => 1 & e ? 3 : 0))
}

function l() {
    var a;
    t.clearRect(0, 0, e.width, e.height), o(e.width / 2 - 4, e.height / 2 - 4, 8, 8), i.h.forEach((t => {
        o(e.width / 2 - 4 + 40 * (t.x - i.g.x), e.height / 2 - 4 + 40 * (t.y - i.g.y), 8, 8, 2)
    })), [...Array(i.f.y).keys()].forEach((e => {
        [...Array(i.f.x).keys()].forEach((t => {
            0 != (2 & i.f.z[e][t]) && o(40 * (t - i.g.x), 40 * (e - i.g.y), 40, 1), 0 != (1 & i.f.z[e][t]) && o(40 * (t - i.g.x), 40 * (e - i.g.y), 1, 40)
        }))
    })), a = i.l, t.textAlign = "center", t.font = "32px monospace", t.fillStyle = "rgba(25, 255, 25, 255)", t.fillText(a, e.width / 2, e.height / 2)
}

function f(e) {
    i.f.z = [...Array(i.f.y).keys()].map((t => [...Array(i.f.x - 1).keys()].reduce(((a, n) => t > 0 && t < i.f.x - 1 && c(23 * n + 7 * t + 3 * e) > .5 == 0 ? [
        [...a[0].slice(0, a[1] + Math.floor(c(17 * n + 9 * t + 3 * e) * (n - a[1] + 1))), 1 | a[0][a[1] + Math.floor(c(17 * n + 9 * t + 3 * e) * (n - a[1] + 1))], ...a[0].slice(a[1] + Math.floor(c(17 * n + 9 * t + 3 * e) * (n - a[1] + 1)) + 1)], n + 1
    ] : [
        [...a[0].slice(0, n), 2 | a[0][n], ...a[0].slice(n + 1)], a[1]
    ]), [t < i.f.x - 1 ? [1, ...new Array(i.f.x - 2).fill(0), 1] : new Array(i.f.x).fill(0), 0]))).map((e => e[0])), i.h = [...Array(6).keys()].map((t => ({
        x: Math.floor(30 * c(17 * t + 23 * e)),
        y: Math.floor(30 * c(23 * t + 17 * e)),
        name: r[Math.floor(8 * c(3 * t + 21 * e))]
    })))
}

function s(e, t, a = 800, n = .6, r = .03) {
    const i = (e + t) % a;
    return (.8 + Math.sin(7 * e) * r) * Math.min(1, n + Math.max(0, .009 * (i - a / 2) ** 2))
}
e.width = 600, e.height = 600, document.onkeydown = e => {
        i.c[i.d.indexOf(e.keyCode)] |= 1
    }, document.onkeyup = e => {
        i.c[i.d.indexOf(e.keyCode)] = 0
    }, f(i.b),
    function e() {
        y(), l(), i.a++, requestAnimationFrame(e)
    }();

조금 읽어보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
i.g.x === i.f.x - 9 && i.g.y === i.f.y - 9 && (i.g.x = -7, i.g.y = -7, i.i = [], i.j = 1, i.k = 0, f(++i.b), 4 !== i.b || 4 !== i.e.length || i.e.some((e => -1 === e)) || async function(e) {
    const t = i.e.map((e => e.toString(16).padStart(8, "0"))).join(""),
        a = new Uint8Array(atob(e).split("").map((e => e.charCodeAt(0)))),
        n = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
        r = (new TextEncoder).encode(t),
        c = await crypto.subtle.importKey("raw", r, {
            name: "AES-GCM"
        }, !1, ["decrypt"]),
        o = await crypto.subtle.decrypt({
            name: "AES-GCM",
            iv: n
        }, c, a);
    return (new TextDecoder).decode(o)
}(n).then((e => i.l = e)))

여기에서 특정 조건이 만족되면 AES-GCM으로 뭔가 decrypt하는 부분이 있다

i.e가 암호문으로 사용되는데 i.e는 아래에서 특정 조건이 만족되면 push된다

1
i.j <<= 2, i.j |= 3 & i.d[i.c.findIndex((e => 1 === e))], i.j >= 64 && (i.i.push((63 & i.j) - 32), i.j = 1), i.k = i.k + 211 * (i.g.x + 9) * (i.g.y + 9) * 239 & 4294967295, 16 == i.i.length && 1 === i.j && i.e.push("1,6,11,16" == (e = [...Array(4).keys()].map((e => i.i.slice(4 * e, 4 * e + 4))), t = a[i.b], e.map((e => t[0].map(((e, a) => t.map((e => e[a])))).map((t => e.map(((a, n) => e[n] * t[n])).reduce(((e, t) => e + t))))))).flatMap((e => e)).map((e => Math.round(100 * e) / 100)).map(((e, t) => 1 === e ? t + 1 : e)).filter((e => e)) ? i.k : -1);

얘를 정리해보면 대충 이렇게 된다

1
2
3
4
5
6
7
8
i.j <<= 2
i.j |= 3 & <누른 방향키 charcode>
if i.j >= 64:
    i.i.push((63 & i.j) - 32)
    i.j = 1
    i.k = i.k + 211 * (i.g.x + 9) * (i.g.y + 9) * 239 & 4294967295
    if 16 == i.i.length and 1 === i.j:
        i.e.push(i.k if <조건> else -1)

i.ei.k가 push되는 조건을 분석해보면 아래와 같다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
i.e.push(
    "1,6,11,16" == (
        e = [...Array(4).keys()].map((e => i.i.slice(4 * e, 4 * e + 4))), // 4*4 배열
        t = a[i.b], // 4*4 배열
        e.map(
            (
                e => t[0].map(
                    ((e, a) => t.map((e => e[a]))) // t -> transpose
                ).map(
                    (t => e.map(
                        ((a, n) => e[n] * t[n])
                    ).reduce(((e, t) => e + t))) // e[N][M]에는 e[N], t.transpose()[M]의 내적 --> 행렬곱
                )
            )
        )    
    ).flatMap((e => e)).map((e => Math.round(100 * e) / 100)).map(((e, t) => 1 === e ? t + 1 : e)).filter((e => e)) // 행렬곱 결과 1인 요소의 index가 1, 6, 11, 16인지 확인함 (identity matrix인지 확인)
    ? i.k : -1
)

solution

각 스테이지별 a의 역행렬을 구하고 bruteforce로 올바른 i.i가 나오는 path를 찾으면 된다.

sol.sage

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
99
100
101
102
103
104
105
106
107
108
109
# run on sage
from itertools import product
from collections import defaultdict

def solve():
    matrices = [
        [[-17969, -16540, 11745, -12783, 3226, 15010, 10940, 3387, -5306, -4100, -21425, 10338, -16904, -355, 13485, -25858], 503155],
        [[24356, 12443, -34624, -20408, 7719, 2169, -12039, -4767, -11817, -10941, 24441, 12396, -17878, -8011, 28295, 19198], 138081],
        [[-14826, 3464, 5822, -13182, 51761, -11669, -19467, 45292, 29097, -6763, -10919, 25324, -11126, 2364, 4412, -9672], 10270],
        [[-10870, 13314, 3852, 6736, 8930, -9852, -1980, -5468, -982, 3891, 1980, 3481, 7174, -9705, -4194, -6127], 35766]
    ]

    ret = '''
    finished = true;

    async function pressKey(key) {
        keydownEvent = new KeyboardEvent('keydown', {
            key: key,
            code: key,
            keyCode: key,
            which: key,
        });

        keyupEvent = new KeyboardEvent('keyup', {
            key: key,
            code: key,
            keyCode: key,
            which: key,
        });

        const targetElement = document;
        targetElement.dispatchEvent(keydownEvent);
        await delay(100);
        targetElement.dispatchEvent(keyupEvent);
    }

    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function checkFinished() {
        while (!finished) {
            await delay(100);
        }
    }

    async function solve() {
        lst = [solve0, solve1, solve2, solve3]
        for (func of lst) {
            finished = false;
            func();
            await checkFinished();
            finished = true;
            i.g = {x: 31, y: 31}
            await pressKey(39);
            console.log(func.name + " done!");
        }
    }

    '''

    script = """
    function solve{idx}() {{
        path = {path};
        let idx = 0;
        async function go() {{
            if (idx < path.length) {{
                const key = path[idx];
                await pressKey(key);
                idx++;
                go();
            }} else {{
                finished = true;
            }}
        }}

        go();
    }}
    
    """

    for idx, (M, c) in enumerate(matrices):
        path = []
        M = Matrix(4, 4, map(lambda x: x / c, M))
        res = M.inverse().coefficients()

        d = {38: 'up', 40: 'down', 37: 'left', 39: 'right'}
        candi = defaultdict(list)

        for lst in product(d, repeat=3):
            j = 1
            for c in lst:
                j = (j << 2) | (3 & c)
            candi[(j & 63) - 32] += [lst]

        # find path
        for i in range(16):
            path += candi[res[i]][0]

        ret += script.format(idx=idx, path=path)

    ret += 'solve();'

    with open('solve.js', 'w') as f:
        f.write(ret)

solve()

로컬에서 solve.js를 개발자 도구에 붙여넣고 기다리면 플래그를 얻을 수 있다.

flag : irisctf{thankfully_no_ghost_girl}

Small Universe - 484pts

Description

1
For some reason this binary file acts differently on my different Apple Devices.

universe가 주어진다.

1
2
$ file universe
universe: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit x86_64 executable] [arm64]

Universal binary

Universal Binary는 애플 용어로 이전에 사용하던 기반에서도 별도의 과정 없이 실행되는 응용프로그램이다.

쉽게 silicon, intel 프로세서 둘 다에서 동작하는 바이너리이다.

찾아보니까 universal binary는 각 아키텍처로 두 번 빌드해서 lipo -create -output myapp myapp_amd64 myapp_arm64를 통해 바이너리를 합쳐서 만들 수 있다고 한다.

그리고 lipo -extract arm64 -output extracted_binary your_universal_binary를 이용해서 특정 아키텍처 바이너리를 추출할 수도 있다.

analysis

mac이 없어서 https://github.com/konoui/lipolipo를 이용했다.

1
2
$ ./lipo -info ./universe
Architectures in the fat file: ./universe are: x86_64 arm64
1
2
$ ./lipo -extract x86_64 -output ./universe_x86-64 ./universe
$ ./lipo -extract arm64 -output ./universe_arm64 ./universe

위 명령어들을 이용해 바이너리를 추출해서 분석을 시작했다.

x86-64

1
2
3
4
5
6
7
8
9
10
...
fmt_Fprintf(&go_itab__ptr_os_File_comma_io_Writer, os_Stdout, &unk_10B45BB, 5LL, 0LL, 0LL, 0LL); // Key:
...
fmt_Fscanln(&go_itab__ptr_os_File_comma_io_Reader, os_Stdin, key_ipt, 1LL, 1LL);
...
key_decoded = encoding_base64__ptr_Encoding_DecodeString(runtime_bss, key->ptr, key->len, ...);
...
v26 = main_Decrypt(key_decoded, ptr, v36, main_flag, qword_1151F08, qword_1151F10);
...
 // "Failed to decrypt"나 "Flag: %s"가 출력됨
1
2
3
4
5
6
7
8
9
_int64 main_Decrypt(...) {
    ...
    crypto_aes_NewCipher(key);
    ...
    stream = crypto_cipher_newCFB(block, a2, flag, 16, v23, 1, ...)
    stream[3](a2, &flag[((16 - v20) >> 63) & 0x10], ...) // decrypt
    ...
    return &flag[((16 - v20) >> 63) & 0x10];
}

x86-64 바이너리는 key를 입력받고 flag[16:]{key: key, iv: flag[:16]}으로 decrypt한다.

arm64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
fmt_Fprintf(&go_itab__os_File_io_Writer, os_Stdout, (const char *)&unk_1000978DD, 6LL, 0LL); // Flag: 
...
fmt_Fscanln(&go_itab__os_File_io_Reader, os_Stdin, &flag_ipt);
...
flag_decoded = encoding_base64___Encoding__DecodeString(runtime_bss, *flag_encoded, flag_encoded[1]);
...
v13 = main_Decrypt(
          &flag[(cap - 16) & (-(v8 - cap + 16) >> 63)], // flag[len(flag)-16:]
          16LL,
          v8 - v7 + 16,
          (__int64)main_key,
          ...);
...
// "Failed to decrypt"나 "Key: %s"가 출력됨
1
2
3
4
5
6
7
8
9
10
11
_int64 main_Decrypt(...) {
    ...
    crypto_aes_NewCipher(flag);
    ...
    res = &key[((16 - cap) >> 63) & 0x10]; // key[16:]
    crypto_cipher_newCFB(block, v18, _key, 16LL, a15, 1, ...)
    ...
    result = res;
    ...
    return result
}

arm64 바이너리는 flag를 입력받고 key[16:]{key: flag[-16:], iv: key[:16]}으로 decrypt한다.

solution

arm64 바이너리에서 key를 획득하고 x86-64 바이너리에서 flag를 얻어야 한다.

go로 solver를 작성하여 플래그를 획득했다

sol.go

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
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"fmt"
	"encoding/hex"
)

func main() {
	// x86-64
	flag_enc_hex := "83f3487dd71182d26b77697980e1f1239cf1f49026d1f6d49bed2021a8cb4fa684ec3e7a6a78cfc80ac4446f1111a92feaf636c82b68"
	flag_enc, err := hex.DecodeString(flag_enc_hex)

	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// arm64
	key1_hex := "506422ba994951c2b76063e89f1a1660c5674a192b91b4bcf14fd163ba213019"
	key1, err := hex.DecodeString(key1_hex)

	if err != nil {
		fmt.Println("Error:", err)
		return
	}
 
	// get key
	block1, err := aes.NewCipher(flag_enc[len(flag_enc)-16:])
	stream1 := cipher.NewCFBDecrypter(block1, key1[:16])
	key2 := make([]byte, 16)
	stream1.XORKeyStream(key2, key1[16:])

	// get flag
	block2, err := aes.NewCipher(key2)
	stream2 := cipher.NewCFBDecrypter(block2, flag_enc[:16])
	buf := make([]byte, len(flag_enc))

	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	stream2.XORKeyStream(buf, flag_enc[16:])
	fmt.Printf("Flag: %s\n", buf)
}

flag : irisctf{uN!v3rSaL5_B1n4Ries_arE_wEirD}

CloudVM - 496pts

Description

1
2
3
why run on your pc when cloud pc do trick?

Note: example.bin to get your bearings straight, michaelpaint.bin contains actual challenge

문제가 닫혀서 못 푼다 :sob:

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