CCE 2024 Quals Write Up
CCE 2024 Quals
CCE 2024 예선에 있었던 비극적인 사건을 여러분은 아는가? 작년 디미고 1학년 4명은 CCE 예선에 참가했고 4명 중 3명이 0솔을 기록하며 예선 14등으로 막을 내렸다. (당시는 본선 진출팀이 더 적었다.) 2024 CCE의 예선 리버싱 문제는 분명히 올 해 문제들보다 훨씬 쉬웠다. 개인적으로 정말 한 맺히는 사건이 아닐 수 없고 지금 보면 한 문제를 제외한 모든 문제가 쉽다는 것을 알 수 있다. 선배에게 문제 파일을 받고 업솔빙을 하였으나 내가 푼 것이 아니라서 개인 블로그에 올려두진 않았는데 여기에 적어보도록 하겠다.
Auto REV
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rcx
__int64 v4; // r8
__int64 v5; // r9
_QWORD s2[6]; // [rsp+0h] [rbp-C0h] BYREF
int v8; // [rsp+30h] [rbp-90h]
char s1[64]; // [rsp+40h] [rbp-80h] BYREF
char s[64]; // [rsp+80h] [rbp-40h] BYREF
s2[4] = 0LL;
s2[5] = 0LL;
LOWORD(v8) = 0;
__isoc99_scanf(
&unk_2004,
s,
0x5ADC1CD256D3DC5DLL,
v3,
v4,
v5,
0x58D413DA1BDC5C9CLL,
0x5ADC1CD256D3DC5DLL,
231497173LL,
0);
if ( strlen(s) == 20 )
{
process(s, s1);
if ( !memcmp(s1, s2, 0x14uLL) )
printf("OK");
else
printf("Fail");
return 0;
}
else
{
printf("Fail");
return 0;
}
}
전형적인 Auto 리버싱 문제이다. 당연히 문제 파일을 base64 인코딩하여 던져주고 이를 일정한 횟수 이상 자동으로 성공하면 플래그를 주는 문제이다. 기본적인 바이너리는 위 구조로 이루어지며, process 함수의 연산과 비교하는 값이 바뀐다.
size_t __fastcall process(const char *a1, __int64 a2)
{
size_t result; // rax
int i; // [rsp+1Ch] [rbp-14h]
for ( i = 0; ; ++i )
{
result = strlen(a1);
if ( i >= result )
break;
*(_BYTE *)(i + a2) = ((unsigned __int8)a1[i] >> 1) | (a1[i] << 7);
}
return result;
}
size_t __fastcall process(const char *a1, __int64 a2)
{
size_t result; // rax
int i; // [rsp+1Ch] [rbp-14h]
for ( i = 0; ; ++i )
{
result = strlen(a1);
if ( i >= result )
break;
*(_BYTE *)(i + a2) = a1[i] ^ 0x62;
}
return result;
}
size_t __fastcall process(const char *a1, __int64 a2)
{
size_t result; // rax
int i; // [rsp+1Ch] [rbp-14h]
for ( i = 0; ; ++i )
{
result = strlen(a1);
if ( i >= result )
break;
*(_BYTE *)(i + a2) = a1[i] - 99;
}
return result;
}
process 함수는 위 세 가지 중 한 가지로 결정된다. 모든 항에 임의의 값으로 xor 연산을 가하거나, 임의의 값으로 ror(rol) 연산을 시행한다. 또는 임의의 값을 뺀다. 따라서 process 함수가 어떤 연산을 가하는지, 어떤 값으로 연산을 가하는지를 알아내면 역연산을 통해 적절한 입력값을 찾아낼 수 있다.
0x1169: push rbp
0x116a: mov rbp, rsp
0x116d: push rbx
0x116e: sub rsp, 0x28
0x1172: mov qword ptr [rbp - 0x28], rdi
0x1176: mov qword ptr [rbp - 0x30], rsi
0x117a: mov dword ptr [rbp - 0x14], 0
0x1181: jmp 0x11c4
0x1183: mov eax, dword ptr [rbp - 0x14]
0x1186: movsxd rdx, eax
0x1189: mov rax, qword ptr [rbp - 0x28]
0x118d: add rax, rdx
0x1190: movzx eax, byte ptr [rax]
0x1193: movzx eax, al
0x1196: shl eax, 7
0x1199: mov ecx, eax
0x119b: mov eax, dword ptr [rbp - 0x14]
0x119e: movsxd rdx, eax
0x11a1: mov rax, qword ptr [rbp - 0x28]
0x11a5: add rax, rdx
0x11a8: movzx eax, byte ptr [rax]
0x11ab: shr al, 1
0x11ad: or ecx, eax
0x11af: mov eax, dword ptr [rbp - 0x14]
0x11b2: movsxd rdx, eax
0x11b5: mov rax, qword ptr [rbp - 0x30]
0x11b9: add rax, rdx
0x11bc: mov edx, ecx
0x11be: mov byte ptr [rax], dl
0x11c0: add dword ptr [rbp - 0x14], 1
0x11c4: mov eax, dword ptr [rbp - 0x14]
0x11c7: movsxd rbx, eax
0x11ca: mov rax, qword ptr [rbp - 0x28]
0x11ce: mov rdi, rax
0x11d1: call 0x1030
0x11d6: cmp rbx, rax
0x11d9: jb 0x1183
0x11db: nop
0x11dc: nop
0x11dd: mov rbx, qword ptr [rbp - 8]
0x11e1: leave
0x11e2: ret
0x1169: push rbp
0x116a: mov rbp, rsp
0x116d: push rbx
0x116e: sub rsp, 0x28
0x1172: mov qword ptr [rbp - 0x28], rdi
0x1176: mov qword ptr [rbp - 0x30], rsi
0x117a: mov dword ptr [rbp - 0x14], 0
0x1181: jmp 0x11a9
0x1183: mov eax, dword ptr [rbp - 0x14]
0x1186: movsxd rdx, eax
0x1189: mov rax, qword ptr [rbp - 0x28]
0x118d: add rax, rdx
0x1190: movzx edx, byte ptr [rax]
0x1193: mov eax, dword ptr [rbp - 0x14]
0x1196: movsxd rcx, eax
0x1199: mov rax, qword ptr [rbp - 0x30]
0x119d: add rax, rcx
0x11a0: xor edx, 0x62
0x11a3: mov byte ptr [rax], dl
0x11a5: add dword ptr [rbp - 0x14], 1
0x11a9: mov eax, dword ptr [rbp - 0x14]
0x11ac: movsxd rbx, eax
0x11af: mov rax, qword ptr [rbp - 0x28]
0x11b3: mov rdi, rax
0x11b6: call 0x1030
0x11bb: cmp rbx, rax
0x11be: jb 0x1183
0x11c0: nop
0x11c1: nop
0x11c2: mov rbx, qword ptr [rbp - 8]
0x11c6: leave
0x11c7: ret
0x11e4: push rbp
0x11e5: mov rbp, rsp
0x11e8: sub rsp, 0xc0
0x11ef: movabs rax, 0x58d413da1bdc5c9c
0x11f9: movabs rdx, 0x5adc1cd256d3dc5d
0x1203: mov qword ptr [rbp - 0xc0], rax
0x120a: mov qword ptr [rbp - 0xb8], rdx
0x1211: mov qword ptr [rbp - 0xb0], 0xdcc5dd5
0x1169: push rbp
0x116a: mov rbp, rsp
0x116d: push rbx
0x116e: sub rsp, 0x28
0x1172: mov qword ptr [rbp - 0x28], rdi
0x1176: mov qword ptr [rbp - 0x30], rsi
0x117a: mov dword ptr [rbp - 0x14], 0
0x1181: jmp 0x11a9
0x1183: mov eax, dword ptr [rbp - 0x14]
0x1186: movsxd rdx, eax
0x1189: mov rax, qword ptr [rbp - 0x28]
0x118d: add rax, rdx
0x1190: movzx edx, byte ptr [rax]
0x1193: mov eax, dword ptr [rbp - 0x14]
0x1196: movsxd rcx, eax
0x1199: mov rax, qword ptr [rbp - 0x30]
0x119d: add rax, rcx
0x11a0: sub edx, 0x63
0x11a3: mov byte ptr [rax], dl
0x11a5: add dword ptr [rbp - 0x14], 1
0x11a9: mov eax, dword ptr [rbp - 0x14]
0x11ac: movsxd rbx, eax
0x11af: mov rax, qword ptr [rbp - 0x28]
0x11b3: mov rdi, rax
0x11b6: call 0x1030
0x11bb: cmp rbx, rax
0x11be: jb 0x1183
0x11c0: nop
0x11c1: nop
0x11c2: mov rbx, qword ptr [rbp - 8]
0x11c6: leave
0x11c7: ret
세 어셈블리어 코드는 위에 있는 세 가지의 process 함수를 capstone을 이용하여 출력한 것이다. 여러분은 두 어셈블리 코드의 차이를 알겠는가? 위의 코드를 보면 shr과 shl의 명령어를 사용하는 것을 볼 수 있고, xor 명령어를 사용하는 것을 볼 수 있다. 또한 세 번째 코드는 sub를 사용하는 것을 볼 수 있다. 따라서 해당 명령어들을 찾고 오프코드를 파싱해서 임의의 값을 찾아 역연산을 할 수 있다.
0x11e3: push rbp
0x11e4: mov rbp, rsp
0x11e7: sub rsp, 0xc0
0x11ee: movabs rax, 0x3ba9a8bbb6b221b8
0x11f8: movabs rdx, 0x3ba43298ac9a3a19
0x1202: mov qword ptr [rbp - 0xc0], rax
0x1209: mov qword ptr [rbp - 0xb8], rdx
0x1210: mov qword ptr [rbp - 0xb0], 0x249aba34
0x121b: mov qword ptr [rbp - 0xa8], 0
0x1226: mov qword ptr [rbp - 0xa0], 0
0x1231: mov qword ptr [rbp - 0x98], 0
0x123c: mov word ptr [rbp - 0x90], 0
0x1245: lea rax, [rbp - 0x40]
0x1249: mov rsi, rax
0x124c: lea rax, [rip + 0xdb1]
0x1253: mov rdi, rax
0x1256: mov eax, 0
0x125b: call 0x1060
0x1260: lea rax, [rbp - 0x40]
0x1264: mov rdi, rax
0x1267: call 0x1030
0x126c: cmp rax, 0x14
0x1270: je 0x128d
0x1272: lea rax, [rip + 0xd8e]
0x1279: mov rdi, rax
0x127c: mov eax, 0
0x1281: call 0x1040
0x1286: mov eax, 0
0x128b: jmp 0x12ee
0x128d: lea rdx, [rbp - 0x80]
0x1291: lea rax, [rbp - 0x40]
0x1295: mov rsi, rdx
0x1298: mov rdi, rax
0x129b: call 0x1169
0x12a0: lea rcx, [rbp - 0xc0]
0x12a7: lea rax, [rbp - 0x80]
0x12ab: mov edx, 0x14
0x12b0: mov rsi, rcx
0x12b3: mov rdi, rax
0x12b6: call 0x1050
0x12bb: test eax, eax
0x12bd: jne 0x12d5
0x12bf: lea rax, [rip + 0xd46]
0x12c6: mov rdi, rax
0x12c9: mov eax, 0
0x12ce: call 0x1040
0x12d3: jmp 0x12e9
0x12d5: lea rax, [rip + 0xd2b]
0x12dc: mov rdi, rax
0x12df: mov eax, 0
0x12e4: call 0x1040
0x12e9: mov eax, 0
0x12ee: leave
0x12ef: ret
비교 배열은 main 함수에서 찾을 수 있다.
from pwn import *
from capstone import *
def rev(xv, rv, sv):
if xv:
for i in range(20):
ans[i] ^= xv
elif rv:
for i in range(20):
ans[i] = (ans[i] << rv | ans[i] >> (8 - rv)) & 0xff
elif sv:
for i in range(20):
ans[i] += sv
ans[i] &= 0xff
p = process('./binary')
e = ELF('./binary', checksec = False)
md = Cs(CS_ARCH_X86, CS_MODE_64)
elf = ELF('./binary', checksec = False)
func_addr = elf.symbols['main']
func_size = elf.functions['main'].size
func_code = elf.read(func_addr, func_size)
mov_cnt = 0
ans = b''
for i in md.disasm(func_code, func_addr):
print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")
if i.mnemonic == 'movabs':
ans += bytes.fromhex(i.op_str[7:])[::-1]
if i.mnemonic == 'mov':
mov_cnt += 1
if mov_cnt == 4:
ans += bytes.fromhex(i.op_str[-8:].replace('x', '0'))[::-1]
break
ans = list(ans)
elf = ELF('./binary', checksec = False)
func_addr = elf.symbols['process']
func_size = elf.functions['process'].size
func_code = elf.read(func_addr, func_size)
xor_val = 0
ror_val = 0
sub_val = 0
for i in md.disasm(func_code, func_addr):
# print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")
if i.mnemonic == 'shr':
ror_val = int(i.op_str[4:], 16)
break
if i.mnemonic == 'xor':
xor_val = int(i.op_str[5:], 16) & 0xff
break
if i.mnemonic == 'sub' and i.op_str[:3] == 'edx':
sub_val = int(i.op_str[5:], 16)
print(len(ans))
rev(xor_val, ror_val, sub_val)
pay = ''.join([chr(i) for i in ans])
# print(p)
p.sendline(pay)
p.interactive()
위와 같은 코드로 일반화 시켜 하나의 바이너리를 익스했으므로 반복문을 돌리면 풀이할 수 있을 것이다.
very scalable
╭─tuplest@BOOK-8JHQNP0CQB ~/Hack/ctf/CCE2024/verysc
╰─$ ./problem
zsh: exec format error: ./problem
실행시켜 보면 심상치 않은 것을 볼 수 있다.
FROM arm64v8/ubuntu:20.04
COPY ./problem /usr/local/bin/
CMD ["sh"]
함께 주어진 Dockerfile을 보면 arm의 프로그램인 것을 알 수 있다.
╭─tuplest@BOOK-8JHQNP0CQB ~/Hack/ctf/CCE2024/verysc
╰─$ qemu-aarch64-static -L /usr/aarch64-linux-gnu ./problem
cce2024{ignoring_sve_is_a_meme}
qemu를 통해 x86-64에서도 실행시켜 볼 수 있는데 실행시키면 플래그가 나온다. (...) 참고로 이 문제는 딱 1솔이 나온 문제이다. ida를 통해 분석해보기 전에 꼭 파일은 실행시켜보도록 하자...
Interlag
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v7; // rax
__int64 v8; // rax
__int64 v9; // rbx
unsigned int v10; // eax
__int64 v11; // rax
int i; // [rsp+1Ch] [rbp-64h]
int j; // [rsp+1Ch] [rbp-64h]
int v14; // [rsp+20h] [rbp-60h]
unsigned int v15; // [rsp+24h] [rbp-5Ch]
char *s; // [rsp+28h] [rbp-58h]
_DWORD *v17; // [rsp+30h] [rbp-50h]
void *v18; // [rsp+38h] [rbp-48h]
_BYTE v19[16]; // [rsp+40h] [rbp-40h] BYREF
_BYTE v20[24]; // [rsp+50h] [rbp-30h] BYREF
unsigned __int64 v21; // [rsp+68h] [rbp-18h]
v21 = __readfsqword(0x28u);
if ( a1 > 1 )
{
sub_13B6(v19, 145LL, 76LL, 48LL, 257LL);
s = a2[1];
v14 = strlen(s);
v17 = malloc(4LL * v14);
v18 = malloc(4LL * v14);
memset(v18, 0, 8uLL);
for ( i = 0; i < v14; ++i )
v17[i] = s[i];
sub_1298(v20, v17, (unsigned int)(v14 - 1), 257LL);
for ( j = 0; j < v14; ++j )
{
do
v15 = sub_143A(v19, 1LL, 257LL);
while ( (unsigned __int8)sub_14A9(v15, v18) );
*((_DWORD *)v18 + j) = v15;
v7 = std::operator<<<std::char_traits<char>>(&std::cout, "(");
v8 = std::ostream::operator<<(v7, v15);
v9 = std::operator<<<std::char_traits<char>>(v8, ", ");
v10 = sub_1330(v20, v15);
v11 = std::ostream::operator<<(v9, v10);
std::operator<<<std::char_traits<char>>(v11, ") ");
}
std::ostream::operator<<(&std::cout, &std::endl<char,std::char_traits<char>>);
return 0LL;
}
else
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "usage : ");
v4 = std::operator<<<std::char_traits<char>>(v3, *a2);
v5 = std::operator<<<std::char_traits<char>>(v4, " [message to encrypt]");
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
return 0LL;
}
}
어김없이 등장한 선형방정식 풀이 문제이다.
__int64 __fastcall sub_143A(__int64 a1, int a2, int a3)
{
int v5; // [rsp+18h] [rbp-8h]
int i; // [rsp+1Ch] [rbp-4h]
v5 = sub_13FA(a1);
if ( a3 > *(_DWORD *)(a1 + 12) )
{
for ( i = 0; i <= a3 / *(_DWORD *)(a1 + 12); ++i )
v5 += sub_13FA(a1);
}
return (unsigned int)((v5 + a2) % a3);
}
핵심 로직만 간략하게 분석해보면 입력값이랑 곱해서 더한 후 비교한다. 딱 봐도 선형 시스템 구축이라는 것을 알 수 있다.
따라서 output.txt의 첫 번째 항이 곱해지는 계수, 두 번째 항이 배교 배열인 것을 수 있다.
from sage.all import *
c = [(18, 145) , (56, 95) , (117, 23) , (127, 207) , (116, 253) , (51, 224) , (251, 187), (31, 31), (16, 83) , (161, 86) , (130, 30) , (87, 224) , (160, 179), (54, 209), (222, 13), (140, 59), (76, 24), (95, 85) , (254, 64), (2, 56), (125, 12), (221, 38), (64, 206), (211, 114), (75, 72), (19, 33), (132, 40), (239, 134), (147, 78), (94, 250), (178, 79) , (137, 132) , (105, 33) , (243, 41) , (194, 64) , (68, 253), (1, 221) , (49, 147), (99, 53) , (44, 207) , (233, 219) , (205, 105) , (133, 250) , (58, 241) , (12, 31) , (114, 49) , (156, 108)]
flag_len = len(c)
field = 0x101
A = []
B = []
for a, b in c:
row = []
for i in range(flag_len):
row.append(pow(a, i, field))
B.append(b)
A.append(row)
A = matrix(IntegerModRing(field), A)
B = vector(IntegerModRing(field), B)
sol = A.solve_right(B)
print(''.join([chr(i) for i in list(sol)]))
위와 같이 from sage.all import *의 메서드들을 이용하여 플래그를 구할 수 있다.
Comment
리버싱 문제는 이 외의 Dying Message라는 문제와 Intel's Dream이라는 문제가 있었는데 생략하도록 하겠다. 여기서 배워야 할 교훈은,
- 바이너리가 주어지면 일단 실행시켜 보자.
- 기본적으로 선형방정식을 풀 수 있어야 한다.
잘 알아두도록 하자.
1학년들이 이번 CCE 예선에 통과한 것으로 알고 있는데 CCE 본선에는 Live Fire라는 다른 대회와는 차별화된 개념이 존재하기에 몰랐다면 찾아보길 바란다.