having fun with exploit 200 ctf 2013
This post will be more about explorint techniquies rather that reaching direct goals. It’s about the binary expl200 (SHA1: 873060b493f8627ec598dc84352064f49d005116) yurket gave me the other day.
So far, we have expl200 with SUID-root vilnerable binary to get local root.
Before running it let’s look at it in some disassembler. I’m a bit hardcore, thus used objdump -D -R -r expl200.
It’s a typical C-written binary. I’ve annotated output a bit in comments. We are interested in 2 parts: main function and cycle data.
>:
0000000000400614 <mainpush %rbp
400614: 55 %rsp,%rbp
400615: 48 89 e5 mov 20 02 00 00 sub $0x220,%rsp ; alloc 544 bytes on stack (%rbp points to bottom, %rsp to top), of which:
400618: 48 81 ec mov %fs:0x28,%rax
40061f: 64 48 8b 04 25 28 00
400626: 00 00 %rax,-0x8(%rbp) ; store '-fstack-protector' 8-byte canary, 536 stack bytes left
400628: 48 89 45 f8 mov xor %eax,%eax ; cleanse canary (otherwise it's broad infoleak)
40062c: 31 c0 01 jmp 400631 <main+0x1d> ; GOTO again:
40062e: eb nop ; again_:
400630: 90 -0x210(%rbp),%rsi ; again: char buffer[512]; (+8 bytes unused); 16 bytes of stack left
400631: 48 8d b5 f0 fd ff ff lea 00 00 00 00 mov $0x0,%eax ;
400638: b8 40 00 00 00 mov $0x40,%edx ;
40063d: ba %rsi,%rdi ;
400642: 48 89 f7 mov %rdx,%rcx ; __builtin_memset (buffer, 0, sizeof (buffer)); just a memset, but not from libc
400645: 48 89 d1 mov 48 ab rep stos %rax,%es:(%rdi)
400648: f3 09 20 00 mov 0x2009e6(%rip),%rax # 601038 <__bss_start>
40064b: 48 8b 05 e6 %rax,%rdx ; arg1 = 'stdin@@GLIBC_2.2.5'
400652: 48 89 c2 mov -0x210(%rbp),%rax ; arg2 = 'buffer'
400655: 48 8d 85 f0 fd ff ff lea 01 00 00 mov $0x1ff,%esi ; arg3 = 511
40065c: be ff %rax,%rdi
400661: 48 89 c7 mov 400510 <fgets@plt> ; fgets (stdin, buffer, 511);
400664: e8 a7 fe ff ff callq -0x210(%rbp),%rax
400669: 48 8d 85 f0 fd ff ff lea 00 movzbl (%rax),%eax
400670: 0f b6 %al,%al
400673: 84 c0 test jne 400681 <main+0x6d> ; if (buffer[0] != '\0') goto fine;
400675: 75 0a $0xffffffff,%edi
400677: bf ff ff ff ff mov 9f fe ff ff callq 400520 <exit@plt> ; exit (-1);
40067c: e8 -0x210(%rbp),%rax ; fine: arg1 = 'buffer'
400681: 48 8d 85 f0 fd ff ff lea %rax,%rdi
400688: 48 89 c7 mov 00 00 00 00 mov $0x0,%eax
40068b: b8 5b fe ff ff callq 4004f0 <printf@plt> ; printf (buffer); GAH!
400690: e8 09 20 00 mov 0x2009b5(%rip),%eax # 601050 <cycle>
400695: 8b 05 b5 00 00 and $0xffff,%eax
40069b: 25 ff ff %eax,-0x218(%rbp) ; int lo16 = cycle & 0xFFFF; low half; 12 bytes on stack left
4006a0: 89 85 e8 fd ff ff mov 09 20 00 mov 0x2009a4(%rip),%eax # 601050 <cycle>
4006a6: 8b 05 a4 10 shr $0x10,%eax
4006ac: c1 e8 %eax,-0x214(%rbp) ; int hi16 = (uint32_t)(cycle >> 16); high half; 8 bytes on stack left
4006af: 89 85 ec fd ff ff mov -0x218(%rbp),%eax ; uint32_t v1 = lo16;
4006b5: 8b 85 e8 fd ff ff mov %eax,%edx
4006bb: 89 c2 mov 95 e8 fd ff ff imul -0x218(%rbp),%edx
4006bd: 0f af -0x214(%rbp),%eax ; uint32_t v2 = hi16;
4006c4: 8b 85 ec fd ff ff mov 85 ec fd ff ff imul -0x214(%rbp),%eax
4006ca: 0f af $0xffffffe3,%eax,%eax
4006d1: 6b c0 e3 imul %edx,%eax
4006d4: 01 d0 add 01 cmp $0x1,%eax ; if (v1*v1 - 29 * v2 * v2 != 1)
4006d6: 83 f8 400630 <main+0x1c> ; goto again_;
4006d9: 0f 85 51 ff ff ff jne 07 40 00 mov $0x4007dc,%edi
4006df: bf dc 4004e0 <system@plt> ; system ("exec /bin/sh"); aha! :]
4006e4: e8 f7 fd ff ff callq 42 ff ff ff jmpq 400630 <main+0x1c> ; goto again_;
4006e9: e9 nop
4006ee: 90 nop
4006ef: 90
...>:; int32_t cycle
0000000000601050 <cycle
...@@GLIBC_2.2.5>:
0000000000601038 <stdin
... 601038: R_X86_64_COPY stdin
Once again, (a bit cleaned) program looks like that:
#include <stdio.h>
#include <stdlib.h>
//#include <string.h> /* memset */
int cycle = 0;
int main()
{
int lo, hi;
char buff[512];
do
{
(buff, 0, 512); /* force rep: stos even on unoptimized builds */
__builtin_memset (buff, sizeof(buff) - 1, stdin);
fgets if (buff[0] == '\0')
(-1);
exit (buff);
printf = cycle & 0xFFFF;
lo = cycle >> 16;
hi } while (lo*lo - 29*hi*hi != 1);
("exec /bin/sh");
system return 0;
}
First, it tries to solve equation of form “x^2 - 29*y^2 == 1”. It’s so-called Pell’s equation, which is common problem to solve in PE tasks.
Some of their first solutions:
*Euler> solve_pell 29 PlusOne
(x,y):
(9801,1820)
(192119201,35675640)
(3765920568201,699313893460)
(73819574785756801,13707950903927280)
(1447011301184484245001,268703252919468649100)
(28364315451998685384752801,5267121150019473555730920)
(555997310043066929727440160201,103246108513978467719968844740)
(10898659243099882504518596635507201,2023830213823884774227355738862560)
(213635517927246586810506601521771993801,39671119748129680830426159473215056380)
...
The only ones that fit into 16 bits are firs two solutions: (9801,1820), or (0x2649, 0x071C). Thus one of solutions is to put 0x071C2649 value into ’cycle’global varioable.
The straightforward way is to exploit uncontrolled ‘printf’ argument vulnerability. Most of arguments in format string effect memory read accesses, but not writes. For example:
("%s%i%d", whatever...);
printf // can only read from memory coded in 'whatever'
But:
("123%n456%n", pi1, pi2);
printf // will effect in *(int*)pi1 = 3; *(int*)pi2 = 6;
Or even better:
("123%hn456%hn", pi1, pi2);
printf // will effect in *(short*)pi1 = 3; *(short)pi2 = 6;
Thus we have mechanism of writing random 2-byte values into memory locations.
Now, the interesting part: how to control ‘printf’ arguments?
Let’s look at x86_64 C function call ABI first.
The following example
#include <stdio.h>
void test()
{
("", 2,3,4,5,6,7,8,9,10);
printf }
gets assembled into (gcc -fomit-frame-pointer -S arg_test.c -o arg_test.S)
.section .rodata
.LC0:
.string ""
.text
:
test40, %rsp ; reserve 8 * 8 on stack. always-16-byte stack alignment goes from SSE requirement.
subq $10, 24(%rsp) ; 10: , go on stack
movl $9, 16(%rsp) ; 9: , all
movl $8, 8(%rsp) ; 8: , arguments
movl $7, (%rsp) ; 7: starting from 7-th
movl $6, %r9d
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $.LC0, %edi
movl $0, %eax
movl $
call printf40, %rsp
addq $ ret
Thus in order to get access to our stack we need to throw out (or use wisely) values situated in registers.
It’s time to look at the stack and register layout right before printf call (eip=0x400690) moment a bit closer:
%rsp+000h(or %rbp-0x220): [ uninitialized value, created, but not used by our program. can be handy! ]
%rsp+0008(or %rbp-0x218): [ pair of values: int lo16; int hi16; ]
%rsp+010h(or %rbp-0x210): [ buffer, 512 bytes ]
%rsp+210h(or %rbp-0x010): [ 8 bytes unused ]
%rsp+218h(or %rbp-0x008): [ stack protector canary ]
%rsp+220h(or %rbp-0x000): [ 8 bytes unused ]
%rsp+228h(or %rbp+0x008): [ previous frame (%rbp) ]
...
() call.
registers are in the state left from fgets, but I don't care for now. Can be handy as well
And our printf call will look like that:
( %rdi = %rbp-210h /*aka buffer*/,
printf , %rsi /* left from fgets */
, %rdx /* left from fgets */
, %rcx /* left from fgets */
, %r8 /* left from fgets */
, %r9 /* left from fgets */
, %rbp-0x220
, %rbp-0x218
, %rbp-0x210 /* aha, 'buffer' met once again! */
, %rbp-0x208
...
);
Our goal is to control format string the way we could inject nice bits there,
Suppose, we pass the followint thing as a buffer:
("%c%c%c%c%c%c%c%c\n" /* 8 times */ "%p"); printf
It will show us value for ‘%rbp-0x210’. Program won’t crashm thus we will be able to pass it another format string with achieved info.
To be continued :]