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_8FA
를 jns 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.e
에 i.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/lipo
의 lipo
를 이용했다.
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
문제가 닫혀서 못 푼다