1. Introduction
  2. Tools Used
  3. Challenges
  4. Conclusion

0. Introduction

I participated in h1702ctf.com, a mobile security CTF where you reverse iOS/Android apps, along with the armv7 and armv64 shared libraries that they use. In the end, i only solved 6/12 challenges even thought I thought I would have completed at least nine. Some of the challenges, I felt I was close to the answer, but just couldn’t figure out what I’m missing in the puzzle. Still, it was a fun experience, and will hopefully do better next time.

1. Tools Used

  • Hopper
  • radare
  • lldb + voltron
  • cycript
  • smalidea
  • apktool
  • gdb
  • dex2jar/jd-gui
  • mitmproxy

2. Challenges

iOS 1

This one was easy. The downloadable item is an .ipa file, which I installed on my iOS phone using cydia’s impactor. After opening the app, and seeing nothing out of ordinary, I decided to disassemble the binary ( which can be optained by unzipping the .ipa file) using Hopper. I scan function symbols, and looked for interesting strings. I found a function that contained the substring Level1ViewController, but when I looked at some of the methods, such as viewDidLoad, it didnt yield anything particularly worth noting.

So I went back to check the UI, and noticed the image is zoomed in. This is when I decided to see what the actual image looks like, as the flag migth be embedded in the image. I looked for the actual resource file in unzipped ipa, and Assets.car happen to be a good candidate given its file size and based on my experience making swift apps. I downloaded https://github.com/insidegui/AssetCatalogTinkerer, which enabled me to see original images being used. Saw morpheous image containing the flag, cApwN{yOu_are_th3_chOsen_1}, which didnt work immediately. But after replacing ‘O’ with ‘0’, it worked.

iOS 2

I interacted with the iOS app again, and two input fields were shown. Playing around with it didn’t immediately anything interesting, so I decided to analyze the behavior using Hopper + lldb/voltron. Interestingly, hopper showed a function symbols “[_TtC11IntroLevels20Level2ViewController buttonTouched:]”, which is most likey a good entry point for analysis. I signed the lldb debugserver with the correct entitlement, “task_for_pid-allow”, so that it can debug 3rd party programs. I ran the code below on both my device and host computer to start debugging.

debugserver *:1234 -a IntroLevels

$ lldb
(lldb) platform select remote-ios
(lldb) process connect connect://

Once I grabbed the ASLR offset of the binary via “image list -o -f”, I was able set the breakpoint at buttonTouched, and step through it. Analysis didnt go anywhere in initial run. So I analzed the disassembly again, and looked for interesting functions and strings. When I saw the strings 5b6da8f65476a399050c501e27ab7d91 and the presence of CryptoSwift.AES, and Swift.String.md5 related functions, I thought it could be something worth investigating. I didn’t remember seeing them on my initial run of lldb, so I used Hopper’s graph view this time around, which helped me pinpoint what condition is needed in order to reach that codepath. I was then able to narrow it down to the line at 0x100004e70 cmp x26, #0x6 .

I decided to provide a random string to the input field, and see where it would appear in the lldb run. When I reached the branching point mentioned above, I realized the register being compared to 0x6 corresponds to my input string length. I looked at the code more, and stumbled upon a line that gets the md5 of our string and checks it against the value “5b6da8f65476a399050c501e27ab7d91”. After further analysis, I realized the program is taking the md5 of our input string which should be 6 bytes in length, and using that as the AES key to decrypt the flag. I created new swift project, imported CryptoSwift library and generated md5 for repeatedpermutations of 6 character string. However, since it’s a little slow, I downloaded hashcat and used it to crack the md5.

echo -n '5b6da8f65476a399050c501e27ab7d91' > file.txt
./hashcat -a 3 -m 0 file.txt ?a?a?a?a?a?a

I got a value of 424241, and when entered into the textbox, gives me cApwN{0mg_d0es_h3_pr4y}

iOS 3

This one was pretty straightfoward as well. Using mitmproxy, I was able to see HTTP requests made by app. And I noticed that touching any of the rock/paper/scissors button would do a GET on google.com. Inspecting the request, I see the custom header “look at me i am a header” with the value cApwN{1m_1n_ur_n00twork_tere3fik}

iOS 4

This one was supposed to be easy. You simply had to pass all 3 flags into a function, but because of a typographical error in my 3rd flag, I wasted a lot of time reversing the flag decryption algorithm, thinking that it requires a sophisticated solution where I need to modify some of the arm64 instructions with respect to AES decryption settings, padding, etc..but is in fact not necessary. Using cyrcript, I was able to call the Objective-C function doTheThing that I found on Hopper, and passing it the previous 3 flags, the flag was returned.

cycript -p 4145
cy# [ZhuLi doTheThing:@"cApwN{y0u_are_th3_ch0sen_1}" flag2:@"cApwN{0mg_d0es_h3_pr4y}" flag3:@"cApwN{1m_1n_ur_n00twork_tere3fik}" ]

Using a ruby script in irb, I can just then decode the hex string as:

2.3.0 :001 > ["634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d"].pack("H*")
 => "cApwN{f0h_sw1zzle_my_n1zzle}

Android 1

The first challenge was very simple. The activity cycles through different images, so checking the assets folder, you see an image with the caption “cApwN{WELL_THAT_WAS_SUPER_EASY}”

Android 2

Using JD-GUI, I looked at what activity, fragment was being used for this level. I noticed that the InCryption class was calling the method hashOfPlainText(), which is just a SHA1 of a plain text being return from a decryption function call in the same class. To quickly see what this plain text value is, I copied the class to a plain java project, and removing the SHA1 call and running the program, I see The non-hashed plain text “DASH SPACE DOT SPACE DASH DOT DASH SPACE DOT…” when decoded as morse code corresponds to “capwn{cryp706r4phy_15_h4rd_br0}” . However, when I tried putting as an answer it didn’t work. When I changed “capwn” to “cApwN” to match other flag formats, it didn’t work either. Reading the flag “cryptography is hard bro”, I thought it’s implying that it necessitates a difficult solution, and that a simple morse code decryption is wrong and oversimplying things, which made me spent hours and hours looking at this level, and trying different things, and research different crypto attacks

In the end, I didn’t get this point. After the competition ended thought, I saw that the correct answer had all of “cryp706r4phy_15_h4rd_br0” capitalized instead of lowercase..had I tried that, it would’ve worked.. Too bad.

Android 3

Using JD-GUI again, I noticed that the requestor class was not being used, but I wanted to see what kind of http request it was making. I simply had to patch the smali code in the TabFragment2$1.smali so that pressing button calls the method.

     .line 25
-    iget-object v2, p0, Lcom/h1702ctf/ctfone/TabFragment2$1;->this$0:Lcom/h1702ctf/ctfone/TabFragment2;
-    iget-object v2, v2, Lcom/h1702ctf/ctfone/TabFragment2;->mHashView:Landroid/widget/TextView;
-    const/4 v3, -0x1
-    invoke-virtual {v2, v3}, Landroid/widget/TextView;->setBackgroundColor(I)V
+    invoke-static {}, Lcom/h1702ctf/ctfone/Requestor;->request()V

All I had to do now is remote debug the android process using smalidea, and look at what values are being returned in “Object localObject2 = hName();” and “String str = hVal();”. I get

hName - X-Level3-Flag
hVal  - V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=

As there’s an equals at the end of the hVal string, I thought its base64 encoded, and doing a base64.decode several times on the hVal encoded value gives me “cApwN{1_4m_numb3r_7hr33}”.

Android 5

This one, I felt like I’m close to getting the answer, but for some reason, my inputs are not generating the right flag. There are 3 textboxes that when provided with the correct values, would like result in a flag. The apk contained JNI native function called “one()”, which looked interesting. One can broadcast an intent using “adb shell am start -a android.intent.action.VIEW”, but instead of doing that, I patched one the “hint action buttons” to call the JNI native function instead.

# virtual methods
.method public onClick(Landroid/view/View;)V
    .locals 3
    .param p1, "view"    # Landroid/view/View;

    .line 35
    +    new-instance v3, Lcom/h1702ctf/ctfone5/CruelIntentions;
    +    invoke-direct {v3}, Lcom/h1702ctf/ctfone5/CruelIntentions;-><init>()V
    +    invoke-virtual {v3}, Lcom/h1702ctf/ctfone5/CruelIntentions;->one()V

    -    const/4 v1, 0x0
    -    invoke-static {p1, v0, v1}, Landroid/support/design/widget/Snackbar;->make(Landroid/view/View;Ljava/lang/CharSequence;I)Landroid/support/design/widget/Snackbar;

    .line 37
.end method

Running the shared library through gdb remotely, the “Tracepid” check exits my debugger. At this point, my Hopper was still a free version, so I used radare to patch the tracepid check instead.

0x255a - atoi (gets pid integer value of Tracepid), if not 0 (being traced), will go to 0x2570, which will then raise (stop executing)

[0x000023bc]> oo+
[0x000023bc]> s 0x2564
[0x00002564]> pd 5
|       ,=< 0x00002564      09d0           beq 0x257a
|      ,==< 0x00002566      ffe7           b 0x2568
|      ||      ; JMP XREF from 0x00002566 (sym.Java_com_h1702ctf_ctfone5_CruelIntentions_one)
|      `--> 0x00002568      306f           ldr r0, [r6, 0x70]
|       |   0x0000256a      4069           ldr r0, [r0, 0x14]
|       |   0x0000256c      fff76aed       blx sym.imp.fclose          ; int fclose(FILE *stream)

[0x00002564]> wa b 0x257a
Written 2 bytes (b 0x257a) = wx 09e0

[0x00002564]> pd 5
|       ,=< 0x00002564      09e0           b 0x257a
|      ,==< 0x00002566      ffe7           b 0x2568
|      ||      ; JMP XREF from 0x00002566 (sym.Java_com_h1702ctf_ctfone5_CruelIntentions_one)
|      `--> 0x00002568      306f           ldr r0, [r6, 0x70]
|       |   0x0000256a      4069           ldr r0, [r0, 0x14]
|       |   0x0000256c      fff76aed       blx sym.imp.fclose          ; int fclose(FILE *stream)

I started tracing the main routine, and noted points of interesting, and added comments.

0x26a0 - seems to be start of a loop that process the phrases “A man…”
0x26a8 - r2 contains the string being tested
0x273e - seems to be processing next phrase (cmp r1, r2) greater than
0x26f0 - test if char is alphabetic
0x2702 - when char is not alphabetic, execute this routine
0x2694 - have we completed iterating whole string?
0x2808 - checks whether we have processed all strings?
0x2640 - for getting new string?

Running through the code more, I realized that what the function does is normalize the string, stripping it out of non-alphabetical characters, and then checks whether it’s a “palindrome” or not, while also ignoring case sensitivity. For example “A car, a man, a maraca” would become “Acaramanamaraca” .

I set breakpoints, and set gdb to display the following struct pointers to observe behavior better.

# On Emulator
gdbserver :1234 --attach 3021

# On Host
~/Downloads/android-ndk-r15b$ ./prebuilt/darwin-x86_64/bin/gdb ~/android/app_process
(gdb) file ~/security/h1_702/android/chall_5/ctfone5/lib/armeabi-v7a/libnative-lib.so
Reading symbols from ~/security/h1_702/android/chall_5/ctfone5/lib/armeabi-v7a/libnative-lib.so...(no debugging symbols found)...done.
(gdb) target remote :1234
(gdb) info proc mappings

0xa6e08000 0xa6e09000 0x1000 0x0 [anon:thread signal stack guard page]
0xa6e09000 0xa6e0b000 0x2000 0x0 [anon:thread signal stack]
0xa6e0b000 0xa6e18000 0xd000 0x0 /dev/ashmem/dalvik-large object space allocation (deleted)
0xa6e18000 0xa6e38000 0x20000 0x0 /dev/ashmem/dalvik-LinearAlloc (deleted)
0xa6e38000 0xa6e44000 0xc000 0x0 /data/app/com.h1702ctf.ctfone5-2/lib/arm/libnative-lib.so
0xa6e44000 0xa6e45000 0x1000 0xb000 /data/app/com.h1702ctf.ctfone5-2/lib/arm/libnative-lib.so
0xa6e45000 0xa6e46000 0x1000 0xc000 /data/app/com.h1702ctf.ctfone5-2/lib/arm/libnative-lib.so
0xa6e46000 0xa6e48000 0x2000 0x0 /dev/ashmem/dalvik-indirect ref table
0xa6e48000 0xa6e49000 0x1000 0x0 [anon:thread signal stack guard page]
0xa6e49000 0xa6e4b000 0x2000 0x0 [anon:thread signal stack]
0xa6e4b000 0xa6e8b000 0x40000 0x205000 /data/app/com.h1702ctf.ctfone5-2/base.apk
0xa6e8b000 0xa6e97000 0xc000 0x244000 /data/app/com.h1702ctf.ctfone5-2/base.apk

(gdb) display/10i $pc
(gdb) b *(0xa6e38000 + 0x26a0)            // shared library offset + relative addr in hopper
(gdb) display/s *($r6 + 0x34)             // string stripped out non-alphabetic chars  "AdogApanicinapagoda"
(gdb) display/x *($r6 + 0x70) + 0x40      // index of palindrome_check

The strings that passed the palindrome check are “A car, a man, a maraca”, “A but tuba”, and “A dog! A panic in a pagoda!” . I’m not sure why gdb is not showing “A dog, a plan, a canal: pagoda”, perhaps I missed something on the code analysis.

I thought by putting the palindrome passable phrases to the 3 textboxes mentioned earlier, I would get the flag. However, trying different order, using the normalized versions, and even using the hex equivalent of the strings (which I thought the omit the “oh ex” is referring to, where “oh ex” corresponds to 0x in the hex notation). But I didnt’ see any flag. I tried the non-palindrome ones as well, and didn’t give much result.

3. Conclusion

Even though I was not able to complete all challenges, its still a fun CTF, and learn quite a few things. Will strive to do better next time around.