
Roulette Royale (com.mw.rouletteroyale, uitgegeven door Mywavia Studios) is een gratis Android casinospel met meer dan 10 miljoen downloads in Google Play. Je krijgt een stapel chips, speelt die weg aan de tafels, en zodra je door je voorraad heen bent word je richting de shop geduwd. Boven op de chips zitten nog twee valuta (gems en diamonds), een VIP-programma met vijf niveaus, en een "Unlock All" aankoop die hogere chipwaardes en bulkaankopen in de shop vrijspeelt.
Er wordt nooit echt geld uitgekeerd. De chips, gems en diamonds zijn virtueel, je kunt niets laten uitbetalen, en dit is een spel en geen echt casino. Geld gaat maar één kant op, namelijk wanneer een speler via de Play Store chips of de premium unlock koopt. Dat is voor de rest van dit artikel wel van belang, want juist die betaalde niveaus en premium functies laat de app door de client zelf bepalen.
Wat wij wilden weten is wie bepaalt hoeveel je bezit. In een goed ontworpen spel is de server de enige bron van waarheid: de client vraagt om een inzet, de server keurt die goed en trekt het bedrag af, en de client laat zien wat er terugkomt. In Roulette Royale houdt de client de boekhouding bij en vertelt de server hoe rijk hij besloten heeft te zijn.
Dit is deel 1 van twee. Deel 1 gaat over de clientkant: de APK uit elkaar trekken, opzoeken waar de economie wordt bepaald, die aanpassen, opnieuw bouwen en installeren, en daarna met Frida de TLS-laag eraf halen om te zien hoe de aangepaste client zijn zelfbedachte rijkdom aan de server doorgeeft. Het configuratieantwoord van de server bevestigt de ontwerpfout. In deel 2 gebruiken we de request-signing en de endpoints die we hier terugvinden, en blijkt dat de server nergens controleert of een account wel van jou is. Daarmee wordt de clienttruc lees- en schrijftoegang tot elk account, plus een RabbitMQ-broker die de hele spelerspopulatie blootlegt.
Disclosure: we hebben dit op 28 augustus 2026 bij de leverancier gemeld en op 6 en 15 september herinnerd. Op geen van de drie mails kwam antwoord. We publiceren 23 dagen na de eerste melding, met de bevindingen compleet en de werkende onderdelen eruit. Er staat hier geen proof of concept-code, geen broker-wachtwoord, geen srscore-sleutel en geen enkel echt account-identificatienummer; de leverancier kan dat allemaal op verzoek krijgen. Deel 2 bevat de volledige tijdlijn en de onderbouwing. De scope is de mywavia.com-hosts en classspace.in. Uitsluitend testaccounts.
| Onderdeel | Waarde |
|---|---|
| Package | com.mw.rouletteroyale, clientversie 17.2 (uit MW-version) |
| Toestel | Android-emulator, x86_64, geroot met Magisk, frida-server 17.15.3 als root |
| Accountserver | https://adgen-self.mywavia.com/ (/config, /gameserver_roul, /srscore) |
| Realtime broker | wss://roulmulti.mywavia.com:443/roul-ws (STOMP, deel 2) |
| Gereedschap | apktool, jadx, frida, zipalign, apksigner, Burp Suite, en de MCP-wrappers eromheen |
Root en Frida vooraf:
$ adb shell su -c id
uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0
$ adb shell su -c 'setsid /data/local/tmp/frida-server -D &'
$ frida-ps -U | grep -i roulette
9156 Roulette Royale - Casino
We gebruiken twee tools naast elkaar omdat ze verschillende vragen beantwoorden. apktool MCP levert de smali, die je kunt bewerken en weer kunt bouwen. JADX MCP levert gedecompileerde Java, die prettiger leest als je logica zoekt. De klassenamen in de libraries zijn versluierd tot losse letters, maar de eigen klassen van de app staan gewoon onder com.mw.rouletteroyale, en een paar vallen meteen op:
RRRefreshActivityenRRGameActivity, de belangrijkste spelschermenAbstractMasterActivity, een basisklasse waar veel schermen van ervenVipTierManager, het VIP-programmaRRShopActivity, de shopGameRoomenChatManager, het multiplayertransport (deel 2)
De economie staat in het geheugen in een uitgelezen JSON-configuratie (MWDeviceGlobals.config) en wordt opgehaald via kleine getters die elk een int teruggeven op basis van een versluierde sleutel. Er wordt per scherm niets bij de server opgehaald. De getters die ertoe doen:
RRRefreshActivity.getChips(), het chipsaldoRRGameActivity.getGemsValue(), gemsAbstractMasterActivity.getDiamondsValue(), diamondsVipTierManager.get_sp(), VIP-statuspuntenVipTierManager.get_tier(), het VIP-niveauRRGameActivity.bigChipsAvailable(), de drempel voor de hoge chipwaardesRRShopActivity.enableBulk(), de drempel voor bulkaankopen in de shop
Gedecompileerd is zo'n getter simpel en doet hij geen enkele serveraanroep:
public int getGemsValue() {
try {
return MWDeviceGlobals.config.getInt("<obfuscated_key>");
} catch (Exception e) {
return 0;
}
}
VipTierManager werkt net zo. Die leest get_sp() en get_tier() en tekent daarmee het VIP Center, en de server toetst het getoonde niveau nergens voordat de client ernaar handelt.
Oorzaak. De client is leidend voor de economie. Saldo's, extra valuta, VIP-niveau en premium drempels worden op het toestel berekend en afgedwongen. Verander wat deze getters teruggeven en je verandert het vermogen, het niveau en de vrijgespeelde functies van de speler zonder de server ook maar aan te raken.
Gedecompileerde Java compileert niet netjes terug, dus bewerken we smali, waar de wijziging exact is en apktool het geheel weer bouwt. De aanpak is om de inhoud van elke getter weg te gooien en te vervangen door een constante en een return. De opcodegrootte telt daarbij: const/4 gaat tot 7, const/16 tot 32767, en const neemt een volledige 32 bits.
Gems, voor de wijziging:
.method public getGemsValue()I
.locals 3
sget-object v0, Lcom/mw/.../MWDeviceGlobals;->config:Lorg/json/JSONObject;
const-string v1, "<obfuscated_key>"
invoke-virtual {v0, v1}, Lorg/json/JSONObject;->getInt(Ljava/lang/String;)I
move-result v0
return v0
.end method
Erna, een vaste 10000 (0x2710):
.method public getGemsValue()I
.locals 1
const/16 v0, 0x2710
return v0
.end method
Diamonds krijgen dezelfde 10000. De VIP-statuspunten gaan naar 999999 (0xf423f), ruim voorbij de hoogste drempel, en het niveau zetten we vast op 5, Ruby. Booleans zijn ints, dus de premium drempels worden const/4 v0, 0x1:
.method public get_sp()I
.locals 1
const v0, 0xf423f
return v0
.end method
.method public get_tier()I
.locals 1
const/4 v0, 0x5
return v0
.end method
.method public bigChipsAvailable()Z
.locals 1
const/4 v0, 0x1
return v0
.end method
enableBulk() krijgt dezelfde wijziging van één regel. getChips() hebben we bewust met rust gelaten: als elke teller een verdacht rond bedrag laat zien valt dat zelfs op een schermafdruk meteen op, dus verplaatsten we de waarden die het punt bewijzen (gems, diamonds, VIP, unlocks) en lieten we het zichtbare chipsaldo normaal staan. Met precies dezelfde ingreep pak je getChips() er alsnog bij.
Android installeert geen APK zonder handtekening, en installeert ook geen anders ondertekende build over de winkelversie heen, dus dit komt binnen als een nieuwe app:
$ apktool b roulette-royale -o roulette-royale-modded.apk
$ zipalign -p -f 4 roulette-royale-modded.apk roulette-royale-aligned.apk
$ keytool -genkey -v -keystore mod.keystore -alias mod -keyalg RSA -keysize 2048 \
-validity 10000 -dname "CN=websec" -storepass modpass -keypass modpass
$ apksigner sign --ks mod.keystore --ks-key-alias mod --ks-pass pass:modpass \
--key-pass pass:modpass --out roulette-royale-signed.apk roulette-royale-aligned.apk
$ adb uninstall com.mw.rouletteroyale && adb install roulette-royale-signed.apk
Success
Bij het opstarten oogt er niets anders, en het hoofdmenu toont nog steeds de gebruikelijke 25.000 chips omdat we die getter hebben laten staan.

Het VIP Center draait volledig op get_sp() en get_tier(), en die liegen nu allebei in ons voordeel: 999.999 statuspunten en het hoogste Ruby-niveau, waar een gewone speler alleen komt door over de drempel van 10000 punten te gaan na lang spelen of flink betalen. De drempelrij (0, 1000, 3000, 6000, 10000) is dezelfde die de server meestuurt, daar komen we zo op terug.

bigChipsAvailable() op true zetten speelt de premium chips vrij. Open een tafel, tik op de chipkiezer, en in plaats van de lage waardes waar een gratis speler het mee moet doen staat alles er: 500, 1000, 10K, 100K, 1M, 10M, tot 200M.

De luxeshop waar de extra valuta en statusitems worden uitgegeven laat de aangepaste toestand ook zien.

Impact van een client-leidende economie. Elke speler kan zichzelf willekeurig veel gems en diamonds geven, het hoogste VIP-niveau met de bijbehorende blijvende vermenigvuldigers (tot 3,0x VIP-winst) toekennen zonder te betalen, en de "Unlock All" premium functies (hogere chips, bulkaankopen) vrijspelen die anders echt geld kosten.
getChips()ligt één wijziging verderop. Zowel de VIP- als de premium-monetisatie wordt offline onderuitgehaald, en zoals het volgende deel laat zien praat de aangepaste client daarna gewoon met de live server.
Met het toestel via Burp geproxyd en het CA-certificaat vertrouwd bleef het API-verkeer van het spel weg, en gaf de app de melding "Account Creation Failed, network error". De gebruikelijke verdachte is certificate pinning, dus die halen we met Frida weg in plaats van de vertrouwenslogica van de app te reverse engineeren. Frida MCP hangt zich aan het draaiende proces (frida-server draait al als root) en injecteert een universeel unpinning-script onder de V8-runtime.

Het script haakt in op de conscrypt TrustManagerImpl.verifyChain, de SSLContext.init-truc met een accept-all trustmanager, OkHttp CertificatePinner, de hostnameverifiers en WebView-SSL-fouten. Eén regel uit de uitvoer is veelzeggend: de klasse OkHttp CertificatePinner zit er niet eens in.
[unpin] skip okhttp3 CertificatePinner: ClassNotFoundException
[unpin] hooks installed (5): conscrypt.TrustManagerImpl.verifyChain,
conscrypt.TrustManagerImpl.checkTrustedRecursive, SSLContext.init,
apache.AbstractVerifier.verify, WebViewClient.onReceivedSslError
Geen OkHttp-pinning betekent dat de game-API geen OkHttp gebruikt. Die gebruikt het conscrypt-pad van het platform en een oude Apache HTTP-client, allebei afgedekt door onze conscrypt- en SSLContext-hooks. Zonder pinning en met Burp Intercept uit (laat je die aanstaan, dan lopen vastgehouden requests in een timeout en lijkt het precies op een netwerkfout) komt het aanmaken van het account er wel doorheen en wordt het echte API-verkeer leesbaar.
Ontsleuteld ziet de config-aanroep er zo uit, rechtstreeks uit Burp met de toestel-identificatie weggelaten:
GET /config?appid=MW_RR_ANDROID2&cv=17.2&rand=0.798656 HTTP/1.1
Host: adgen-self.mywavia.com
MWHDR-MWHDR2: 211575b094b4804
MWHDR-MWHDR1: 9607bc08729587b91903f952b40ef485
MW-uuid: f6a5606d........ (device udid, weggelaten)
MW-platform: android
MW-appid: MW_RR_ANDROID
MW-version: 17.2
User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.5)
De Apache-HttpClient user agent bevestigt wat Frida al liet vermoeden, en het antwoord beschrijft dezelfde betaalmuur die we offline hebben omzeild:
{
"cfg": {
"unlock_all_diam": 100000,
"vip_hdr": [0, 1000, 3000, 6000, 10000],
"vip_mult": [[1.0,1.0,1.0,1.0,0], ..., [1.4,1.4,1.4,1.4,0.8]],
"inapp": { "unlock": { "best": [{
"id": "unlock_all",
"title": "Unlock All Features",
"short_desc": "Unlock All Features - Higher Chips & Shop Bulk Purchases."
}]}}
}
}
De vip_hdr-array is precies de drempelreeks van Bronze tot Ruby uit de schermafdruk van het VIP Center, inclusief de 10000 waar onze get_sp()-override overheen walste. Het product unlock_all wordt in de eigen woorden van de leverancier omschreven als "Higher Chips & Shop Bulk Purchases", en dat is nu net wat de drempels bigChipsAvailable() en enableBulk() regelen.
Impact en bewijs. De server stuurt de client de definitie van de betaalmuur mee, de niveaudrempels en het unlock-product met wat het oplevert, en vertrouwt er vervolgens op dat de client die handhaaft. Er wordt nergens gecontroleerd of een toestel dat Ruby-niveau of vrijgespeelde functies claimt daar ook voor betaald heeft.
De headers MWHDR-MWHDR1 en MWHDR-MWHDR2 op dat verzoek vormen het zelfgebouwde ondertekeningsschema van de app. De client berekent en beheert de handtekening zelf, dus het is geen integriteitscontrole, maar een verzoek moet hem wel meedragen om geaccepteerd te worden, en we hebben hem nodig om in deel 2 onze eigen aanroepen te vervalsen. apktool MCP over classes2.dex levert de hele backend plus beide werkende geheimen als constanten.

De ondertekenaar is com.mw.secure.S, rechtstreeks uit de gedecodeerde app.

Hij draait volledig op het toestel. Er komt geen servernonce, sleutel of token aan te pas, alleen een herhaalde MD5 over de querystring en een openbare tijdstempel, met één eigenaardigheid: er komt een nul voor als de hex op 31 tekens uitkomt.


We hebben ook gecontroleerd of er geen native achterdeur in zit. De enige .so in de APK is de AndroidX DataStore-teller.

Impact van het ondertekeningsontwerp.
MWHDRis een checksum zonder sleutel over gegevens die de aanroeper toch al heeft, dus elke client kan elk verzoek voor elk account offline ondertekenen. Het maakt elke/gameserver_roul- en/srscore-bevinding uit deel 2 mogelijk en je kunt er beter van uitgaan dat het nul beveiliging biedt.
De getters hierboven bepalen wat het toestel toont. Daarnaast is er een vermogen aan de serverkant dat naar /srscore gaat, en dat is net zo makkelijk te vervalsen. Dit endpoint gebruikt wel een echt geheim, de 129 tekens lange string uit classes2.dex, maar het is in elke installatie dezelfde string.

Een vermogensmutatie is één GET. CurrentWorth en de ranglijstvelden gaan in het ondertekende score-blok, terwijl een reeks valuta- en unlockvelden in de querystring buiten de hash om meeliften:
GET /srscore?type=put&un=websec.nl&all=Y&appid=MW_RR_ANDROID&cv=17.2
&uuid=<rdsid>&o_uuid=<udid>&isocc=
&score=...***CurrentWorth<<>>100000000000***...&hash=<md5(score_payload + key)>
&gems=0&diamonds=0&unlock=0&invite_coupon=0&promotion_coupon=0&defer=0 <-- NIET door de hash gedekt
We hebben de sleutel eruit gehaald, de salt en de hash nagebouwd, een vermogen van 100000000000 ingestuurd, en na een hersynchronisatie toont het account dat gewoon.

Impact van het srscore-ontwerp. Twee problemen. Doordat de sleutel gedeeld is bewijst een geldige handtekening alleen dat de aanroeper de APK heeft uitgepakt, dus elke speler kan zijn eigen
CurrentWorthen alle ranglijststatistieken zetten. En de staart (gems,diamonds,unlock, coupons,defer) wordt buiten de ondertekende payload aangeplakt, dus de premium valuta is zonder enige sleutel aan te passen.
Elk symptoom is terug te voeren op één beslissing: de economie is client-leidend. Saldo's, valuta, VIP-status en premium unlocks worden op het toestel berekend en afgedwongen, de server neemt aan wat het toestel meldt, en stuurt de client zelfs de betaalmuur mee om te handhaven. Versluiering, pinning en de MWHDR-handtekening maken meekijken lastiger en verder niets; wij hadden de pinning met een standaardscript in een paar minuten weg.
Alles hierboven ging over ons eigen account. In deel 2 veranderen we één veld, het account-ID in het verzoek, en blijkt dat de server nooit controleert of dat account van ons is. De MWHDR-ondertekenaar en de gedeelde srscore-sleutel van hierboven worden daarmee: het uitlezen van e-mailadres, toestel-ID, Facebook-ID, vermogen en privékamercode van elk account; het aanpassen van naam, vermogen, statistieken en valuta van elk account; het overnemen van elke profielfoto; en via de RabbitMQ-broker, bereikbaar met één gedeeld wachtwoord, het meelezen van elke tafel en het plaatsen van chatberichten namens elke speler.
