Skip to content

[Writeup] Hackappatoi CTF 2022

Posted on:December 15, 2022 at 11:04 AM

1.

이번에 팀에서 참가해주신 분들이 많았다. 20등 안에 들기도 했고 개인적으로는 힘 빼고 참가했지만 푼 문제도 많았다. 결과와는 무관하게 정말정말 재밌게 참가한 CTF다. 운영진이 로마 출신인데 모든 문제가 술 테마였다. 이벤트도 많았고 공식 디스코드 서버 분위기도 좋았다. 술 사진과 함께 HCTF라고 적어보내면 10점을 보너스로 준다고 해서 다마신 몬스터캔 10캔이랑 밤샘하면서 먹은 불닭면 사진 보냈더니 you deserve it 하면서 주류 아닌데도 10점 주셨다. 재밌는 것도 많이 구경했다.

  1. DFIR 문제를 ChatGPT로 푼 사람이 있었다.
  2. 사진이랑 근처 대학 이름만 알려주고 장소 찾는 문제가 있었는데 어떤 사람이 그 대학 텔레그램 가입해서 아무나 붙잡고 여기 어디냐고 물어봐서 풀었다고 한다.

쉬운 문제는 너무 삭삭 풀리고 안 풀리는 건 너무 안풀리다 보니, 주력인 웹에 집중을 못했다. OSINT나 misc가 꽤 있었는데 다 안풀려서 이것저것 찍먹하다가 좀 지치기도 했다. 크립토도 없었고; 운영진이 운영은 잘했는데 문제 범위 면에서 살짝 아쉬웠다. 웹은 너무 쉬워서 풀이를 쓸 필요가 없을 정도거나 풀지 못했다. 대신 리버싱 로드맵을 드림핵에서 4일만에 다 끝내고 갔더니 리버싱을 다 풀었다. 돌아보니까 기억에 남을 정도로 기술적으로 한 게 없다. ㅎㅎ; 적어도 리버싱 몇개라도 라업을 적어보고자 한다. 너무 쉬웠던 문제 세네개는 제외하고 풀이를 적는다.

2. THE CHALLS

Braindrunk - misc

attachment.txt를 하나 던져준다.

🥂🍷🍷🍷🍷🍷🍷🍷🍷🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍺🍺🍺🥂🥂🍷🍷🍷🍷🍷🍷🍷🍷🍷🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍷🥂🥂🍷🍷🍷🍷🍷🍷🍷🍷🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍷🍷🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍺🍺🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🍺🍺🍺🍺🥂🥂🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🥂🍷🥃🍹🥂🥂🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍸🍹🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🍺🥂(생략)

까보면 이렇게 생겼다. brainfuck 코드로 변환하면 뭐가 될 것 같다. 침착하고 brainfuck의 공식 문서를 읽고와서 각 이모지의 개수를 세본다. 🥂는 602개고 🍺는 1189개, 🍹는 593개, 🍷는 358개, 🥃는 54개, 🍾는 22개, 🍸는 54개다.

  1. [ 와 ]는 서로 짝이다. 그러면 54개씩 있는 🥃, 🍸는 각각 먼저 나온 순서대로 [ 와 ] 다.
  2. input은 없을거라는 생각이 든다. 이모지는 총 7개니까 , 는 배제한다.
  3. output은 . 인데 출력값이 22개면 문자열 한 줄일 거라는 생각이 든다. 🍾가 . 라고 가정하자.
  4. 🥂 🍹의 양이 비슷하다. 포인터 연산자인 >, < 라고 추론해본다.
  5. 남는 🍺, 🍷는 +, - 이다.

이런식으로 모든 코드를 치환하고 돌려보면 다음과 같은 출력값이 나온다.

bf

난 그래서 처음에 “Do you want a drink?” 가 플래그인줄 알았다. 혹시나 해서 플래그 포맷으로 감싸고 넣어봤더니 아니다. 다른 곳에서 다시 돌리면 메모리값에 이런 결과가 들어가있다.

bf2

그러면 제대로 된 플래그가 나올 때까지 연산자들을 움직여본다. 메모리에 출력되지 않은 값이 들어있다면, 출력하는 연산자가 꼭 필요할까? 코드의 모든 . 연산자를 > 로 바꿔본다. 593개에 22 더하면 <의 수인 602랑 비슷해지기 때문이다.

bf3

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하면 플래그가 나온다.

asdf