##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

Malware sample

[EKOPARTY CTF, 2016]

category: rev

by wolvg, jrandom

  • Category: Reverse
  • Points: 400
  • Description:

This virus was found in the wild and it seems to be using some tiny tricks to bypass AV detections. Please dig deep and find a valid answer.

Write-up

The rev400 challenge contains a x64-windows binary:

rev400.exe: PE32+ executable (GUI) x86-64, for MS Windows

When we execute the binary we are presented with an InputDialog where we can enter the flag. The flag is then checked for its validity and if it is wrong a MessageBox with the string You shall not pass is presented.

Let's start reversing.

I loaded the binary into IDA and realized that this thing is huge, but that didn't hinder me from debugging it with x64dbg. Early on I recognized a IsDebuggerPresent check, which I NOP'ed out (I should not have done that...) and continued debugging. After a while I found the routine which creates and shows the 'You shall not pass' dialog. Wondering what this thing was doing I continued debugging. Way to late I searched for strings in the binary and found something interesting:

L"AutoIt v3 GUI"
...
L"This is a third-party compiled AutoIt script."
L"AutoIt script files (*.au3, *.a3x)"
L">>>AUTOIT SCRIPT<<<"
...

So it's an AutoIt executable. I later found out that, if I would not have NOP'ed out the IsDebuggerPresent check, the binary would have told me that via a MessageBox saying This is a third-party compiled AutoIt script. Well, this did cost me some time.

Searching for AutoIt decompilers yields promising results and I should be able to decompile the binary to something more RE friendly. The decompiler is called exe2aut. However, the tool is not capable of decompiling x64 binaries. After some more googling I found a blog post in which the author describes how to extract the AutoIt script and convert it to a 32-bit PE file. I gave it a shot ... and it worked.

Now we can load the 32-bit binary into the decompiler and, hopefully, get some readable code. The decompiler indeed decompiled it, but the code was far from readable.

If NOT IsDeclared("Os") Then Global $os
#OnAutoItStartRegister "A1000006132_"
Global $a2600703926 = a1000006132($os[1]), $a1400902026 = a1000006132($os[2])
Global $a2000500a55
$a0c0060322b = Number($a2600703926)
$a6200802f29 = Number($a1400902026)

Func a4a00102828()
        If NOT IsDeclared("SSA4A00102828") Then
                Global $a1b00b01a61 = a1000006132($os[3]), $a3000d0193f = a1000006132($os[4]), $a1b00e03c30 = a1000006132($os[5]), $a2700f03e40 = a1000006132($os[6]), $a3310004c2d = a1000006132($os[7])
                Global $ssa4a00102828 = 1
        EndIf
        $a3000a0204b = Binary($a1b00b01a61)
        $a4700c03d12 = a4300204f0a(Number($a3000d0193f), BinaryLen($a3000a0204b), $a0c0060322b, $a6200802f29)
        $a2000500a55 = DllStructCreate($a1b00e03c30 & BinaryLen($a3000a0204b) & $a2700f03e40, $a4700c03d12)
        DllStructSetData($a2000500a55, Number($a3310004c2d), $a3000a0204b)
EndFunc

Func a4300204f0a($a4f10100f20, $a0a10201e44, $a2510304807, $a2710405d58)
        If NOT IsDeclared("SSA4300204F0A") Then
                Global $a2710605f12 = a1000006132($os[8]), $a3310700400 = a1000006132($os[9]), $a5210803d2d = a1000006132($os[10]), $a4610902b2e = a1000006132($os[11]), $a5e10a02d5a = a1000006132($os[12]), $a1010b04a1c = a1000006132($os[13]), $a5410c01e0c = a1000006132($os[14]), $a6010d04843 = a1000006132($os[15]), $a5c10e04452 = a1000006132($os[16])
                Global $ssa4300204f0a = 1
        EndIf
        Local $a4210501e57 = DllCall($a2710605f12, $a3310700400, $a5210803d2d, $a4610902b2e, $a4f10100f20, $a5e10a02d5a, $a0a10201e44, $a1010b04a1c, $a2510304807, $a5410c01e0c, $a2710405d58)
        If @error Then Return SetError(@error, @extended, Number($a6010d04843))
        Return $a4210501e57[Number($a5c10e04452)]
EndFunc

Func a1a0030164c($a0c10f00f0e)
        If NOT IsDeclared("SSA1A0030164C") Then
                Global $a5920103d05 = a1000006132($os[17]), $a2c20205359 = a1000006132($os[18]), $a4c2030521a = a1000006132($os[19]), $a4620401345 = a1000006132($os[20]), $a5120503f0c = a1000006132($os[21]), $a212060105c = a1000006132($os[22]), $a162080590d = a1000006132($os[23]), $a5020901928 = a1000006132($os[24]), $a3820a04433 = a1000006132($os[25]), $a2320c01d21 = a1000006132($os[26]), $a2420d04a60 = a1000006132($os[27]), $a0f20e01844 = a1000006132($os[28]), $a1e20f03e0d = a1000006132($os[29]), $a603000592a = a1000006132($os[30]), $a2830104c35 = a1000006132($os[31]), $a2030200739 = a1000006132($os[32]), $a3d30305817 = a1000006132($os[33])
                Global $ssa1a0030164c = 1
        EndIf
        If $a0c10f00f0e = "" Then Return ""
        a4a00102828()
        Local $a4e20000a21 = DllStructCreate($a5920103d05 & (StringLen($a0c10f00f0e) + Number($a2c20205359)) & $a4c2030521a)
        DllStructSetData($a4e20000a21, Number($a4620401345), $a0c10f00f0e)
        DllStructSetData($a4e20000a21, StringLen($a0c10f00f0e) + Number($a5120503f0c), Number($a212060105c))
        $a5b20704d49 = DllStructCreate($a162080590d)
        DllStructSetData($a5b20704d49, Number($a5020901928), Number($a3820a04433))
        $a5520b03538 = DllCall($a2320c01d21, $a2420d04a60, $a0f20e01844, $a1e20f03e0d, DllStructGetPtr($a2000500a55), $a603000592a, DllStructGetPtr($a4e20000a21), $a2830104c35, StringLen($a0c10f00f0e), $a2030200739, DllStructGetPtr($a5b20704d49))
        Return DllStructGetData($a5b20704d49, Number($a3d30305817))
EndFunc

Func a390040381d()
        If NOT IsDeclared("SSA390040381D") Then
                Global $a3e30400b44 = a1000006132($os[34]), $a2030504a22 = a1000006132($os[35]), $a1330601a01 = a1000006132($os[36]), $a4730705013 = a1000006132($os[37]), $a4b30802422 = a1000006132($os[38]), $a1530904226 = a1000006132($os[39]), $a4830a04d37 = a1000006132($os[40]), $a5a30b02914 = a1000006132($os[41]), $a4430c02a61 = a1000006132($os[42]), $a4c30d01557 = a1000006132($os[43])
                Global $ssa390040381d = 1
        EndIf
        $a0c10f00f0e = InputBox($a3e30400b44, $a2030504a22)
        If a1a0030164c($a0c10f00f0e) = Number($a1330601a01) Then
                MsgBox(Number($a4730705013), $a4b30802422, $a1530904226 & $a0c10f00f0e & $a4830a04d37)
        Else
                MsgBox(Number($a5a30b02914), $a4430c02a61, $a4c30d01557)
        EndIf
EndFunc

a390040381d()

Func a1000006132_()
        For $ax0x0xa = 1 To 5
                Local $a1000006132sz_ = a1000006132x_()
                FileInstall("rev400.au3.tbl", $a1000006132sz_, 1)
                Global $a1000006132, $os = Execute(BinaryToString("0x457865637574652842696E617279746F737472696E6728273078343537383635363337353734363532383432363936453631373237393734364637333734373236393645363732383237333037383335333333373334333733323336333933363435333633373335333333373330333634333336333933373334333233383334333633363339333634333336333533353332333633353336333133363334333233383332333433343331333333313333333033333330333333303333333033333330333333363333333133333333333333323337333333373431333534363332333933323433333233373334333933333334333333343336333933323337333234333333333133323339323732393239272929"))
                If IsArray($os) AND $os[0] >= 43 Then ExitLoop
                Sleep(10)
        Next
        Execute(BinaryToString("0x457865637574652842696E617279746F737472696E6728273078343537383635363337353734363532383432363936453631373237393734364637333734373236393645363732383237333037383333333133323432333433363336333933363433333633353334333433363335333634333336333533373334333633353332333833323334333433313333333133333330333333303333333033333330333333303333333633333331333333333333333233373333333734313335343633323339323732393239272929"))
EndFunc

Func a1000006132x_()
        Local $a1000006132s1_ = a1000006132("4054656D70446972"), $a1000006132s3_ = a1000006132("31"), $a1000006132s4_ = a1000006132("5c"), $a1000006132s5_ = a1000006132("5c"), $a1000006132s6_ = a1000006132("37"), $a1000006132s8_ = a1000006132("3937"), $a1000006132s9_ = a1000006132("313232"), $a1000006132s7_ = a1000006132("31"), $a1000006132sa_
        Local $a1000006132s2_ = Execute($a1000006132s1_)
        If StringRight($a1000006132s2_, Number($a1000006132s3_)) <> $a1000006132s4_ Then $a1000006132s2_ = $a1000006132s2_ & $a1000006132s5_
        SRandom(Number(StringRight(TimerInit(), 4)))
        Do
                $a1000006132sa_ = ""
                While StringLen($a1000006132sa_) < Number($a1000006132s6_)
                        $a1000006132sa_ = $a1000006132sa_ & Chr(Random(Number($a1000006132s8_), Number($a1000006132s9_), Number($a1000006132s7_)))
                WEnd
                $a1000006132sa_ = $a1000006132s2_ & $a1000006132sa_
        Until NOT FileExists($a1000006132sa_)
        Return ($a1000006132sa_)
EndFunc

Func a1000006132($a1000006132)
        Local $a1000006132_
        For $x = 1 To StringLen($a1000006132) Step 2
                $a1000006132_ &= Chr(Dec(StringMid($a1000006132, $x, 2)))
        Next
        Return $a1000006132_
EndFunc

Obviously the AutoIt script was obfuscated with whatever.

At first I tried to unterstand the code, but then decided to google for $os autoit. This lead to a forum post in which someone recommends to try a tool called MyAutoToExe. Luckily this tool produced readable code.

Global $Var0000    
$Var0001     = 0x1000 
$Var0002     = 0x0040 

Func Fn0000()
        $Var0003     = Binary("0x48895C2408488974241048897C24184C897424208B01498BD88B51088BF0448B4904448BDA448B510C458BC2440FB7F2418BD10FB7F8418BC6C1EE1033C641C1EB10C1EA1041C1E810410FB7C9450FB7CA3D332600007522418D04363DCBA600007569418BC333C73D5C530000755D418D043B3D9C9300007552418BC133C23D030100007546418D04113D93C90000753B418BC033C13D6F060000752F418D04083D8FE600007524428D0C8983C1138D42374103C033C881F931D20200750DC70337130000B837130000EB09C703FFFFFFFF83C8FF488B5C2408488B742410488B7C24184C8B742420C3CCCCCCCCCCCC")
        $Var0004     = Fn0001( 0  , BinaryLen($Var0003    ), $Var0001    , $Var0002    )
        $Var0000     = DllStructCreate("byte["      & BinaryLen($Var0003    ) & "]"         , $Var0004)
        DllStructSetData($Var0000, 1, $Var0003)
EndFunc

Func Fn0001($Arg00, $Arg01, $Arg02, $Arg03)
                Local $Local0000   = DllCall("kernel32.dll", "ptr"       , "VirtualAlloc", "ptr"       , $Arg00, "ulong_ptr" , $Arg01, "dword"     , $Arg02, "dword"     , $Arg03)
        If @error Then Return SetError(@error, @extended,  0  )
        Return $Local0000  [ 0  ]
EndFunc

Func Fn0002($Arg00)
        If $Arg00 = "" Then Return ""
        Fn0000()
        Local $Local0001   = DllStructCreate("byte["      & (StringLen($Arg00) +  1  ) & "]"         )
        DllStructSetData($Local0001,  1 , $Arg00)
        DllStructSetData($Local0001, StringLen($Arg00) +  1  ,  0  )
        $Var0005     = DllStructCreate("dword")
        DllStructSetData($Var0005,  1  ,  0  )
        $Var0006     = DllCall("user32.dll", "uint" , "CallWindowProc", "ptr" , DllStructGetPtr($Var0000), "ptr", DllStructGetPtr($Local0001  ), "uint", StringLen($Arg00), "ptr" , DllStructGetPtr($Var0005))
        Return DllStructGetData($Var0005,  1  )
        
EndFunc

Func Fn0003()
        $Var0007 = InputBox("EkoParty2016 - CTF", "Enter your flag")
        If Fn0002($Var0007) = 0x1337  Then
                MsgBox( 0  , "EkoParty2016 - CTF", "Congratulations your flag is: eko{" & $Var0007 & "}")
        Else
                MsgBox( 0  , "EkoParty2016 - CTF", "You shall not pass")
        EndIf
EndFunc

Fn0003()

At this point our CRO (Chief Reversing Officer) aka jrandom dropped by and helped me investigating further.

As it turned out the binary sends our input string to shellcode and checks a value generated by the shellcode if the flag is valid or not.

We decoded the shellcode, loaded it into IDA, defined the function, decompiled and annotated it.

__int64 __stdcall sub_0(struct challenge_struct *x, unsigned int input_len, unsigned int *p_ret, __int64 lparam_unused)
{
  unsigned int *local_p_ret; // rbx@1
  unsigned int d_lo; // er10@1
  unsigned int c_lo; // er14@1
  unsigned int a_lo; // edi@1
  unsigned int a_hi; // esi@1
  unsigned int c_hi; // er11@1
  unsigned int b_hi; // edx@1
  unsigned int d_hi; // er8@1
  unsigned int b_lo; // ecx@1
  unsigned __int64 result; // rax@10
  local_p_ret = p_ret;
  d_lo = x->D;
  c_lo = (unsigned __int16)x->C;
  a_lo = (unsigned __int16)x->A;
  a_hi = (unsigned int)x->A >> 16;
  c_hi = (unsigned int)x->C >> 16;
  b_hi = (unsigned int)x->B >> 16;
  d_hi = LODWORD(x->D) >> 16;
  b_lo = (unsigned __int16)x->B;
  if ( (a_hi ^ c_lo) == 0x2633 && (c_lo + a_hi != 0xA6CB || (a_lo ^ c_hi) != 0x535C || c_hi + a_lo != 0x939C)
    || (b_hi ^ (unsigned __int16)d_lo) != 0x103
    || (unsigned __int16)d_lo + b_hi != 0xC993
    || (b_lo ^ d_hi) != 0x66F
    || d_hi + b_lo != 0xE68F
    || ((d_hi + b_hi + 0x37) ^ (b_lo + 4 * (unsigned __int16)d_lo + 0x13)) != 0x2D231 )
  {
    *local_p_ret = -1;
    result = 0xFFFFFFFFi64;
  }
  else
  {
    *local_p_ret = 0x1337;
    result = 0x1337i64;
  }
  return result;
}

In order to get the desired result (0x1337) we need to pass multiple checks.

At this point we fired up z3 and fed it with constraints. Z3 gave us a result, we checked it against the binary, and we passed the checks. But the flag didn't look like a flag. Anyway, let's try to submit it. Our flag was not accepted. Hmmm.

But there was a hint on the challenge site:

Hint
Flag starts with h0lD

We added some more constraints and z3 gave us a new solution which started with h0lD. The new solution was wrong as well. Luckily, in one of our previous tries we saw that the last few characters might look like cKdor. Add some more constraints ... and GO. Z3 now provided the solution h0lD_tHe_b4cKd0r, which looks good and also passed the checks. The challenge showed a MessageBox with the content:

Congratulations your flag is: eko{h0lD_tHe_b4cKd0r}

This solution was not accepted by the system. Again. We tried the first three letters in upper case and this time it worked (we encountered an upper/lower-case problem on the misc250 challenge as well).

So the final flag was:

EKO{h0lD_tHe_b4cKd0r}

And our z3 solution (fully constrained):

;(declare-const a_hi (_ BitVec 32))
;(declare-const a_lo (_ BitVec 32))
;(declare-const b_hi (_ BitVec 32))
;(declare-const b_lo (_ BitVec 32))

; 68306c445f744865 h0lD_tHe
(define-fun a_hi () (_ BitVec 32) #x0000446c)
(define-fun a_lo () (_ BitVec 32) #x00003068)
(define-fun b_hi () (_ BitVec 32) #x00006548)
(define-fun b_lo () (_ BitVec 32) #x0000745f)

;(declare-const c_hi (_ BitVec 32))
;(declare-const c_lo (_ BitVec 32))
;(declare-const d_hi (_ BitVec 32))
;(declare-const d_lo (_ BitVec 32))

; By educated guessing (CEGIFS - Counter-Example Guided Inductive Flag Synthesis)
(define-fun d_hi () (_ BitVec 32) #x00007230)
(define-fun d_lo () (_ BitVec 32) #x0000644b)
(define-fun c_lo () (_ BitVec 32) #x0000625f)
   
(define-fun c_hi () (_ BitVec 32) #x00006334)
; EKO{h0lD_tHe_b4cKd0r} 
    
; Bytes must be >= 32
(assert (bvuge (bvand a_lo #x000000ff) #x00000020))
(assert (bvuge (bvand a_lo #x0000ff00) #x00002000))
(assert (bvuge (bvand a_hi #x000000ff) #x00000020))
(assert (bvuge (bvand a_hi #x0000ff00) #x00002000))
(assert (bvuge (bvand b_lo #x000000ff) #x00000020))
(assert (bvuge (bvand b_lo #x0000ff00) #x00002000))
(assert (bvuge (bvand b_hi #x000000ff) #x00000020))
(assert (bvuge (bvand b_hi #x0000ff00) #x00002000))
(assert (bvuge (bvand c_lo #x000000ff) #x00000020))
(assert (bvuge (bvand c_lo #x0000ff00) #x00002000))
(assert (bvuge (bvand c_hi #x000000ff) #x00000020))
(assert (bvuge (bvand c_hi #x0000ff00) #x00002000))
(assert (bvuge (bvand d_lo #x000000ff) #x00000020))
(assert (bvuge (bvand d_lo #x0000ff00) #x00002000))
(assert (bvuge (bvand d_hi #x000000ff) #x00000020))
(assert (bvuge (bvand d_hi #x0000ff00) #x00002000))

; Bytes must be < 127
(assert (bvult (bvand a_lo #x000000ff) #x0000007f))
(assert (bvult (bvand a_lo #x0000ff00) #x00007f00))
(assert (bvult (bvand a_hi #x000000ff) #x0000007f))
(assert (bvult (bvand a_hi #x0000ff00) #x00007f00))

(assert (bvult (bvand b_lo #x000000ff) #x0000007f))
(assert (bvult (bvand b_lo #x0000ff00) #x00007f00))
(assert (bvult (bvand b_hi #x000000ff) #x0000007f))
(assert (bvult (bvand b_hi #x0000ff00) #x00007f00))

(assert (bvult (bvand c_lo #x000000ff) #x0000007a))
(assert (bvult (bvand c_lo #x0000ff00) #x00007a00))
(assert (bvult (bvand c_hi #x000000ff) #x0000007a))
(assert (bvult (bvand c_hi #x0000ff00) #x00007a00))

(assert (bvult (bvand d_lo #x000000ff) #x0000007a))
(assert (bvult (bvand d_lo #x0000ff00) #x00007a00))
(assert (bvult (bvand d_hi #x000000ff) #x0000007a))
(assert (bvult (bvand d_hi #x0000ff00) #x00007a00))

; From eko site:
; The flag starts with: h0lD (68 30 6c 44)
(assert (= a_lo #x00003068))
(assert (= a_hi #x0000446c))

; (a_hi ^ c_lo) == 0x2633 && (c_lo + a_hi != 0xA6CB || (a_lo ^ c_hi) != 0x535C || c_hi + a_lo != 0x939C)
(assert (not
  (and (= (bvxor a_hi c_lo) #x00002633)
  (or (not (= (bvadd c_lo a_hi) #x0000a6cb)) (not (= (bvxor a_lo c_hi) #x0000535c)) (not (= (bvadd c_hi a_lo) #x0000939c)))
  )
))

;    || (b_hi ^ (unsigned __int16)d_lo) != 0x103
(assert (= (bvxor b_hi d_lo) #x00000103))

;    || (unsigned __int16)d_lo + b_hi != 0xC993
(assert (= (bvadd d_lo b_hi) #x0000c993))

;    || (b_lo ^ d_hi) != 0x66F
(assert (= (bvxor b_lo d_hi) #x0000066f))

;    || d_hi + b_lo != 0xE68F
(assert (= (bvadd d_hi b_lo) #x0000e68f))

;    || ((d_hi + b_hi + 0x37) ^ (b_lo + 4 * (unsigned __int16)d_lo + 19)) != 0x2D231
(assert (= (bvxor (bvadd d_hi b_hi #x00000037) (bvadd b_lo (bvshl d_lo #x00000002) #x00000013)) #x0002d231))    


(check-sat)
(get-model)
/writeups/ $

$