Reverse engineering protobuf API
[web-hugo.git] / content / posts / reverse-engineering-a-mobile-app-protobuf-api.md
1 ---
2 title: "Reverse Engineering a Mobile App Protobuf API"
3 date: 2024-05-11T12:00:00+03:00
4 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."
5 tags: ['tutorial', 'reverse-engineering', 'opensource']
6 type: blog
7 draft: false
8 ---
9
10 # Why
11 Why not? Digital preservation is important, even if you don't care for a specific program.
12 This is also a good way to get started with protocol reverse engineering due to way protobuf
13 is often left behind in source format within client applications.
14
15 ## The target
16 In this series of blogposts, I will be using a mobile game "Egg, Inc." as the target for
17 demonstration. It's a simple time killer app that got me through boring long waits when I was still at school.
18
19 I won't bore you with details, but in essence it's an incremental game with partial online features
20 such as cloud save, co-op contracts and server scheduled boosts.
21
22 ## The existing works
23 In some cases, you will find previous works on the target you pick, in my case, some clever people have created
24 [scripts to extract .proto file out of app.](https://github.com/DavidArthurCole/EggIncProtoExtractor)
25 I advise you to check it out if you wish to get a better understanding of how you would go about retrieving the
26 API spec .proto file for your target.
27
28 Further there are a few dedicated individuals in the game's community who have created numerous tools and historical databases.
29
30 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
31 to make a semi-functional selfhosted gameserver for our own needs, assuming we are the only one on said server.
32
33 ## Getting Started
34 Thanks to the previously mentioned script, it's easy to get started - find the APK, extract protobuf spec file, convert it with
35 protoc and we're done there. One small problem - due to cheaters, latest version of the game includes "AuthenticatedMessage" structure,
36 which contains a salted sha256sum of the payload message.
37
38 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
39 more morally sound decision of picking a version prior to these integrity checks. We can crack that another day as all the needed information
40 is retained in the app itself.
41
42 Going forward with this, we are targetting game version 1.12.13 (Build ID 111121).
43
44 With all that out of the way, lets get into actual commands used here:
45 ```
46 git clone https://github.com/DavidArthurCole/EggIncProtoExtractor.git
47 cd EggIncProtoExtractor
48 ./apkextract.sh com.auxbrain.egginc_1.12.13.apk
49 # We should have a new folder "protos" now with resulting files
50 cd protos
51 # There should be a file called ei.proto - that's our protobuf spec file
52 # At this point, we can use the protoc utility which can convert the specfile
53 # to interfaces in C++, C#, Java, Kotlin, Objective-C, PHP, Python and Ruby with
54 # additional plugin support for Dart and Go.
55 # To make this easier to understand, we will use Python in this demonstration
56 protoc -I=. --python_out=. ./ei.proto
57 # Success! We now have a "ei_pb2.py" file which can be directly imported to Python programs
58 ```
59
60 With the protobuf interface in Python created, we can now proceed with creating the API emulator - but there's a slight problem.
61 What URL? What endpoints? How do we find this out? Simple answer, disassembling the game. Get your RE tool of choice, I will be
62 using [Ghidra](https://ghidra-sre.org/) myself.
63
64 (Note: You can also just try to find this out using tools such as WireShark)
65
66 The game contains a linked-library written in C++, which you can find inside the .apk `lib` folder, named as `libegginc.so`.
67 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
68 and let it perform some analysis on it, have a cup of tea or coffee as this is going to take a hot minute.
69
70 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
71 behind or maybe some clues. I started by searching for `http`, which lead me to following string `"HTTP REQ: %d"`, seems promising.
72 When I jumped to it, I saw an exactly adjacent string to it which could give more clues:
73 ```
74 s_www.auxbrain.com_00c02b60 XREF[0,1]: FUN_00518ab8:00518b38(R)
75 00c02b5e 47 3f 77 ds "G?www.auxbrain.com"
76 77 77 2e
77 61 75 78
78 s_HTTP_REQ:_%d_00c02b71 XREF[1]: makeRequestInternal:0067bbd4(*)
79 00c02b71 48 54 54 ds "HTTP REQ: %d"
80 50 20 52
81 45 51 3a
82 ```
83 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
84 certain global values.
85
86 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,
87 let's try a quick smoke-test. Ensure your phone is rooted and you have a variant of Xposed Framework installed (I used LSPosed).
88 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.
89 (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)
90
91 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.
92 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.
93
94 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.
95 Import the self-signed CA to your phone's truststore. Once all of that is done, run the app for first time.
96
97 ```
98 192.168.1.212 - - [...] "POST /ei/first_contact HTTP/1.1" 404 0 "-"
99 ```
100
101 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
102 to go off from. We have everything we need to start creating the server.
103
104 ## Implementing the Server - Getting first contact
105 Next up, we create a new project - as we generated the protobuf definitions for Python, we will proceed accordingly.
106 If you are following along, get respective packages for your operating system to create python venvs.
107 As the protobufs are being sent over HTTP, we will be serving our application over flask which is being reverse proxied by nginx.
108
109 ```
110 # Lets stage the environment
111 mkdir apiserver
112 cd apiserver
113 python -m venv .venv
114 source .venv/bin/activate
115 touch app.py
116 cp ~/EggIncProtoExtractor/protos/ei.proto .
117
118 # Get some dependeices
119 pip install protobuf
120 pip install flask
121 ```
122
123 We now have the project set up for reading protobuf definitions and a framework to listen for HTTP and routes sent to it.
124 Let's create an actual listener application, open app.py with your favourite IDE or text editor.
125
126 ```
127 import ei_pb2 as EIProto
128 from flask import Flask
129 from flask import request
130
131 @app.route("/ei/<path:subpath>", methods=["POST"])
132 def ei_routes(subpath):
133 print("HTTP POST /ei/" + subpath)
134 print(request.headers)
135 return ""
136 ```
137
138 This should get the ball rolling, we will see whatever call comes in and we can see what the payload of each request contains.
139 At this point you should setup the reverse proxy, override your nginx / directive with:
140 ```
141 location / {
142 proxy_pass http://127.0.0.1:5000;
143 }
144 ```
145
146 Reload your nginx and start the flask application you just created with `flask run`.
147
148 Run the app again and have it phone home and see what it contains.
149 ```
150 HTTP POST /ei/first_contact
151 Host: 127.0.0.1:5000
152 Connection: close
153 Content-Length: 37
154 Content-Type: application/x-www-form-urlencoded
155 User-Agent: Dalvik/2.1.0 (Linux; U; Android 13; M2012K11AG Build/TQ3A.230901.001)
156 Accept-Encoding: gzip
157 ```
158
159 We can see there's a form payload attached to this request, let's modify our app route a bit:
160 ```
161 @app.route("/ei/<path:subpath>", methods=["POST"])
162 def ei_routes(subpath):
163 print("HTTP POST /ei/" + subpath)
164 print(request.form)
165 return ""
166 ```
167
168 Now if we run the modified flask application again, we see following output on the first_contact endpoint.
169 ```
170 HTTP POST /ei/first_contact
171 ImmutableMultiDict([('data', 'ChAzNTVlNDZlOTA4OWQxZTRjEAAYAg==')])
172 ```
173
174 We have a base64-encoded protobuf binary data - which isn't terribly useful for reading plain-text, since protobuf *is* a binary
175 format, so we will need to figure out what protobuf message this payload belongs to.
176
177 Remember that ei.proto file alongside the ei_pb2.py we got earlier? Lets go back there and inspect it a bit.
178 We know we just contacted something called "first_contact", maybe there is something in that file that could help us?
179
180 ```
181 message EggIncFirstContactRequest {
182 optional string user_id = 1;
183 optional uint32 client_version = 2;
184 optional Platform platform = 3;
185 }
186
187 message EggIncFirstContactResponse {
188 optional Backup backup = 1;
189 }
190 ```
191
192 Seems like the application is using message names in almost similar fashion to API endpoint names themselves. This will prove
193 to be useful knowledge. We now know what the payload should be, lets put this to the test.
194
195 Edit your app routine again
196 ```
197 # add "import base64" to top of the file
198 @app.route("/ei/<path:subpath>", methods=["POST"])
199 def ei_routes(subpath):
200 print("HTTP POST /ei/" + subpath)
201 if subpath == "first_contact":
202 # Create the protobuf object so we can load data from the b64 payload
203 FirstContact = EIProto.EggIncFirstContactRequest()
204 FirstContact.ParseFromString(base64.b64decode(form["data"]))
205 print(FirstContact)
206 else:
207 print(request.form)
208 return ""
209 ```
210
211 We should now be able to see deserialized output when we run the flask application and the mobile app, let's try it out:
212
213 ```
214 HTTP POST /ei/first_contact
215 user_id: "355e46e9089d1e4c"
216 client_version: 0
217 platform: DROID
218 ```
219
220 Nice! We now know how to identify which protobuf object corresponds to which API endpoint. We can now make an educated guess
221 on what would come next.
222
223 Seeing how we got `EggIncFirstContactRequest` and saw an adjacent `EggIncFirstContactResponse` message in the proto file, we
224 can safely assume that this is what the game is expecting from us in return.
225
226 Lets modify the server a bit to account for that.
227
228 ```
229 @app.route("/ei/<path:subpath>", methods=["POST"])
230 def ei_routes(subpath):
231 print("HTTP POST /ei/" + subpath)
232 if subpath == "first_contact":
233 # Create the protobuf object so we can load data from the b64 payload
234 FirstContact = EIProto.EggIncFirstContactRequest()
235 FirstContact.ParseFromString(base64.b64decode(form["data"]))
236 print("We got a first contact hello from user " + FirstContact.user_id)
237 # Lets respond with a FirstContactResponse
238 FirstContactResp = EIProto.EggIncFirstContactResponse()
239 # This takes only one optional argument - a Backup object - but we have no account
240 # system yet, so we will opt out of sending that for now.
241 # ---
242 # We send the payload back as a base64 string - the same way we retrieved it.
243 return base64.b64encode(FirstContactResp.SerializeToString())
244 else:
245 print(request.form)
246 return ""
247 ```
248
249 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
250
251 ## Implementing the Server - New Friends
252 Say hello to `/ei/save_backup` and `/ei/get_periodicals`. We can infer from the name, that save_backup would involve a Backup message
253 and get_periodicals would involve a GetPeriodicalsRequest, both of which are defined fully in the proto spec file.
254
255 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.
256
257 ```
258 @app.route("/ei/<path:subpath>", methods=["POST"])
259 def ei_routes(subpath):
260 print("HTTP POST /ei/" + subpath)
261 if subpath == "first_contact":
262 # Create the protobuf object so we can load data from the b64 payload
263 FirstContact = EIProto.EggIncFirstContactRequest()
264 FirstContact.ParseFromString(base64.b64decode(form["data"]))
265 print("We got a first contact hello from user " + FirstContact.user_id)
266 # Lets respond with a FirstContactResponse
267 FirstContactResp = EIProto.EggIncFirstContactResponse()
268 # This takes only one optional argument - a Backup object - but we have no account
269 # system yet, so we will opt out of sending that for now.
270 # ---
271 # We send the payload back as a base64 string - the same way we retrieved it.
272 return base64.b64encode(FirstContactResp.SerializeToString())
273 elif subpath == "save_backup":
274 # NOTE: This took me way longer to realize than it should have, but the base64
275 # payload you receive from client is broken due to some Android bug, where it
276 # substitutes "+" symbols with a " " whitespace.
277 # I don't want you to waste half hour to hours figuring out why you're getting
278 # corrupted data, so you're welcome.
279 Backup = EIProto.Backup()
280 Backup.ParseFromString(base64.b64decode(form["data"].replace(" ", "+"))
281 print(Backup)
282 elif subpath == "get_periodicals":
283 Periodicals = EIProto.GetPeriodicalsRequest()
284 Periodicals.ParseFromString(base64.b64decode(form["data"])
285 print(Periodicals)
286 else:
287 print(request.form)
288 return ""
289 ```
290
291 We should now see what these payloads actually contain when deserialized (for your reading experience, I advise you to rather
292 try this out yourself - the Backup structure is VERY large).
293
294 Upon these payloads reaching the server, we see that a very much populated Backup message makes its way through and a relatively
295 thin payload of PeriodicalsRequest comes through, which isn't very useful by itself.
296
297 Now, this is why the game developer ended up creating forms of anticheat in future versions of this app - the Backup message contains
298 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
299 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
300 on the client-side, but I digress. We can use this to prove a very obvious vulnerability when using trust-client-always architecture.
301
302 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
303 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",
304 which by default is zero. Lets try to change that and present a modified Backup the next time user opens the game.
305
306
307 ```
308 cache = {}
309
310 @app.route("/ei/<path:subpath>", methods=["POST"])
311 def ei_routes(subpath):
312 print("HTTP POST /ei/" + subpath)
313 if subpath == "first_contact":
314 # Create the protobuf object so we can load data from the b64 payload
315 FirstContact = EIProto.EggIncFirstContactRequest()
316 FirstContact.ParseFromString(base64.b64decode(form["data"]))
317 print("We got a first contact hello from user " + FirstContact.user_id)
318 # Lets respond with a FirstContactResponse
319 FirstContactResp = EIProto.EggIncFirstContactResponse()
320 if FirstContact.user_id in cache:
321 FirstContactResp.backup.CopyFrom(cache[FirstContact.user_id])
322 del cache[FirstContact.user_id]
323 return base64.b64encode(FirstContactResp.SerializeToString())
324 elif subpath == "save_backup":
325 # NOTE: This took me way longer to realize than it should have, but the base64
326 # payload you receive from client is broken due to some Android bug, where it
327 # substitutes "+" symbols with a " " whitespace.
328 # I don't want you to waste half hour to hours figuring out why you're getting
329 # corrupted data, so you're welcome.
330 Backup = EIProto.Backup()
331 Backup.ParseFromString(base64.b64decode(form["data"].replace(" ", "+"))
332 if Backup.game.permit_level == 0:
333 print("Saved a modified Backup for next game load")
334 # Modify the permit level, force offer the backup
335 Backup.game.permit_level = 1
336 Backup.force_offer_backup = True
337 Backup.force_backup = True
338 cache[Backup.user_id] = Backup
339 elif subpath == "get_periodicals":
340 Periodicals = EIProto.GetPeriodicalsRequest()
341 Periodicals.ParseFromString(base64.b64decode(form["data"])
342 print(Periodicals)
343 else:
344 print(request.form)
345 return ""
346 ```
347
348 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
349 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
350 Backup from server. Let's accept that.
351
352 Now click on your silos, you have the Pro Permit for free.
353
354 Now, it goes without saying, I do not condone piracy - the future versions of this game are very much guarded against this, rightfully so.
355 If you attempt this in actual game servers, this is considered fraud and IS detectable by the developer (every IAP has a receipt, logically!).
356
357 This version of the game is defunct as the protocol has changed quite a bit in the years since this version and additional anticheat
358 measures have been added since. You cannot transfer this status (or even purchase what you just did) from this game version to the next.
359
360 ### Onto the PeriodicalsRequest
361 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
362 the game disassembly again.
363
364 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
365 and look at any potential leads in strings. Drone seems like a good canditate, lets look into that.
366 ```
367 drone_fans2
368 drone_crash
369 drone_enemy
370 drone_hunter
371 r_icon_drone_rewards
372 b_icon_drone_boost
373 drone_touch
374 ei_drone_lights_green
375 ei_drone_lights_red
376 ei_drone_package
377 ei_drone_propeller
378 drone-boost
379 GENEROUS DRONES
380 ```
381 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`.
382
383 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
384 ```
385 piggy-boost (Rate piggy fills is increased.)
386 piggy-cap-boost (UNLIMITED PIGGY;Gains are retained when event ends.)
387 prestige-boost (PRESTIGE BOOST;Collect more soul eggs on prestige, you must prestige to take advantage of this event.)
388 earnings-boost (CASH BOOST;Regular earnings are increased.)
389 gift-boost (GENEROUS GIFTS;Boost applies to random gifts and video gifts.)
390 drone-boost (GENEROUS DRONES;Drones will produce larger rewards.)
391 epic-research-sale (EPIC RESEARCH SALE;Only applies to Epic Research.)
392 vehicle-sale (VEHICLE SALE;Applies to all vehicles.)
393 boost-sale (BOOST SALE;Applies to the gold price of boosts.)
394 boost-duration (BOOST TIME+;Boosts last longer, you must start a boost during the event.)
395 ```
396 I recall there being a few more boosts, but this is useful for getting started with compositing PeriodicalsResponse with an active running event.
397
398 ### Putting together the response
399 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.
400 ```
401 elif subpath == "get_periodicals":
402 # We don't actually need the information client sends us,
403 # we aren't verifying any stats about client in our server.
404 CurrentPeriodicals = EIProto.PeriodicalsResponse()
405 # In order to add items to a repeatable field in protobuf structure,
406 # we need to call .add() method on it
407 event = CurrentPeriodicals.events.events.add()
408 # Refer to ei.proto - we are filling fields for EggIncEvent structure here.
409 event.type = "drone-boost"
410 event.multiplier = 5.00
411 event.subtitle = "Drones will produce larger rewards."
412 event.identifier = "GENEROUS DRONES"
413 event.seconds_remaining = 300.0
414 # Lets make it respond with a 5 minute event (this will re-arm itself when client calls
415 # for get_periodicals again every 6 minutes)
416 return base64.b64encode(CurrentPeriodicals.SerializeToString())
417 ```
418
419 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.
420
421 ## Created the Server - What now?
422 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
423 trusting client in server. We also created a very basic server event, which always rearms itself to never expire.
424
425 What do we do next?
426
427 At this point, we can start dog-fooding the project. Lets start with whatever ball game throws at us as we progress.
428
429 ### Contracts
430 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
431 accept. So far we stil see our good friends `/ei/get_periodicals` and `/ei/save_backup` hammering the server at regular intervals.
432
433 When we created the periodicals response payload, you might have noticed in the protobuf message an optional field called `ContractsResponse contracts`. Lets see
434 what this ContractsResponse message contains.
435
436 ```
437 message ContractsResponse {
438 repeated Contract contracts = 1;
439 optional string warning_message = 4;
440 optional double server_time = 2;
441 optional uint32 max_eop = 3 [default = 1000];
442 }
443 ```
444
445 Notice there being an array of Contract messages right off the bat - lets find its message structure next:
446
447 ```
448 message Contract {
449 optional string identifier = 1;
450 optional string name = 9;
451 optional string description = 10;
452 optional Egg egg = 2;
453
454 repeated Goal goals = 3;
455 message Goal {
456 optional GoalType type = 1;
457 optional double target_amount = 2;
458 optional RewardType reward_type = 3;
459 optional string reward_sub_type = 4;
460 optional double reward_amount = 5;
461 optional double target_soul_eggs = 6;
462 }
463
464 repeated GoalSet goal_sets = 16;
465 message GoalSet {
466 repeated Goal goals = 1;
467 }
468
469 optional bool coop_allowed = 4;
470 optional uint32 max_coop_size = 5;
471 optional uint32 max_boosts = 12;
472 optional double minutes_per_token = 15 [default = 60];
473 optional double expiration_time = 6;
474 optional double length_seconds = 7;
475 optional double max_soul_eggs = 13;
476 optional uint32 min_client_version = 14;
477 optional bool debug = 11;
478 }
479 ```
480
481 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
482 [older revision of Contracts wiki page from 2021](https://egg-inc.fandom.com/wiki/Contracts?oldid=13015) and did some slight research.
483
484 From what I gather, at one point, there was only one set of contract rewards, shared between everyone - then they created a system where
485 beginners will get easier contract goals while more advanced players get harder contract goals.
486
487 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
488 and `repeated GoalSet goal_sets` is the *new* goal system that is split into Standard and Elite.
489
490 We also learn that in future game version, they completely reworked how contracts work *yet* again into a grading "bracket" system. Fortunately,
491 we do not have to worry about that in our current target revision.
492
493 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
494 contracts by creating a simple & easy contract called [Your First Contract](https://egg-inc.fandom.com/wiki/Contracts/Your_First_Contract?oldid=13547).
495
496 This page tells us all the information we need to compose our first Contract, so lets try to make one.
497
498 ```
499 elif subpath == "get_periodicals":
500 # We don't actually need the information client sends us,
501 # we aren't verifying any stats about client in our server.
502 CurrentPeriodicals = EIProto.PeriodicalsResponse()
503 # [...]
504 Contract = CurrentPeriodicals.contracts.contracts.add()
505 Contract.identifier = "first-contract"
506 Contract.name = "Your First Contract"
507 Contract.description = "We heard you are open to contract work! Help fill this order from the local pharmacy!"
508 Contract.egg = EIProto.Egg.MEDICAL
509 Contract.coop_allowed = False
510 Contract.minutes_per_token = 5
511 # Lets set expiry time to always be 3 days into future
512 Contract.expiration_time = time.time() + (3600.0 * 72.0)
513 Contract.length_seconds = 3600.0 * 4.0
514 # The wiki mentions that you cannot get this contract after you reach 5000 Soul Eggs
515 Contract.max_soul_eggs = 5000.0
516 # We should have the basic metadata set now, lets create the goalsets.
517 FirstSet = Contract.goal_sets.add()
518 Goal = FirstSet.goals.add()
519 # There is only one type of goal in this verison
520 Goal.type = EIProto.GoalType.EGGS_LAID
521 Goal.target_amount = 100000.0
522 Goal.reward_type = EIProto.RewardType.GOLD
523 Goal.reward_amount = 192
524 Goal = FirstSet.goals.add()
525 Goal.type = EIProto.GoalType.EGGS_LAID
526 Goal.target_amount = 500000000.0
527 Goal.reward_type = EIProto.RewardType.PIGGY_FILL
528 Goal.reward_amount = 10000
529 # Lets now add the Elite table, we can pretty much copy-paste the above here.
530 SecondSet = Contract.goal_sets.add()
531 Goal = SecondSet.goals.add()
532 Goal.type = EIProto.GoalType.EGGS_LAID
533 Goal.target_amount = 100000.0
534 Goal.reward_type = EIProto.RewardType.GOLD
535 Goal.reward_amount = 500
536 Goal = SecondSet.goals.add()
537 Goal.type = EIProto.GoalType.EGGS_LAID
538 Goal.target_amount = 500000000.0
539 Goal.reward_type = EIProto.RewardType.PIGGY_FILL
540 Goal.reward_amount = 10000
541 return base64.b64encode(CurrentPeriodicals.SerializeToString())
542 ```
543
544 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.
545 The contract goals are swapped! I am getting Elite contract rewards for a Standard contract.
546
547 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.
548 After swapping the sets around, we now see a contract with the corrected rewards.
549
550 I playtested it a bit and the contract worked as expected.
551
552 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
553 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
554 for roughly 2 weeks.
555
556 ## Conclusion so far
557 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.
558 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.
559
560 Before I give you the public source to the project, you might want to try your hand at creating a few more things.
561 - "Cloud" save, present a Backup to any new device that just started playing.
562 - Contracts Database and scheduler
563 - Server Event scheduler
564
565 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
566 about the project you are currently doing - refactoring becomes an essential part once you have documented the protocol to a comfortable degree.
567
568 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:
569 [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).
570
571 Thank you for reading and making it all the way to the end,
572 - Cernodile