vendredi 26 janvier 2018

Solving a CTF chall the [easy|lazy] way (FIC2018)

During the FIC2018, there was a CTF. One of the challenge was reverse, and we were given an a.out file.

1/ First steps:

mitsurugi@dojo:~/chall/FIC2018/v24$ ls -l a.out
-rwxr-xr-x 1 mitsurugi mitsurugi 15568 Jan 24 14:30 a.out
mitsurugi@dojo:~/chall/FIC2018/v24$ file a.out 
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, for GNU/Linux 2.6.32, BuildID[sha1]=4cde05f176c1b36218ba56a4f1fb1249ad1a8c1b, not stripped
mitsurugi@dojo:~/chall/FIC2018/v24$ ./a.out 

Ok, so it's a crackme. Let's toy a little bit with it:

mitsurugi@dojo:~/chall/FIC2018/v24$ ./a.out 
ENTER PASS ::aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
FAILEDnmitsurugi@dojo:~/chall/FIC2018/v24$ aaaaaaaaaaaaaaaaaaaa
bash: aaaaaaaaaaaaaaaaaaaa: command not found

Weird. It seems that it take only 12 chars in consideration:
mitsurugi@dojo:~/chall/FIC2018/v24$ ./a.out 
ENTER PASS :123456123456abcde
FAILEDnmitsurugi@dojo:~/chall/FIC2018/v24$ abcde
bash: abcde: command not found

Ok, time to dig in:

mitsurugi@dojo:~/chall/FIC2018/v24$ nm -A a.out 
a.out:00000000006011c8 d _DYNAMIC
a.out:00000000006013a0 d _GLOBAL_OFFSET_TABLE_
a.out:0000000000400ee0 R _IO_stdin_used
a.out:                 w _ITM_deregisterTMCloneTable
a.out:                 w _ITM_registerTMCloneTable
a.out:                 w _Jv_RegisterClasses
a.out:00000000004011a8 r __FRAME_END__
a.out:00000000006011c0 d __JCR_END__
a.out:00000000006011c0 d __JCR_LIST__
a.out:00000000006028e0 D __TMC_END__
a.out:00000000006028e0 B __bss_start
a.out:0000000000601400 D __data_start
a.out:0000000000400650 t __do_global_dtors_aux
a.out:00000000006011b8 t __do_global_dtors_aux_fini_array_entry
a.out:0000000000601408 D __dso_handle
a.out:00000000006011b0 t __frame_dummy_init_array_entry
a.out:                 w __gmon_start__
a.out:00000000006011b8 t __init_array_end
a.out:00000000006011b0 t __init_array_start
a.out:0000000000400ed0 T __libc_csu_fini
a.out:0000000000400e60 T __libc_csu_init
a.out:                 U __libc_start_main@@GLIBC_2.2.5
a.out:00000000006028e0 D _edata
a.out:0000000000602920 B _end
a.out:0000000000400ed4 T _fini
a.out:0000000000400488 T _init
a.out:000000000040059e T _start
a.out:00000000006028e0 b completed.6661
a.out:0000000000601400 W data_start
a.out:00000000004005d0 t deregister_tm_clones
a.out:                 U exit@@GLIBC_2.2.5
a.out:00000000006028f0 B flags
a.out:0000000000400670 t frame_dummy
a.out:                 U free@@GLIBC_2.2.5
a.out:0000000000601440 D g_data
a.out:0000000000400530 T main
a.out:                 U malloc@@GLIBC_2.2.5
a.out:                 U read@@GLIBC_2.2.5
a.out:0000000000400610 t register_tm_clones
a.out:0000000000602900 B regs
a.out:0000000000602918 B stack
a.out:0000000000400ab9 t vm_add
a.out:0000000000400bed t vm_and
a.out:0000000000400798 t vm_cll
a.out:0000000000400852 t vm_cmp
a.out:0000000000400d28 t vm_ecl
a.out:0000000000602800 D vm_func
a.out:0000000000400963 t vm_jmp
a.out:0000000000400a40 t vm_jnz
a.out:00000000004009c7 t vm_jzz
a.out:0000000000400697 t vm_mov
a.out:0000000000400b6a t vm_mvp
a.out:0000000000400799 t vm_psh
a.out:0000000000400696 t vm_ret
a.out:0000000000400c9e t vm_xor
a.out:                 U write@@GLIBC_2.2.5

That's really, really interesting. Function names let think this is a VM. The function vm_xor leads us to imagine that the input will be XORED, then compared, thanks to vm_cmp function.

We have two ways for solving this:

  1. bruteforce solution
  2. doing it in a clean way

This is a CTF, lets work dirty.

2/ Straight to the winning point

The strategy is this one: let try by bruteforce any character and count how many instructions this program will do before saying the password is bad. If we have one (or more) good characters, the program will run longer to check other characters. Easy.

pin is a program which can count instructions. The inscount library is in the source tree. Inscount can count how many instructions a program will compute during its lifetime. Compile it, and use it.

mitsurugi@dojo:~/chall/FIC2018/v24$ ./pin-3.5-97503-gac534ca30-gcc-linux/pin -t pin-3.5-97503-gac534ca30-gcc-linux/source/tools/ManualExamples/obj-intel64/ -- ./a.out
mitsurugi@dojo:~/chall/FIC2018/v24$ cat inscount.out 
Count 191674
mitsurugi@dojo:~/chall/FIC2018/v24$ ./pin-3.5-97503-gac534ca30-gcc-linux/pin -t pin-3.5-97503-gac534ca30-gcc-linux/source/tools/ManualExamples/obj-intel64/ -- ./a.out
mitsurugi@dojo:~/chall/FIC2018/v24$ cat inscount.out 
Count 191674

Ok, two wrong password use the same numbers of instructions.

This is the shell script:

#! /bin/bash
# If there is a will, there is a way
#                        0xMitsurugi


for char in a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H J K L M N O P Q R S T U V W X Y Z
  ./pin-3.5-97503-gac534ca30-gcc-linux/pin -t pin-3.5-97503-gac534ca30-gcc-linux/source/tools/ManualExamples/obj-intel64/ -- ./a.out <<< $pass$char
  cat inscount.out | tr -d '\n'
  echo "  "$pass$char

Let's industrialize it:

mitsurugi@dojo:~/chall/FIC2018/v24$ ./
ENTER PASS :FAILEDnCount 191674  a
ENTER PASS :FAILEDnCount 191674  b
ENTER PASS :FAILEDnCount 191674  c
ENTER PASS :FAILEDnCount 191674  d
ENTER PASS :FAILEDnCount 191674  e
ENTER PASS :FAILEDnCount 191674  f
ENTER PASS :FAILEDnCount 191674  g
ENTER PASS :FAILEDnCount 191675  h
ENTER PASS :FAILEDnCount 191964  i
ENTER PASS :FAILEDnCount 191674  j
ENTER PASS :FAILEDnCount 191674  k
ENTER PASS :FAILEDnCount 191675  l
ENTER PASS :FAILEDnCount 191675  m
ENTER PASS :FAILEDnCount 191675  n
ENTER PASS :FAILEDnCount 191675  o
ENTER PASS :FAILEDnCount 191674  p
ENTER PASS :FAILEDnCount 191674  q
ENTER PASS :FAILEDnCount 191674  r
ENTER PASS :FAILEDnCount 191690  s
ENTER PASS :FAILEDnCount 191674  t
ENTER PASS :FAILEDnCount 191674  u
ENTER PASS :FAILEDnCount 191674  v
ENTER PASS :FAILEDnCount 191674  w
ENTER PASS :FAILEDnCount 191674  x
ENTER PASS :FAILEDnCount 191674  y
ENTER PASS :FAILEDnCount 191674  z
ENTER PASS :FAILEDnCount 191674  A
ENTER PASS :FAILEDnCount 191674  B
ENTER PASS :FAILEDnCount 191674  C
ENTER PASS :FAILEDnCount 191674  D
ENTER PASS :FAILEDnCount 191674  E
ENTER PASS :FAILEDnCount 191674  F
ENTER PASS :FAILEDnCount 191674  G
ENTER PASS :FAILEDnCount 191674  H
ENTER PASS :FAILEDnCount 191674  J
ENTER PASS :FAILEDnCount 191674  K
ENTER PASS :FAILEDnCount 191674  L
ENTER PASS :FAILEDnCount 191674  M
ENTER PASS :FAILEDnCount 191674  N
ENTER PASS :FAILEDnCount 191690  O
ENTER PASS :FAILEDnCount 191674  P
ENTER PASS :FAILEDnCount 191674  Q
ENTER PASS :FAILEDnCount 191674  R
ENTER PASS :FAILEDnCount 191674  S
ENTER PASS :FAILEDnCount 191674  T
ENTER PASS :FAILEDnCount 191674  U
ENTER PASS :FAILEDnCount 191674  V
ENTER PASS :FAILEDnCount 191674  W
ENTER PASS :FAILEDnCount 191674  X
ENTER PASS :FAILEDnCount 191674  Y
ENTER PASS :FAILEDnCount 191674  Z

It seems that we have some artefacts (???). Let's reduce the output by taking the biggest numbers only, and iterate until we got the solution:

mitsurugi@dojo:~/chall/FIC2018/v24$ ./ | sort | tail -3
ENTER PASS :FAILEDnCount 191690  Q
ENTER PASS :FAILEDnCount 191690  f
ENTER PASS :FAILEDnCount 191964  i
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ i | sort | tail -3
ENTER PASS :FAILEDnCount 191981  iZ
ENTER PASS :FAILEDnCount 191993  iS
ENTER PASS :FAILEDnCount 192931  iW
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iW | sort | tail -3
ENTER PASS :FAILEDnCount 192931  iWo
ENTER PASS :FAILEDnCount 192946  iWO
ENTER PASS :FAILEDnCount 193218  iWa
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWa | sort | tail -3
ENTER PASS :FAILEDnCount 193221  iWao
ENTER PASS :FAILEDnCount 193221  iWar
ENTER PASS :FAILEDnCount 194724  iWas           //it looks like an english sentence
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWas | sort | tail -3
ENTER PASS :FAILEDnCount 194724  iWasx
ENTER PASS :FAILEDnCount 194724  iWasy
ENTER PASS :FAILEDnCount 194724  iWasz          //??? Maybe not
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWasz | sort | tail -3
ENTER PASS :FAILEDnCount 194739  iWaszR
ENTER PASS :FAILEDnCount 194739  iWaszn
ENTER PASS :FAILEDnCount 195689  iWasze
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWasze | sort | tail -3
ENTER PASS :FAILEDnCount 195690  iWaszeY
ENTER PASS :FAILEDnCount 195690  iWaszeZ
ENTER PASS :FAILEDnCount 195979  iWaszeM
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWaszeM | sort | tail -3
ENTER PASS :FAILEDnCount 195980  iWaszeMz
ENTER PASS :FAILEDnCount 195995  iWaszeMn
ENTER PASS :FAILEDnCount 196945  iWaszeMy
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWaszeMy | sort | tail -3
ENTER PASS :FAILEDnCount 196962  iWaszeMye
ENTER PASS :FAILEDnCount 196962  iWaszeMym
ENTER PASS :FAILEDnCount 197236  iWaszeMyT
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWaszeMyT | sort | tail -3
ENTER PASS :FAILEDnCount 197236  iWaszeMyTz
ENTER PASS :FAILEDnCount 197264  iWaszeMyTE
ENTER PASS :FAILEDnCount 198201  iWaszeMyTi
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWaszeMyTi | sort | tail -3
ENTER PASS :FAILEDnCount 198201  iWaszeMyTiz
ENTER PASS :FAILEDnCount 198202  iWaszeMyTil
ENTER PASS :FAILEDnCount 198491  iWaszeMyTim
mitsurugi@dojo:~/chall/FIC2018/v24$ ./ iWaszeMyTim | sort | tail -3
ENTER PASS :FAILEDnCount 198492  iWaszeMyTimn
ENTER PASS :FAILEDnCount 198492  iWaszeMyTimo
ENTER PASS :WINnCount 195256  iWaszeMyTime

And here, I was WTF?? "iWaszeMyTime" ?? This one didn't flag on the platform.


mitsurugi@dojo:~/chall/FIC2018/v24$ ./a.out 
ENTER PASS :iWas#eMyTime
mitsurugi@dojo:~/chall/FIC2018/v24$ ./a.out 
ENTER PASS :iWas*eMyTime

I thing you already find the real answer: 'iWasteMyTime'.

This is really a strange side effect of the binary. I don't understand why it validate any strings.
In CTF, you run for flag, you don't dissect binaries :-)

Maybe the challenge is buggy ^_^

Let me give you a taste of my steel.
~Soulcalibur III - Mitsurugi

1 commentaire:

  1. Hello!

    Joli writeup :) Effectivement j'ai regardé après coup, j'ai le même comportement avec mon pintool.

    En fait le handler "vm_mov" est buggé, et oublie d'incrémenter une dernière fois le pc. Cela ne pose pas de problème si le byte qui est mov dans le registre n'est pas une instruction valide (il sera ignoré), mais si c'est le cas ça modifie le comportement de la VM ! Dans notre cas le byte est 0xbb, qui correspond à un "AND", qui sera executé à la place du "CMP" normalement attendu, et derrière le "JNZ" n'est jamais pris :)

    M'enfin ce n'est pas comme si y'avait eu des bugs dans presque tous les challs (#norage)...

    Pendant le CTF j'avais implem le désassembleur sans le bug, donc je ne l'ai pas vu...