1.
이번에 팀에서 참가해주신 분들이 많았다. 20등 안에 들기도 했고 개인적으로는 힘 빼고 참가했지만 푼 문제도 많았다. 결과와는 무관하게 정말정말 재밌게 참가한 CTF다. 운영진이 로마 출신인데 모든 문제가 술 테마였다. 이벤트도 많았고 공식 디스코드 서버 분위기도 좋았다. 술 사진과 함께 HCTF라고 적어보내면 10점을 보너스로 준다고 해서 다마신 몬스터캔 10캔이랑 밤샘하면서 먹은 불닭면 사진 보냈더니 you deserve it 하면서 주류 아닌데도 10점 주셨다. 재밌는 것도 많이 구경했다.
- DFIR 문제를 ChatGPT로 푼 사람이 있었다.
- 사진이랑 근처 대학 이름만 알려주고 장소 찾는 문제가 있었는데 어떤 사람이 그 대학 텔레그램 가입해서 아무나 붙잡고 여기 어디냐고 물어봐서 풀었다고 한다.
쉬운 문제는 너무 삭삭 풀리고 안 풀리는 건 너무 안풀리다 보니, 주력인 웹에 집중을 못했다. OSINT나 misc가 꽤 있었는데 다 안풀려서 이것저것 찍먹하다가 좀 지치기도 했다. 크립토도 없었고; 운영진이 운영은 잘했는데 문제 범위 면에서 살짝 아쉬웠다. 웹은 너무 쉬워서 풀이를 쓸 필요가 없을 정도거나 풀지 못했다. 대신 리버싱 로드맵을 드림핵에서 4일만에 다 끝내고 갔더니 리버싱을 다 풀었다. 돌아보니까 기억에 남을 정도로 기술적으로 한 게 없다. ㅎㅎ; 적어도 리버싱 몇개라도 라업을 적어보고자 한다. 너무 쉬웠던 문제 세네개는 제외하고 풀이를 적는다.
2. THE CHALLS
- Braindrunk - misc
- eXclusive club - rev
- Hackappa_rev - rev
- Best videogame - rev
Braindrunk - misc
attachment.txt를 하나 던져준다.
🥂🍷🍷🍷🍷🍷🍷🍷🍷🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍺🍺🍺🥂🥂🍷🍷🍷🍷🍷🍷🍷🍷🍷🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍷🥂🥂🍷🍷🍷🍷🍷🍷🍷🍷🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍷🍷🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍺🍺🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍺🍺🍺🍺🥂🥂🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂(생략)
까보면 이렇게 생겼다. brainfuck 코드로 변환하면 뭐가 될 것 같다. 침착하고 brainfuck의 공식 문서를 읽고와서 각 이모지의 개수를 세본다. 🥂는 602개고 🍺는 1189개, 🍹는 593개, 🍷는 358개, 🥃는 54개, 🍾는 22개, 🍸는 54개다.
- [ 와 ]는 서로 짝이다. 그러면 54개씩 있는 🥃, 🍸는 각각 먼저 나온 순서대로 [ 와 ] 다.
- input은 없을거라는 생각이 든다. 이모지는 총 7개니까 , 는 배제한다.
- output은 . 인데 출력값이 22개면 문자열 한 줄일 거라는 생각이 든다. 🍾가 . 라고 가정하자.
- 🥂 🍹의 양이 비슷하다. 포인터 연산자인 >, < 라고 추론해본다.
- 남는 🍺, 🍷는 +, - 이다.
이런식으로 모든 코드를 치환하고 돌려보면 다음과 같은 출력값이 나온다.
난 그래서 처음에 “Do you want a drink?” 가 플래그인줄 알았다. 혹시나 해서 플래그 포맷으로 감싸고 넣어봤더니 아니다. 다른 곳에서 다시 돌리면 메모리값에 이런 결과가 들어가있다.
그러면 제대로 된 플래그가 나올 때까지 연산자들을 움직여본다. 메모리에 출력되지 않은 값이 들어있다면, 출력하는 연산자가 꼭 필요할까? 코드의 모든 . 연산자를 > 로 바꿔본다. 593개에 22 더하면 <의 수인 602랑 비슷해지기 때문이다.
eXclusive club - rev
리버싱도 쉬운 편이었다. 입문한지 며칠 안됐으니까 기록용으로 풀이를 쓴다. main 파일을 ida에서 잘 disassemble 해보자.
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // edx
__int64 v5; // [rsp+18h] [rbp-28h]
char s[24]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-8h]
v7 = __readfsqword(0x28u);
printf("Welcome to our eXclusive club.\nType your password to join us.\n>");
__isoc99_scanf("%s", s);
v5 = obfuscation(s);
v3 = strlen(s);
if ( (unsigned int)check_access(v5, v3) )
puts("Access granted, bro!");
else
puts("You can't join us this time...");
return 0;
}
들어오는 문자열을 obfuscation하고 문자열의 길이와 함께 check_access로 보낸다. check_access는 뭐하는 애고 obfuscation는 뭐하는 애인지 한 번 보자.
_DWORD *__fastcall obfuscation(const char *a1)
{
size_t v1; // rax
int i; // [rsp+10h] [rbp-20h]
_DWORD *v4; // [rsp+18h] [rbp-18h]
v1 = strlen(a1);
v4 = malloc(4 * v1);
for ( i = 0; i < strlen(a1); ++i )
v4[i] = a1[i] ^ 0x41;
return v4;
}
v4를 리턴하는데 v4는 0x41 즉 65와 각 문자열의 모든 글자를 xor 연산한 결과값이다. 그건 어디로 들어가냐..
__int64 __fastcall check_access(__int64 a1, int a2)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-74h]
int v4[26]; // [rsp+20h] [rbp-70h]
unsigned __int64 v5; // [rsp+88h] [rbp-8h]
v5 = __readfsqword(0x28u);
result = 0LL;
v4[0] = 41;
v4[1] = 34;
v4[2] = 53;
v4[3] = 39;
v4[4] = 58;
v4[5] = 36;
v4[6] = 25;
v4[7] = 34;
v4[8] = 45;
v4[9] = 20;
v4[10] = 116;
v4[11] = 112;
v4[12] = 55;
v4[13] = 114;
v4[14] = 30;
v4[15] = 113;
v4[16] = 51;
v4[17] = 31;
v4[18] = 15;
v4[19] = 113;
v4[20] = 53;
v4[21] = 126;
v4[22] = 60;
if ( a2 != 23 )
return 0LL;
for ( i = 0; i <= 22; ++i )
{
result = (unsigned int)v4[i];
if ( *(_DWORD *)(4LL * i + a1) != (_DWORD)result )
return 0LL;
if ( i == 22 )
return '\x01';
}
return result;
}
여기에 저장된 값들과 같은지 체크한다. 설명할 거리가 별로 없다. 파이썬으로 다시 써주자.
myList = [41, 34, 53, 39, 58, 36, 25, 34, 45, 20, 116, 112, 55, 114, 30 , 113 , 51, 31, 15, 113, 53, 126, 60]
for i in myList:
print(chr(i ^0x41), end="")
Hackappa_rev - rev
똑같이 main을 disassemble해주자.
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3[2]; // [rsp+8h] [rbp-8h] BYREF
puts("Welcome to the Hackappapub ! ");
while ( 1 )
{
puts("What do you want to drink ?");
puts("1)Hackappa beer");
puts("2)Hackappa Cocktail");
puts("3)Hackappacola ");
v3[0] = 0;
__isoc99_scanf("%d", v3);
holder(v3[0]);
if ( b == 6 && c == 6 && h == 6 )
break;
if ( b == 10 && c == 7 && h == 11 )
decrypt();
}
v3[1] = out();
exit(1);
}
decrypt
라는 함수가 하나 보인다.
__int64 decrypt()
{
__int64 v1[3]; // [rsp+0h] [rbp-20h] BYREF
int i; // [rsp+1Ch] [rbp-4h]
qmemcpy(v1, "KFWI~6}bGuxqnbU6y", 17);
printf("%s", (const char *)v1);
for ( i = 0; i <= 99 && *((_BYTE *)v1 + i); ++i )
*((_BYTE *)v1 + i) -= 3;
printf("%s}\n", (const char *)v1);
return 0LL;
}
decrypt는 "KFWI~6}bGuxqnbU6y"
를 받아서 위와 같은 비트연산을 수행한 뒤 뱉어낸다. 파이썬으로 코드를 다시 쓰고 돌려주면 플래그가 나온다.
v1 ="KFWI~6}bGuxqnbU6y"
for i in range(100):
print(chr(ord(v1[i % len(v1)]) - 3), end="")
print("}")
Best Videogame - rev
main 함수는 다음과 같다.
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *v4; // rsp
const char **v5; // [rsp+8h] [rbp-F0h] BYREF
int v6; // [rsp+14h] [rbp-E4h]
int k; // [rsp+24h] [rbp-D4h]
int j; // [rsp+28h] [rbp-D0h]
int i; // [rsp+2Ch] [rbp-CCh]
int v10; // [rsp+30h] [rbp-C8h]
unsigned int v11; // [rsp+34h] [rbp-C4h]
__int64 v12; // [rsp+38h] [rbp-C0h]
__int64 v13; // [rsp+40h] [rbp-B8h]
const char ***v14; // [rsp+48h] [rbp-B0h]
__int64 v15; // [rsp+50h] [rbp-A8h]
int v16[26]; // [rsp+58h] [rbp-A0h]
unsigned __int64 v17; // [rsp+C0h] [rbp-38h]
v6 = argc;
v5 = argv;
v17 = __readfsqword(0x28u);
if ( argc == 3 )
{
v16[0] = 61;
v16[1] = 164;
v16[2] = 170;
v16[3] = 39;
v16[4] = 239;
v16[5] = 123;
v16[6] = 13;
v16[7] = 77;
v16[8] = 104;
v16[9] = 196;
v16[10] = 194;
v16[11] = 153;
v16[12] = 26;
v16[13] = 117;
v16[14] = 248;
v16[15] = 2;
v16[16] = 50;
v16[17] = 15;
v16[18] = 139;
v16[19] = 116;
v16[20] = 25;
v16[21] = 151;
v16[22] = 157;
v10 = strlen(v5[2]);
v11 = strlen(v5[1]);
if ( !strcmp(v5[1], "play") )
{
if ( v10 == 23 )
{
v12 = generatekey(v5[1]);
v13 = 22LL;
v4 = alloca(96LL);
v14 = &v5;
for ( i = 0; i < v10; ++i )
*((_DWORD *)v14 + i) = v5[2][i];
v15 = RC4(v12, v14, v11, (unsigned int)v10);
for ( j = 0; ; ++j )
{
if ( j >= v10 )
return 0;
if ( *(_DWORD *)(4LL * j + v15) != v16[j] )
{
puts("You got the console right, but not the game :(\nTry again!");
return 0;
}
if ( j == v10 - 1 )
break;
}
printf("Correct! Here is your prize!\nhctf{");
for ( k = 0; k < v10; ++k )
putchar(*((_DWORD *)v14 + k));
puts("}");
return 0;
}
else
{
puts("You got the console right, but not the game :(\nTry again!");
return 0;
}
}
else
{
puts("Mmh the console is not correct :/\nTry again!");
return 0;
}
}
else
{
puts("Discover my favourite videogame saga!\nUsage:\n\t./bestvideogame <console> <myfavouritesaga>");
return 0;
}
}
차근차근 읽어보자.
프로그램이 처음 받는 인자값은 v5[1] 이고, “play”와 비교한다.
v12 = generatekey(v5[1]);
v12는 나중에 RC4라는 함수에 들어간다. RC4는 stream cipher의 일종이다. RC4와 관련이 있는 문젠가보다.
_DWORD *__fastcall generatekey(const char *a1)
{
int i; // [rsp+10h] [rbp-10h]
int v3; // [rsp+14h] [rbp-Ch]
_DWORD *v4; // [rsp+18h] [rbp-8h]
v3 = strlen(a1);
v4 = malloc(4LL * v3);
for ( i = 0; i < v3; ++i )
v4[i] = a1[i];
return v4;
}
generatekey
는 이렇게 생겼는데, 결국 주어진 인자값에서 바이트들을 복사하고 저장하는 일을 한다.
v12는 결국 play
라는 키가 되고, 이걸 키값으로 v16의 값들을 decode하면 플래그가 나온다.