Thanks guys for the support so far!
I started a quick poll to see what’s more important for you in protecting your app:
https://twitter.com/lua_protect/status/1220447701650890780?s=20
Thanks guys for the support so far!
I started a quick poll to see what’s more important for you in protecting your app:
https://twitter.com/lua_protect/status/1220447701650890780?s=20
Oh, right. There was one additional thing I wanted to say about using hashes versus encryption to protect your local save data.
Let’s consider that someone is trying to hack your system, but they have not yet gained access to your source code. In this case, both hashes and encrypted data files can be used to prevent tampering with the save data, however, if the save data isn’t encrypted then the hacker can still read what information is being stored in the file(s).
Now, the moment that a hacker gains access to your source code, i.e. they know what they are doing, then both measures fall flat because they can simply read how you create your hashes or what encryption you are using and what your keys are. Accessing source code could be made somewhat more difficult via obfuscation and other measures, but there’s some debate as to how effective they are.
While we wait for Dave’s approach, the most secure way to prevent cheating or cloning of your apps is to create online games where certain critical functions are run on your server. For instance, if you had a base building game, you’d send your player’s desired actions to the server, such as “build a house to grid {x,y}”. Then your server side script would look at this specific player’s base layout to see if 1) the area is free, 2) if the player has sufficient funds, 3) if all other requirements are met, and then the server side script would make the required changes to the player’s data (which is all on the server) and it’d only return “okay, build it” command back. Even if a cheater could make some local changes, they’d all be blocked or ignored by the server and cleared up by the next start up and if someone tried to clone your game, they’d lack the logic behind executing player commands.
Thanks.
I would agree with this, and our game does use the server for multi-player games. I’ve struggled with this for a few months now… in order to do the record-keeping on our server (which I am very comfortable with) that means that even a casual user would have to establish an account (register) and I feel that is a significant barrier for some who just wants to get in and play single-player games for fun and then get out.
Maybe I should adjust my thinking. These are the sorts of conversations I’ve been needing. I can do the programming with no problem, but there are so many non-programming aspects to launching a mobile game/app that I sometimes feel like I am flying blind, especially since this is our first major attempt.
Perhaps I will adjust our game so that no server involvement and no registration is required unless the player wants to appear on leaderboards (and of course if you want to play multi-player).
Thoughts? Is that maybe a better approach?
While were on this topic, I thought to google a few Lua obfuscation scripts and see how they do and how much difficulty I would have in “breaking” them.
The first obfuscator that I found simply converted the text into bytecode, i.e.
local thing = [[function secret() print( "no one will ever find me" ) end]] -- Obfuscation function source: https://glot.io/snippets/f1tt9okm5w local encoded = thing:gsub(".", function(bb) return "\\" .. bb:byte() end) or thing .. "\"" print( encoded ) -- Output: \102\117\110\99\116\105\111\110\32\115\101\99\114\101\116\40\41\10\9\112\114\105\110\116\40\32\34\110\111\32\111\110\101\32\119\105\108\108\32\101\118\101\114\32\102\105\110\100\32\109\101\34\32\41\10\101\110\100\10
Then I thought, “Alright, so how to revert it?” and here’s what I came up with in around 5 minutes.
local function bytecodeToString( code ) local t, a = {}, 1 while true do if code:find( "\\", a ) then local b = code:find( "\\", a+1 ) if b then t[#t+1] = code:sub(a+1,b-1):char() a = b else t[#t+1] = code:sub(a+1):char() break end else break end end return table.concat( t ) end local decoded = bytecodeToString( encoded ) print( decoded )
Well, granted, that obfuscation function wasn’t all that difficult to break, so I found another one to try as well.
-- LuaSeel, source: https://github.com/Direnta/LuaSeel local a=[[function superSecret() print( "this time no one will ever find me" ) end]] a="--// Decompiled Code. \n"..a;function Obfuscate(b)local c="function IllIlllIllIlllIlllIlllIll(IllIlllIllIllIll) if (IllIlllIllIllIll==(((((919 + 636)-636)\*3147)/3147)+919)) then return not true end if (IllIlllIllIllIll==(((((968 + 670)-670)\*3315)/3315)+968)) then return not false end end; "local d=c;local e=""local f={"IllIllIllIllI","IIlllIIlllIIlllIIlllII","IIllllIIllll"}local g=[[local IlIlIlIlIlIlIlIlII = {]]local h=[[local IllIIllIIllIII = loadstring]]local i=[[local IllIIIllIIIIllI = table.concat]]local j=[[local IIIIIIIIllllllllIIIIIIII = "''"]]local k="local "..f[math.random(1,#f)].." = (7\*3-9/9+3\*2/0+3\*3);"local l="local "..f[math.random(1,#f)].." = (3\*4-7/7+6\*4/3+9\*9);"local m="--// Obfuscated with LuaSeel 1.1 \n\n"for n=1,string.len(b)do e=e.."'\\"..string.byte(b,n).."',"end;local o="function IllIIIIllIIIIIl("..f[math.random(1,#f)]..")"local p="function "..f[math.random(1,#f)].."("..f[math.random(1,#f)]..")"local q="local "..f[math.random(1,#f)].." = (5\*3-2/8+9\*2/9+8\*3)"local r="end"local s="IllIIIIllIIIIIl(900283)"local t="function IllIlllIllIlllIlllIlllIllIlllIIIlll("..f[math.random(1,#f)]..")"local q="function "..f[math.random(1,#f)].."("..f[math.random(1,#f)]..")"local u="local "..f[math.random(1,#f)].." = (9\*0-7/5+3\*1/3+8\*2)"local v="end"local w="IllIlllIllIlllIlllIlllIllIlllIIIlll(9083)"local x=m..d..k..l..i..";"..o.." "..p.." "..q.." "..r.." "..r.." "..r..";"..s..";"..t.." "..q.." "..u.." "..v.." "..v..";"..w..";"..h..";"..g..e.."}".."IllIIllIIllIII(IllIIIllIIIIllI(IlIlIlIlIlIlIlIlII,IIIIIIIIllllllllIIIIIIII))()"print(x)end;do Obfuscate(a)end
Well, now we’ve got something interesting! The GitHub page lists its features as: “Math value algorithms, Variable obfuscator/changer, Bytecode encryption, Randomized strings - credit to SadBoy22”, so now we are probably dealing with some serious shite… but let’s give it a go!
The obfuscated/encrypted code is as follows:
--// Obfuscated with LuaSeel 1.1 function IllIlllIllIlllIlllIlllIll(IllIlllIllIllIll) if (IllIlllIllIllIll==(((((919 + 636)-636)\*3147)/3147)+919)) then return not true end if (IllIlllIllIllIll==(((((968 + 670)-670)\*3315)/3315)+968)) then return not false end end; local IIlllIIlllIIlllIIlllII = (7\*3-9/9+3\*2/0+3\*3);local IIlllIIlllIIlllIIlllII = (3\*4-7/7+6\*4/3+9\*9);local IllIIIllIIIIllI = table.concat;function IllIIIIllIIIIIl(IIllllIIllll) function IIlllIIlllIIlllIIlllII(IIlllIIlllIIlllIIlllII) function IllIllIllIllI(IIlllIIlllIIlllIIlllII) end end end;IllIIIIllIIIIIl(900283);function IllIlllIllIlllIlllIlllIllIlllIIIlll(IllIllIllIllI) function IllIllIllIllI(IIlllIIlllIIlllIIlllII) local IIllllIIllll = (9\*0-7/5+3\*1/3+8\*2) end end;IllIlllIllIlllIlllIlllIllIlllIIIlll(9083);local IllIIllIIllIII = loadstring;local IlIlIlIlIlIlIlIlII = {'\45','\45','\47','\47','\32','\68','\101','\99','\111','\109','\112','\105','\108','\101','\100','\32','\67','\111','\100','\101','\46','\32','\10','\102','\117','\110','\99','\116','\105','\111','\110','\32','\115','\117','\112','\101','\114','\83','\101','\99','\114','\101','\116','\40','\41','\10','\32','\32','\112','\114','\105','\110','\116','\40','\32','\34','\116','\104','\105','\115','\32','\116','\105','\109','\101','\32','\110','\111','\32','\111','\110','\101','\32','\119','\105','\108','\108','\32','\101','\118','\101','\114','\32','\102','\105','\110','\100','\32','\109','\101','\34','\32','\41','\10','\101','\110','\100','\10',}IllIIllIIllIII(IllIIIllIIIIllI(IlIlIlIlIlIlIlIlII,IIIIIIIIllllllllIIIIIIII))() -- Well, that's all fine and good, but all we are interested in are the bytes, so let's just copy and paste them into print() print( "'\45','\45','\47','\47','\32','\68','\101','\99','\111','\109','\112','\105','\108','\101','\100','\32','\67','\111','\100','\101','\46','\32','\10','\102','\117','\110','\99','\116','\105','\111','\110','\32','\115','\117','\112','\101','\114','\83','\101','\99','\114','\101','\116','\40','\41','\10','\32','\32','\112','\114','\105','\110','\116','\40','\32','\34','\116','\104','\105','\115','\32','\116','\105','\109','\101','\32','\110','\111','\32','\111','\110','\101','\32','\119','\105','\108','\108','\32','\101','\118','\101','\114','\32','\102','\105','\110','\100','\32','\109','\101','\34','\32','\41','\10','\101','\110','\100','\10'" ) -- Output: '-','-','/','/',' ','D','e','c','o','m','p','i','l','e','d',' ','C','o','d','e','.',' ',' ','f','u','n','c','t','i','o','n',' ','s','u','p','e','r','S','e','c','r','e','t','(',')',' ',' ',' ','p','r','i','n','t','(',' ','"','t','h','i','s',' ','t','i','m','e',' ','n','o',' ','o','n','e',' ','w','i','l','l',' ','e','v','e','r',' ','f','i','n','d',' ','m','e','"',' ',')',' ','e','n','d',' '
Well, that doesn’t look all that obfuscated or encrypted after all. All that we need to do now is get every fourth character, starting from the second character and we’ll get:
-- This function took between 1 and 2 minutes to write. local function bytecodeToStringWithAwesomeSauce( code ) local t = {} for i = 2, code:len(), 4 do t[#t+1] = code:sub(i,i) end return table.concat( t ) end local decoded = bytecodeToStringWithAwesomeSauce( "'\45','\45','\47','\47','\32','\68','\101','\99','\111','\109','\112','\105','\108','\101','\100','\32','\67','\111','\100','\101','\46','\32','\10','\102','\117','\110','\99','\116','\105','\111','\110','\32','\115','\117','\112','\101','\114','\83','\101','\99','\114','\101','\116','\40','\41','\10','\9','\112','\114','\105','\110','\116','\40','\32','\34','\116','\104','\105','\115','\32','\116','\105','\109','\101','\32','\110','\111','\32','\111','\110','\101','\32','\119','\105','\108','\108','\32','\101','\118','\101','\114','\32','\102','\105','\110','\100','\32','\109','\101','\34','\32','\41','\10','\101','\110','\100','\10'" ) print( decoded ) -- Output: --// Decompiled Code. function superSecret() print( "this time no one will ever find me" ) end
The point is that I’ve spent a lot longer to write this lengthy post than I did to create two different functions to undo obfuscation. Obfuscation by itself isn’t worth it, which is why I am interested in seeing what Dave has planned.
This is exactly what I do… encrypt local save (AES-256) and mirror to my servers.
TBH it is not even worth trying to protect the game data. Players that want to “hack” will simply max out on fake purchases.
I just let them do it and ban them from all internet features so no-one can see what they have done.
The other hack that is so easy to do is in memory value changing. Say player has 199 coins, they simple search all memory addresses containing 199 and change to 9999999999999.
In my experience this is very much less than 1% and certainly from players you will never monetize. So don’t waste your time.
lol I was just looking how to obfuscste my code and then I find a post where the exact function I was using was broken in minutes! I guess it is pointless
BTW, this is a great discussion. Thanks to all who are participating.
To be clear, I am not that worried or interested in obfuscating the SOURCE code, since I’m not distributing the source. Wait… that IS correct, isn’t it? If I do a build in Corona it’s actually compiling to something binary or machine code, right? Right?
Holy Schnikes… if that assumption is incorrect then I must take a big step back to reevaluate. :blink:
Lua is compiled into bytecode when built and that can be easily extracted from apks.
Corona compiles your Lua source code into standard Lua bytecode. There are decompilers and programs that revert the bytecode back to Lua. With Lua Protect, the bytecode is encrypted along with a few other security measures, so it is much harder to revert it.
Dave
I’ve had to DMCA for ripped assets but never for code. Concentrate on what _ needs protecting _.
Thanks! I will dig into this just a bit more, then. I am used to decompilers/disassemblers that revert back to ‘a version of the source code that operates identically’ but not the exact same source code (e.g. variables have generic names, comments are gone, some optimizations may have occurred, etc.)
Dave, let me (us?) know if you need beta testers when the time is right.
Yeah I guess it is better to make an app that everyone wants to copy and then copy it than to make an app and protect it when no one wants to copy it.
A most salient point.
Will do, thanks.
Thanks again for all of the feedback.
I am now in the process of ripping out large quantities of code and local storage for things like badges, rankings, etc. and disabling the ability to participate-in and enjoy those things unless registered and logged-in on our server. So this means that casual players will be able to install and play immediately just for giggles, but then if they want to participate in the game community and be ranked, earn badges, etc. they must be registered and logged-in. THEN, when they want to play multi-user games (and also turn off advertising) they will have to do the IAP to unlock these features.
I think this is a saner way to accomplish what I was trying to do, but willing to hear feedback if anyone finds a flaw in this reasoning.
Now, I think the only thing left for me to figure out is how to validate (server-side) with both app stores whether or not the user has truly paid for the unlocked version. Can anybody recommend any good links/tutorials for that?
@coronasdk66 - please keep us posted. I’m keenly interested in how you will implement the server-side validation
Will do soon. Did most of the new work today.
Dave, sorry if this is a silly question. But, if some encryption is used, then the app must have the key to decrypt and run the app right? If that’s the case, how can the key itself be secured? First thought I had was that the key could come from a server, but that doesn’t help at all since the code that accesses the server would have to be not-encrypted since we don’t have a key yet. So, how does the decryption could work?
It’s not a silly question at all. I’m glad you are questioning because it is something I’ve spent a lot of time on. Obviously it doesn’t help if I tell how I handle it in a public forum
Dave
Basically I will leave settings I don’t really care about (like whether the user preference for music and sound effects is ON or OFF) on the device. If they want to mess with those, that’s fine. I will also store high score, # games played, etc. on the device and they will be the authoritative numbers for game play UNTIL THE USER ESTABLISHES AN ACCOUNT (meaning registers with an email, a game nick and a password). So for a casual player (who is only playing the single-player game) they do not have to take the time to establish an account.
However, they MUST register if they want to do things like play multiplayer games, see themselves on the leaderboard, earn badges, etc. Once they register, the high score, number of games played, badges won storage will be on the server, and that storage will be the authoritative source for these numbers. Even if they’ve manipulated their high score and # of games played locally (prior to registration) it won’t really matter, because those numbers are for single-player games and they only show to the player, not to the community.
When they login, a unique session ID will be created for that login session and any previous active sessionIDs will be expired. This has the effect of ‘logging out’ any other devices logged-in with those same credentials, even if that player is in the middle of a game, so should discourage account sharing. Other things will also go into the creation of this session ID (like IP address, device Unique ID, etc.) to further protect. And since these things could also be spoofed by a clever person, the server will also watch for other scenarios (like… did this registered user start/finish a game while another was already in progress?) and will (probably) summarily invalidate all sessions that appear to be duplicates. The server will also have the ability to ban a player’s login for a certain amount of time based on perceived bad behavior (so perhaps if this happens repeatedly in the same hour, the player’s account will be suspended for 5 minutes on the first offense, an hour on the second, etc.)
There are some other techniques going on behind the scenes to watch for cheats as well, but I do not wish to divulge those here.
To be clear, I know that this is likely to be a never-ending cat and mouse game, but by moving as much as possible to server-side (as was correctly suggested in this thread) I think (hope?) we will be able to tweak the monitoring and responses locally without having to roll-out a lot of app updates every time we see a new technique for trying to bypass the system.
Although I am concerned about those who would patch the app locally to try and get a free version (i.e. theft) I am currently more concerned about those who would ‘cheat’ their scores and sully the experience for the rest of this game’s community.
FEEDBACK/CRITICISM IS WELCOME