Reverse engineering protobuf API
authorJoann Mõndresku <joann@cernodile.com>
Sat, 11 May 2024 09:34:56 +0000 (12:34 +0300)
committerJoann Mõndresku <joann@cernodile.com>
Sat, 11 May 2024 09:34:56 +0000 (12:34 +0300)
content/posts/reverse-engineering-a-mobile-app-protobuf-api.md [new file with mode: 0644]
layouts/partials/links.html

diff --git a/content/posts/reverse-engineering-a-mobile-app-protobuf-api.md b/content/posts/reverse-engineering-a-mobile-app-protobuf-api.md
new file mode 100644 (file)
index 0000000..c28e6e4
--- /dev/null
@@ -0,0 +1,572 @@
+---
+title: "Reverse Engineering a Mobile App Protobuf API"
+date: 2024-05-11T12:00:00+03:00
+description: "In recent times more than ever, live service and overuse of APIs soon-to-be-stale is only increasing. This causes a lot of once-written software to become either unusable or handicapped in many regards. One way to fight this is to learn reverse engineering for sake of digital preservation. In this blog, I take you through a journey of reverse engineering a simple mobile game's protobuf API."
+tags: ['tutorial', 'reverse-engineering', 'opensource']
+type: blog
+draft: false
+---
+
+# Why
+Why not? Digital preservation is important, even if you don't care for a specific program.
+This is also a good way to get started with protocol reverse engineering due to way protobuf
+is often left behind in source format within client applications.
+
+## The target
+In this series of blogposts, I will be using a mobile game "Egg, Inc." as the target for
+demonstration. It's a simple time killer app that got me through boring long waits when I was still at school.
+
+I won't bore you with details, but in essence it's an incremental game with partial online features
+such as cloud save, co-op contracts and server scheduled boosts.
+
+## The existing works
+In some cases, you will find previous works on the target you pick, in my case, some clever people have created
+[scripts to extract .proto file out of app.](https://github.com/DavidArthurCole/EggIncProtoExtractor)
+I advise you to check it out if you wish to get a better understanding of how you would go about retrieving the
+API spec .proto file for your target.
+
+Further there are a few dedicated individuals in the game's community who have created numerous tools and historical databases.
+
+For this blog purposes, we will assume the game server is shut down (as in we cannot query from the live API) and our goal is
+to make a semi-functional selfhosted gameserver for our own needs, assuming we are the only one on said server.
+
+## Getting Started
+Thanks to the previously mentioned script, it's easy to get started - find the APK, extract protobuf spec file, convert it with
+protoc and we're done there. One small problem - due to cheaters, latest version of the game includes "AuthenticatedMessage" structure,
+which contains a salted sha256sum of the payload message.
+
+At this point, after a bit of internal dilemma, I decided to not further the problem while service is still live for people playing and did the
+more morally sound decision of picking a version prior to these integrity checks. We can crack that another day as all the needed information
+is retained in the app itself.
+
+Going forward with this, we are targetting game version 1.12.13 (Build ID 111121).
+
+With all that out of the way, lets get into actual commands used here:
+```
+git clone https://github.com/DavidArthurCole/EggIncProtoExtractor.git
+cd EggIncProtoExtractor
+./apkextract.sh com.auxbrain.egginc_1.12.13.apk
+# We should have a new folder "protos" now with resulting files
+cd protos
+# There should be a file called ei.proto - that's our protobuf spec file
+# At this point, we can use the protoc utility which can convert the specfile
+# to interfaces in C++, C#, Java, Kotlin, Objective-C, PHP, Python and Ruby with
+# additional plugin support for Dart and Go.
+# To make this easier to understand, we will use Python in this demonstration
+protoc -I=. --python_out=. ./ei.proto
+# Success! We now have a "ei_pb2.py" file which can be directly imported to Python programs
+```
+
+With the protobuf interface in Python created, we can now proceed with creating the API emulator - but there's a slight problem.
+What URL? What endpoints? How do we find this out? Simple answer, disassembling the game. Get your RE tool of choice, I will be
+using [Ghidra](https://ghidra-sre.org/) myself.
+
+(Note: You can also just try to find this out using tools such as WireShark)
+
+The game contains a linked-library written in C++, which you can find inside the .apk `lib` folder, named as `libegginc.so`.
+This is perfect for our use-case, Ghidra is going to slice through this like butter. Import the file to your RE tool of choice
+and let it perform some analysis on it, have a cup of tea or coffee as this is going to take a hot minute.
+
+Once that's done, we are going to start by looking at the defined strings - try our luck there. Search for any debug prints left
+behind or maybe some clues. I started by searching for `http`, which lead me to following string `"HTTP REQ: %d"`, seems promising.
+When I jumped to it, I saw an exactly adjacent string to it which could give more clues:
+```
+                             s_www.auxbrain.com_00c02b60                     XREF[0,1]:   FUN_00518ab8:00518b38(R)  
+        00c02b5e 47 3f 77        ds         "G?www.auxbrain.com"
+                 77 77 2e 
+                 61 75 78 
+                             s_HTTP_REQ:_%d_00c02b71                         XREF[1]:     makeRequestInternal:0067bbd4(*)  
+        00c02b71 48 54 54        ds         "HTTP REQ: %d"
+                 50 20 52 
+                 45 51 3a 
+```
+Interesting, `www.auxbrain.com`. If we jump to its XREF, we get a garbled function, but what it seems to be doing is setting up
+certain global values.
+
+So we have a potential API endpoint, let's put it to the test. We're not going to recompile anything yet or do any byte-patching,
+let's try a quick smoke-test. Ensure your phone is rooted and you have a variant of Xposed Framework installed (I used LSPosed).
+We will need to untrip the SSL pinning present in most apps, including this one, I used [io.github.tehcneko.sslunpinning](https://github.com/Xposed-Modules-Repo/io.github.tehcneko.sslunpinning) module.
+(Note: I know it is possible to repackage the app to do SSL unpinning in most cases, but in many cases, you won't know if it's worth the effort yet)
+
+Next, install [AdAway app from F-Droid](https://f-droid.org/packages/org.adaway/) so we can setup a redirection on any network we are on.
+Inside AdAway, add a redirection rule for the address we just found and point it to an IP address in your LAN that will run the API server.
+
+Generate a self-signed certificate authority and a certificate signed by it and run a webserver with both HTTP and HTTPS on the API server machine.
+Import the self-signed CA to your phone's truststore. Once all of that is done, run the app for first time.
+
+```
+192.168.1.212 - - [...] "POST /ei/first_contact HTTP/1.1" 404 0 "-"
+```
+
+Bingo. We have contact and we have an API endpoint. Searching for "ei/" in the strings reveals a extensive list of API endpoints, we now have something
+to go off from. We have everything we need to start creating the server.
+
+## Implementing the Server - Getting first contact
+Next up, we create a new project - as we generated the protobuf definitions for Python, we will proceed accordingly.
+If you are following along, get respective packages for your operating system to create python venvs.
+As the protobufs are being sent over HTTP, we will be serving our application over flask which is being reverse proxied by nginx.
+
+```
+# Lets stage the environment
+mkdir apiserver
+cd apiserver
+python -m venv .venv
+source .venv/bin/activate
+touch app.py
+cp ~/EggIncProtoExtractor/protos/ei.proto .
+
+# Get some dependeices
+pip install protobuf
+pip install flask
+```
+
+We now have the project set up for reading protobuf definitions and a framework to listen for HTTP and routes sent to it.
+Let's create an actual listener application, open app.py with your favourite IDE or text editor.
+
+```
+import ei_pb2 as EIProto
+from flask import Flask
+from flask import request
+
+@app.route("/ei/<path:subpath>", methods=["POST"])
+def ei_routes(subpath):
+       print("HTTP POST /ei/" + subpath)
+        print(request.headers)
+       return ""
+```
+
+This should get the ball rolling, we will see whatever call comes in and we can see what the payload of each request contains.
+At this point you should setup the reverse proxy, override your nginx / directive with:
+```
+location / {
+       proxy_pass http://127.0.0.1:5000;
+}
+```
+
+Reload your nginx and start the flask application you just created with `flask run`.
+
+Run the app again and have it phone home and see what it contains.
+```
+HTTP POST /ei/first_contact
+Host: 127.0.0.1:5000
+Connection: close
+Content-Length: 37
+Content-Type: application/x-www-form-urlencoded
+User-Agent: Dalvik/2.1.0 (Linux; U; Android 13; M2012K11AG Build/TQ3A.230901.001)
+Accept-Encoding: gzip
+```
+
+We can see there's a form payload attached to this request, let's modify our app route a bit:
+```
+@app.route("/ei/<path:subpath>", methods=["POST"])
+def ei_routes(subpath):
+       print("HTTP POST /ei/" + subpath)
+        print(request.form)
+       return ""
+```
+
+Now if we run the modified flask application again, we see following output on the first_contact endpoint.
+```
+HTTP POST /ei/first_contact
+ImmutableMultiDict([('data', 'ChAzNTVlNDZlOTA4OWQxZTRjEAAYAg==')])
+```
+
+We have a base64-encoded protobuf binary data - which isn't terribly useful for reading plain-text, since protobuf *is* a binary
+format, so we will need to figure out what protobuf message this payload belongs to.
+
+Remember that ei.proto file alongside the ei_pb2.py we got earlier? Lets go back there and inspect it a bit.
+We know we just contacted something called "first_contact", maybe there is something in that file that could help us?
+
+```
+message EggIncFirstContactRequest {
+    optional string user_id = 1;
+    optional uint32 client_version = 2;
+    optional Platform platform = 3;
+}
+
+message EggIncFirstContactResponse {
+    optional Backup backup = 1;
+}
+```
+
+Seems like the application is using message names in almost similar fashion to API endpoint names themselves. This will prove
+to be useful knowledge. We now know what the payload should be, lets put this to the test.
+
+Edit your app routine again
+```
+# add "import base64" to top of the file
+@app.route("/ei/<path:subpath>", methods=["POST"])
+def ei_routes(subpath):
+       print("HTTP POST /ei/" + subpath)
+       if subpath == "first_contact":
+               # Create the protobuf object so we can load data from the b64 payload
+               FirstContact = EIProto.EggIncFirstContactRequest()
+               FirstContact.ParseFromString(base64.b64decode(form["data"]))
+               print(FirstContact)
+       else:
+               print(request.form)
+       return ""
+```
+
+We should now be able to see deserialized output when we run the flask application and the mobile app, let's try it out:
+
+```
+HTTP POST /ei/first_contact
+user_id: "355e46e9089d1e4c"
+client_version: 0
+platform: DROID
+```
+
+Nice! We now know how to identify which protobuf object corresponds to which API endpoint. We can now make an educated guess
+on what would come next.
+
+Seeing how we got `EggIncFirstContactRequest` and saw an adjacent `EggIncFirstContactResponse` message in the proto file, we
+can safely assume that this is what the game is expecting from us in return.
+
+Lets modify the server a bit to account for that.
+
+```
+@app.route("/ei/<path:subpath>", methods=["POST"])
+def ei_routes(subpath):
+       print("HTTP POST /ei/" + subpath)
+       if subpath == "first_contact":
+               # Create the protobuf object so we can load data from the b64 payload
+               FirstContact = EIProto.EggIncFirstContactRequest()
+               FirstContact.ParseFromString(base64.b64decode(form["data"]))
+               print("We got a first contact hello from user " + FirstContact.user_id)
+               # Lets respond with a FirstContactResponse
+               FirstContactResp = EIProto.EggIncFirstContactResponse()
+               # This takes only one optional argument - a Backup object - but we have no account
+               # system yet, so we will opt out of sending that for now.
+               # ---
+               # We send the payload back as a base64 string - the same way we retrieved it.
+               return base64.b64encode(FirstContactResp.SerializeToString())
+       else:
+               print(request.form)
+       return ""
+```
+
+Now when we run the app again, we notice that we no longer get spammed this endpoint, but instead in its place we see a few new friends
+
+## Implementing the Server - New Friends
+Say hello to `/ei/save_backup` and `/ei/get_periodicals`. We can infer from the name, that save_backup would involve a Backup message
+and get_periodicals would involve a GetPeriodicalsRequest, both of which are defined fully in the proto spec file.
+
+Both of these are clogging up the flask application log periodically, we should check out what they are so we can have a sane log again.
+
+```
+@app.route("/ei/<path:subpath>", methods=["POST"])
+def ei_routes(subpath):
+       print("HTTP POST /ei/" + subpath)
+       if subpath == "first_contact":
+               # Create the protobuf object so we can load data from the b64 payload
+               FirstContact = EIProto.EggIncFirstContactRequest()
+               FirstContact.ParseFromString(base64.b64decode(form["data"]))
+               print("We got a first contact hello from user " + FirstContact.user_id)
+               # Lets respond with a FirstContactResponse
+               FirstContactResp = EIProto.EggIncFirstContactResponse()
+               # This takes only one optional argument - a Backup object - but we have no account
+               # system yet, so we will opt out of sending that for now.
+               # ---
+               # We send the payload back as a base64 string - the same way we retrieved it.
+               return base64.b64encode(FirstContactResp.SerializeToString())
+       elif subpath == "save_backup":
+               # NOTE: This took me way longer to realize than it should have, but the base64
+               # payload you receive from client is broken due to some Android bug, where it
+               # substitutes "+" symbols with a " " whitespace.
+               # I don't want you to waste half hour to hours figuring out why you're getting
+               # corrupted data, so you're welcome.
+               Backup = EIProto.Backup()
+               Backup.ParseFromString(base64.b64decode(form["data"].replace(" ", "+"))
+               print(Backup)
+       elif subpath == "get_periodicals":
+               Periodicals = EIProto.GetPeriodicalsRequest()
+               Periodicals.ParseFromString(base64.b64decode(form["data"])
+               print(Periodicals)
+       else:
+               print(request.form)
+       return ""
+```
+
+We should now see what these payloads actually contain when deserialized (for your reading experience, I advise you to rather
+try this out yourself - the Backup structure is VERY large).
+
+Upon these payloads reaching the server, we see that a very much populated Backup message makes its way through and a relatively
+thin payload of PeriodicalsRequest comes through, which isn't very useful by itself.
+
+Now, this is why the game developer ended up creating forms of anticheat in future versions of this app - the Backup message contains
+your entire game state, which is often sent as a way to save your progress to cloud, but there is no actual sanity checking in place
+to ensure you're not progressing way too fast. Personally, I am of the mind that anticheat should be done on the server-side, not
+on the client-side, but I digress. We can use this to prove a very obvious vulnerability when using trust-client-always architecture.
+
+The game has an In App Purchase for "Pro Permit", which allows you to build more Silos, which in turn allow you to get offline
+rewards for a longer period of time. If we look at protobuf definition file, you can see under Backup.game, a field called "permit_level",
+which by default is zero. Lets try to change that and present a modified Backup the next time user opens the game.
+
+
+```
+cache = {}
+
+@app.route("/ei/<path:subpath>", methods=["POST"])
+def ei_routes(subpath):
+       print("HTTP POST /ei/" + subpath)
+       if subpath == "first_contact":
+               # Create the protobuf object so we can load data from the b64 payload
+               FirstContact = EIProto.EggIncFirstContactRequest()
+               FirstContact.ParseFromString(base64.b64decode(form["data"]))
+               print("We got a first contact hello from user " + FirstContact.user_id)
+               # Lets respond with a FirstContactResponse
+               FirstContactResp = EIProto.EggIncFirstContactResponse()
+               if FirstContact.user_id in cache:
+                       FirstContactResp.backup.CopyFrom(cache[FirstContact.user_id])
+                       del cache[FirstContact.user_id]
+               return base64.b64encode(FirstContactResp.SerializeToString())
+       elif subpath == "save_backup":
+               # NOTE: This took me way longer to realize than it should have, but the base64
+               # payload you receive from client is broken due to some Android bug, where it
+               # substitutes "+" symbols with a " " whitespace.
+               # I don't want you to waste half hour to hours figuring out why you're getting
+               # corrupted data, so you're welcome.
+               Backup = EIProto.Backup()
+               Backup.ParseFromString(base64.b64decode(form["data"].replace(" ", "+"))
+               if Backup.game.permit_level == 0:
+                       print("Saved a modified Backup for next game load")
+                       # Modify the permit level, force offer the backup
+                       Backup.game.permit_level = 1
+                       Backup.force_offer_backup = True
+                       Backup.force_backup = True
+                       cache[Backup.user_id] = Backup
+       elif subpath == "get_periodicals":
+               Periodicals = EIProto.GetPeriodicalsRequest()
+               Periodicals.ParseFromString(base64.b64decode(form["data"])
+               print(Periodicals)
+       else:
+               print(request.form)
+       return ""
+```
+
+Lets load up the game. Nothing interesting seems to be happening yet - lets wait until we see the "Saved a modified Backup for next game load" message
+show up in the server console. Once this shows up, restart the game - you are presented with a popup that you are offered to load a
+Backup from server. Let's accept that.
+
+Now click on your silos, you have the Pro Permit for free.
+
+Now, it goes without saying, I do not condone piracy - the future versions of this game are very much guarded against this, rightfully so.
+If you attempt this in actual game servers, this is considered fraud and IS detectable by the developer (every IAP has a receipt, logically!).
+
+This version of the game is defunct as the protocol has changed quite a bit in the years since this version and additional anticheat
+measures have been added since. You cannot transfer this status (or even purchase what you just did) from this game version to the next.
+
+### Onto the PeriodicalsRequest
+This one is a bit more fun to delve into blindly - the proto spec wont help you much here. We will need to use our trusty RE tools again and delve into
+the game disassembly again.
+
+By public knowledge, we know there are server events for "Epic Research Sale", "Research Sale", "Drone Bonus" and "Prestige Boost". We can use this information to try
+and look at any potential leads in strings. Drone seems like a good canditate, lets look into that.
+```
+drone_fans2
+drone_crash
+drone_enemy
+drone_hunter
+r_icon_drone_rewards
+b_icon_drone_boost
+drone_touch
+ei_drone_lights_green
+ei_drone_lights_red
+ei_drone_package
+ei_drone_propeller
+drone-boost
+GENEROUS DRONES
+```
+This looks promising, right off the bat, first strings I'd check here are `r_icon_drone_rewards`, `b_icon_drone_boost`, `drone-boost` and `GENEROUS DRONES`.
+
+I inspected all 4 of them, and when I got to final 2, I found the enum string translations used for event IDs - here they are extracted for game version 1.12.13
+```
+piggy-boost (Rate piggy fills is increased.)
+piggy-cap-boost (UNLIMITED PIGGY;Gains are retained when event ends.)
+prestige-boost (PRESTIGE BOOST;Collect more soul eggs on prestige, you must prestige to take advantage of this event.)
+earnings-boost (CASH BOOST;Regular earnings are increased.)
+gift-boost (GENEROUS GIFTS;Boost applies to random gifts and video gifts.)
+drone-boost (GENEROUS DRONES;Drones will produce larger rewards.)
+epic-research-sale (EPIC RESEARCH SALE;Only applies to Epic Research.)
+vehicle-sale (VEHICLE SALE;Applies to all vehicles.)
+boost-sale (BOOST SALE;Applies to the gold price of boosts.)
+boost-duration (BOOST TIME+;Boosts last longer, you must start a boost during the event.)
+```
+I recall there being a few more boosts, but this is useful for getting started with compositing PeriodicalsResponse with an active running event.
+
+### Putting together the response
+We have the enum, we have the names, descriptions, lets try to create a sample server event when the client enqueries about current server periodical events.
+```
+       elif subpath == "get_periodicals":
+               # We don't actually need the information client sends us,
+               # we aren't verifying any stats about client in our server.
+               CurrentPeriodicals = EIProto.PeriodicalsResponse()
+               # In order to add items to a repeatable field in protobuf structure,
+               # we need to call .add() method on it
+               event = CurrentPeriodicals.events.events.add()
+               # Refer to ei.proto - we are filling fields for EggIncEvent structure here.
+               event.type = "drone-boost"
+               event.multiplier = 5.00
+               event.subtitle = "Drones will produce larger rewards."
+               event.identifier = "GENEROUS DRONES"
+               event.seconds_remaining = 300.0
+               # Lets make it respond with a 5 minute event (this will re-arm itself when client calls
+               # for get_periodicals again every 6 minutes)
+               return base64.b64encode(CurrentPeriodicals.SerializeToString())
+```
+
+Launch the server and observe as the client periodically calls this endpoint again, it will now receive a 5 minute 5x Drone Rewards boost on the game.
+
+## Created the Server - What now?
+We have now created a very basic server, which appropriately responds to a first contact, misuses the game backup feature to prove a point about weaknesses of
+trusting client in server. We also created a very basic server event, which always rearms itself to never expire.
+
+What do we do next?
+
+At this point, we can start dog-fooding the project. Lets start with whatever ball game throws at us as we progress.
+
+### Contracts
+As we progress the game and start performing prestiges, we unlock a feature called "Contracts" - but disaster strikes as we don't have any contracts we could
+accept. So far we stil see our good friends `/ei/get_periodicals` and `/ei/save_backup` hammering the server at regular intervals.
+
+When we created the periodicals response payload, you might have noticed in the protobuf message an optional field called `ContractsResponse contracts`. Lets see
+what this ContractsResponse message contains.
+
+```
+message ContractsResponse {
+    repeated Contract contracts = 1;
+    optional string warning_message = 4;
+    optional double server_time = 2;
+    optional uint32 max_eop = 3 [default = 1000];
+}
+```
+
+Notice there being an array of Contract messages right off the bat - lets find its message structure next:
+
+```
+message Contract {
+    optional string identifier = 1;
+    optional string name = 9;
+    optional string description = 10;
+    optional Egg egg = 2;
+
+    repeated Goal goals = 3;
+    message Goal {
+        optional GoalType type = 1;
+        optional double target_amount = 2;
+        optional RewardType reward_type = 3;
+        optional string reward_sub_type = 4;
+        optional double reward_amount = 5;
+        optional double target_soul_eggs = 6;
+    }
+
+    repeated GoalSet goal_sets = 16;
+    message GoalSet {
+        repeated Goal goals = 1;
+    }
+
+    optional bool coop_allowed = 4;
+    optional uint32 max_coop_size = 5;
+    optional uint32 max_boosts = 12;
+    optional double minutes_per_token = 15 [default = 60];
+    optional double expiration_time = 6;
+    optional double length_seconds = 7;
+    optional double max_soul_eggs = 13;
+    optional uint32 min_client_version = 14;
+    optional bool debug = 11;
+}
+```
+
+We will need to do a bit of reading. Fortunately, the game has a community wiki, lets look into how contracts should work. I took an
+[older revision of Contracts wiki page from 2021](https://egg-inc.fandom.com/wiki/Contracts?oldid=13015) and did some slight research.
+
+From what I gather, at one point, there was only one set of contract rewards, shared between everyone - then they created a system where
+beginners will get easier contract goals while more advanced players get harder contract goals.
+
+We can put two-and-two together here and infer that `repeated Goal goals` is the legacy contract system - where everyone was on equal footing
+and `repeated GoalSet goal_sets` is the *new* goal system that is split into Standard and Elite.
+
+We also learn that in future game version, they completely reworked how contracts work *yet* again into a grading "bracket" system. Fortunately,
+we do not have to worry about that in our current target revision.
+
+Now to get the ball rolling, there is conveniently a starting point set ahead for us already. The developer of game intended to ease new players into
+contracts by creating a simple & easy contract called [Your First Contract](https://egg-inc.fandom.com/wiki/Contracts/Your_First_Contract?oldid=13547).
+
+This page tells us all the information we need to compose our first Contract, so lets try to make one.
+
+```
+       elif subpath == "get_periodicals":
+               # We don't actually need the information client sends us,
+               # we aren't verifying any stats about client in our server.
+               CurrentPeriodicals = EIProto.PeriodicalsResponse()
+               # [...]
+               Contract = CurrentPeriodicals.contracts.contracts.add()
+               Contract.identifier = "first-contract"
+               Contract.name = "Your First Contract"
+               Contract.description = "We heard you are open to contract work! Help fill this order from the local pharmacy!"
+               Contract.egg = EIProto.Egg.MEDICAL
+               Contract.coop_allowed = False
+               Contract.minutes_per_token = 5
+               # Lets set expiry time to always be 3 days into future
+               Contract.expiration_time = time.time() + (3600.0 * 72.0)
+               Contract.length_seconds = 3600.0 * 4.0
+               # The wiki mentions that you cannot get this contract after you reach 5000 Soul Eggs
+               Contract.max_soul_eggs = 5000.0
+               # We should have the basic metadata set now, lets create the goalsets.
+               FirstSet = Contract.goal_sets.add()
+               Goal = FirstSet.goals.add()
+               # There is only one type of goal in this verison
+               Goal.type = EIProto.GoalType.EGGS_LAID
+               Goal.target_amount = 100000.0
+               Goal.reward_type = EIProto.RewardType.GOLD
+               Goal.reward_amount = 192
+               Goal = FirstSet.goals.add()
+               Goal.type = EIProto.GoalType.EGGS_LAID
+               Goal.target_amount = 500000000.0
+               Goal.reward_type = EIProto.RewardType.PIGGY_FILL
+               Goal.reward_amount = 10000
+               # Lets now add the Elite table, we can pretty much copy-paste the above here.
+               SecondSet = Contract.goal_sets.add()
+               Goal = SecondSet.goals.add()
+               Goal.type = EIProto.GoalType.EGGS_LAID
+               Goal.target_amount = 100000.0
+               Goal.reward_type = EIProto.RewardType.GOLD
+               Goal.reward_amount = 500
+               Goal = SecondSet.goals.add()
+               Goal.type = EIProto.GoalType.EGGS_LAID
+               Goal.target_amount = 500000000.0
+               Goal.reward_type = EIProto.RewardType.PIGGY_FILL
+               Goal.reward_amount = 10000
+               return base64.b64encode(CurrentPeriodicals.SerializeToString())
+```
+
+Lets try that out in-game now - after waiting for a minute, we see our contract prop up, but I immediately noticed one thing amiss.
+The contract goals are swapped! I am getting Elite contract rewards for a Standard contract.
+
+This piece of information now tells us that the first entry in GoalSets refers to Elite rewards and the second entry in GoalSets to Standard rewards.
+After swapping the sets around, we now see a contract with the corrected rewards.
+
+I playtested it a bit and the contract worked as expected.
+
+Now, the above code could be a lot neater. For your homework, if you're not skipping to the public source release in the end, you should try to create
+a contract database and try scheduling them like the game originally did - a "Leggacy" contract every Friday and regular contracts showing up every 1-2 weeks
+for roughly 2 weeks.
+
+## Conclusion so far
+We have created a (rather ugly looking) server emulator for the game. It functions, but it needs a lot of work still before we can call it ready.
+If you have followed this far, give yourself pat on the back - if you actually tried to run this code, give yourself an extra pat on the back.
+
+Before I give you the public source to the project, you might want to try your hand at creating a few more things.
+- "Cloud" save, present a Backup to any new device that just started playing.
+- Contracts Database and scheduler
+- Server Event scheduler
+
+I apologize if my method of documenting this has been messy, but that's also part of the chaos of reverse engineering, you are constantly learning new things
+about the project you are currently doing - refactoring becomes an essential part once you have documented the protocol to a comfortable degree.
+
+I won't give any promises for a part 2 any time soon, but I will be trying to make this feature complete, so without further ado, here are the git repository links:
+[github.com](https://github.com/cernodile/reEgg), [git.based.quest](https://git.based.quest/?p=reEgg.git;a=tree;h=refs/heads/master;hb=refs/heads/master).
+
+Thank you for reading and making it all the way to the end,
+- Cernodile
index 7609e461645d53784b68ab38dcb5ff9c6be97baf..67cfd73f5e5168e2c015b5128ffa373afcfcc34f 100644 (file)
@@ -1,3 +1,3 @@
-<p class="cern-blue">Quick-links to services I host: <a href="https://tv.based.quest">PeerTube</a>, <a href="https://searx.cernodile.com">Searx</a>, <a href="https://nitter.based.quest">Nitter</a>, <a href="https://piped.based.quest">Piped</a>, <a href="https://red.based.quest">Teddit</a>.</p>
+<p class="cern-blue">Quick-links to services I host: <a href="https://tv.based.quest">PeerTube</a>, <a href="https://searx.cernodile.com">Searx</a>, <a href="https://nitter.based.quest">Nitter</a>, <a href="https://proxitok.based.quest">Proxitok</a>, <a href="https://breezewiki.based.quest">BreezeWiki</a>, <a href="https://quetre.based.quest">Quetre</a>, <a href="https://red.based.quest">Teddit</a>.</p>
 <p class="cern-blue">Hall of Based: <a href="https://reactos.org">ReactOS</a>, <a href="https://matrix.org">Matrix</a>, <a href="https://postmarketos.org">PostmarketOS</a>, <a href="https://pine64.org">Pine64</a>, <a href="https://landchad.net">Landchad.net</a>, <a href="https://based.cooking">based.cooking</a>, <a href="https://www.borgbackup.org/">borgbackup</a>.</p>
-<p class="cern-blue">Fellow landchads: <a href="https://okass.net">okass.net</a>, <a href="https://ghativega.in">ghativega.in</a>.<br><br></p>
+<p class="cern-blue">Fellow landchads: <a href="https://okass.net">okass.net</a>, <a href="https://ghativega.in">ghativega.in</a>, <a href="http://dujemihanovic.xyz/">dujemihanovic.xyz</a>.<br><br></p>