📜 ⬆️ ⬇️

Renewal of the old code or how to do well to an application that is bad

Frankly, I play games no more often than I write in a habr, but I always had a certain weakness in the rhythm genre. At one time I really liked Audiosurf, later I came across its different clones, Beat Hazard, osu. Later I came across Deemo and Duet in the App Store, from which I got a lot of pleasant minutes.

During an endless sleepless evening, wandering in a half-nap on various sites, I noticed a little thing that was part of my interests. Free download, familiar company, yeah, it means there will be a lot of paid content. On the other hand, they probably will play something for nothing. Having drawn an obvious conclusion, I downloaded the game ... and saw a pristine white screen.

Further presentation will not go about what kind of angry feelings I have caused the negligence of publishers who have lost the code and / or scored on the update, but about getting the desired in a situation of easy hopelessness.

Reading the description, and it is still worth doing sometimes, brought two news:

Gorgeous. Probably, many of them would have retreated after this, especially since the platform is not x86 and the game is not, say, hit (although I really liked the mechanics later). But I had a personal insult, and the next morning I decided to examine the patient.

Revitalization


First of all, it is worth looking, but doesn’t something bring the monster to the log? We come on ssh, we open monitoring of syslog and we start the application. Instantly, the screen is filled with a bunch of messages like:
  bird [1792] <Error>: setting error: <NSError: 0x15df7ba0 (BRCloudDocsErrorDomain: 5) - {
     NSDescription = "No document at URL";
     NSFilePath = "/ private / var / mobile / Library / Mobile Documents / JZKSZCX743 ~ com ~ square-enix ~ tact / oks_savedata.bin";
     NSUnderlyingError = "<NSError: 0x15df7b60 (NSPOSIXErrorDomain: 2) - {\ n NSDescription = \" No such file or directory \ "; \ n}>";
 }> 

Op-pa, in the bull's eye. iOS 9 broke the iCloud toy, for some reason the file with saving and settings was not created, and the launch went into an endless loop. Let's try to create it:
  touch "/ private / var / mobile / Library / Mobile Documents / JZKSZCX743 ~ com ~ square-enix ~ tact / oks_savedata.bin" 

And here is the interface 0_o. Pacified, I climbed into the Story, simultaneously putting on headphones. I saw there: there is still nothing :(
')


It is quite understandable, there must be some kind of structure in the file, but I barbarously took it and shoved the stub. Further search by file name leads to two locations:
  find / private / var -name oks_savedata.bin
 /private/var/mobile/Containers/Data/Application/long-uuid/Documents/oks_savedata.bin
 / private / var / mobile / Library / Mobile Documents / JZKSZCX743 ~ com ~ square-enix ~ tact / oks_savedata.bin 

But alas, the first file itself is not created without the second. We need to go deeper.

Let's dump the application, unpack the ipa (remember, this is a regular zip), look inside the application bundle:

The executable file, according to the CFBundleExecutable in Info.plist, is an oks, FAT binary with two architectures:
  jtool -h oks
 Fat binary, big-endian, 2 architectures: armv7, armv7s
 Specify one of these architectures with the environment switch 

Hooray, no arm64. Well, for me it is in a sense a plus, since iOS and its Teutonic limitations, and even in the specifics of the arm, despite the introduction, is not my topic. I rarely come here, and not from a good life. Enjoying entitlements, like a standard set with iCloud.
Jtool output
  jtool --ent -arch armv7s oks
 Warning: companion file ./oks.ARM (unknown) .69981636-7F33-3C43-BD58-7F5BBE2A6CCA not found
 <! DOCTYPE plist PUBLIC "- // Apple // DTD PLIST 1.0 // EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version = "1.0">
	 <dict>
		 <key> keychain-access-groups </ key>
		 <array>
			 <string> JZKSZCX743.com.square-enix.tact </ string>
		 </ array>

		 <key> com.apple.developer.ubiquity-container-identifiers </ key>
		 <array>
			 <string> JZKSZCX743.com.square-enix.tact </ string>
		 </ array>

		 <key> application-identifier </ key>
		 <string> JZKSZCX743.com.square-enix.tacthd </ string>

	 </ dict>
 </ plist> 


Immediately visits the idea, but do we need it. If you then put the application on a device without jailbreaking (re-signing it), then iCloud will not work anyway, and if it does not work, it may cut it off right away. At the same time suddenly it will work itself? It is said - done: re - signed by self - signed certificate without specifying entitlements ( codesign -f -s mycert oks ), at the same time we add the UIFileSharingEnabled property for backups via iTunes to Info.plist (we ’ll find it useful), package the application back to ipa and install it on the device.

You guessed it, after that I played with satisfaction for a few minutes. Exactly before the passage of the first four levels, when the raspberries were brutally cropped built-in purchases. I go to the Store section and understand that it is not just expensive, but priceless. I broke the Store when I was repairing the launch, so even if I really want to, then I won’t buy anything. By that time, I already deleted the original ipa and did not want to pump it. I think, but oh well, I’ll quickly patch up the Inapup and that’s all.

Jam fix 1


Look through the list of Objective-C classes, look for something related to the problem.
Class list
 jtool -d objc oks oksAppDelegate oksExtendView EAGLView oksViewController SeqLogo SeqTitle Sequence SeqMan Sprite SprMan SndOne SndMan SeqIngame DataOne DataMan TouchEff TouchOne KeyMan ChartObj ResultIconOne ResultIcon Gakudan ChobjEndEffOne ChobjEndEff FadeMan NumberSpr SeqResult MenuStatus PopUp SeqMainMenu SeqStory SptMan SptCharaOne SptChara SptMsgLog SptMsg SptBg SptStill SptCol SptShake SptSnd SeqSelConcert MenuCmnBtn OnpuEffOne OnpuEff SelStoryChap SelStoryLine SeqSelStory StaffData SeqStaffRoll MusicStill SeqMusic MenuOption SeqDownload MyStoreObserver Tutorial FontMan VerificationController Reachability 


Judging by the name, this may be something in SeqStory , SelStoryChap or SeqSelStory . And in the second grade we are lucky.
Methods dump
  jtool -d SelStoryChap -arch armv7s oks
 Warning: companion file ./oks.ARM (unknown) .69981636-7F33-3C43-BD58-7F5BBE2A6CCA not found
 // Dumping class 45 (SelStoryChap)
 @interface SelStoryChap: CoreFoundation :: _ OBJC_METACLASS _ $ _ NSObject
 // No properties ..
 // 11 instance variables
  / * 0 * / unsigned int flag;  // I
  / * 1 * / int storyId;  // i
  / * 2 * / int prio;  // i
  / * 3 * / float oriPosx;  // f
  / * 4 * / float oriPosy;  // f
  / * 5 * / float posx;  // f
  / * 6 * / float posy;  // f
  / * 7 * / float plate_w_2;  // f
  / * 8 * / float plate_h_2;  // f
  / * 9 * / sprAry;  // ^ @
  / * 10 * / int sprNum;  // i
 // 25 instance methods
  / * 0 * / 0x38f01 - isUnlock;  // Protocol c8 @ 0: 4
  / * 1 * / 0x38f15 - isHave;  // Protocol c8 @ 0: 4
  / * 2 * / 0x38f29 - canPlay;  // Protocol c8 @ 0: 4
  / * 3 * / 0x38f3d - canSelect;  // Protocol c8 @ 0: 4
  / * 4 * / 0x38f81 - isTouch;  // Protocol c8 @ 0: 4
  / * 5 * / 0x38fd5 - plateTye;  // Protocol i8 @ 0: 4
  / * 6 * / 0x3900d - isKeyDisp;  // Protocol c8 @ 0: 4
  / * 7 * / 0x39031 - isPlayingDisp;  // Protocol c8 @ 0: 4
  / * 8 * / 0x39045 - isChapTitleDisp;  // Protocol c8 @ 0: 4
  / * 9 * / 0x39085 - alpha;  // Protocol f8 @ 0: 4
  / * 10 * / 0x390d9 - isDisp;  // Protocol c8 @ 0: 4
  / * 11 * / 0x39135 - clear;  // Protocol v8 @ 0: 4
  / * 12 * / 0x391d5 - reset;  // Protocol v8 @ 0: 4
  / * 13 * / 0x392a5 - load;  // Protocol v8 @ 0: 4
  / * 14 * / 0x396d1 - initWithPrio :;  // Protocol @ 12 @ 0: 4i8
  / * 15 * / 0x39725 - dealloc;  // Protocol v8 @ 0: 4
  / * 16 * / 0x397a1 - setStoryId :;  // Protocol v12 @ 0: 4i8
  / * 17 * / 0x39859 - updatePos;  // Protocol v8 @ 0: 4
  / * 18 * / 0x3991d - setOriPos: y :;  // Protocol v16 @ 0: 4f8f12
  / * 19 * / 0x3994d - setOfstPos: y :;  // Protocol v16 @ 0: 4f8f12
  / * 20 * / 0x3996d - setDisp :;  // Protocol v12 @ 0: 4c8
  / * 21 * / 0x39aa1 - startSelEff;  // Protocol v8 @ 0: 4
  / * 22 * ​​/ 0x39c7d - storyId;  // Protocol i8 @ 0: 4
  / * 23 * / 0x39c8d - posx;  // Protocol f8 @ 0: 4
  / * 24 * / 0x39c9d - posy;  // Protocol f8 @ 0: 4
 @end


The isUnlock / isHave member methods unobtrusively hint: you need to look there. A literate person in my place would write a Flex patch or compile a short library for Mobile Cydia Substrate. But as the most usual inadequate theos, I did not install, but I do not use Flex. It was possible to write the usual dynamic library using the standard API for method swizzling, but then it did not occur to me, and then it became clear that it would not help. We load the file into IDA, go to the methods, and replace the contents with the mov mov r0, # 1: bx lr .
Like this




After updating the executable file on the device in appearance, I guessed that isUnlock is “is available for the game,” and “isHave” is the purchase state. I open the previously closed episode, I understand that I got up on a rake:


We'll have to see what's inside. Since arm is not the most familiar to me architecture, I will not turn to assembly code without the need, especially since the functions are short and I don’t want to spend a lot of time. Look at the code isHave / isUnlock :

char __cdecl -[SelStoryChap isHave](struct SelStoryChap *self, SEL a2) { return (self->flag >> 1) & 1; } char __cdecl -[SelStoryChap isUnlock](struct SelStoryChap *self, SEL a2) { return (self->flag >> 2) & 1; } 

Yeah, these methods only read the given value, probably the real test somewhere else. According to the XREF flag, we find a method that writes to the flag (hereinafter, the names are partially added manually in order to facilitate readability):
 // SelStoryChap - (void)setStoryId:(int) void __cdecl -[SelStoryChap setStoryId:](struct SelStoryChap *self, SEL a2, int story_id) { self->storyId = story_id; self->flag &= 0xFFFFFF80; if ( checkStoryFlag1(self->storyId) ) self->flag |= 1u; if ( checkStoryFlag2(self->storyId) ) //  isHave self->flag |= 2u; if ( checkStoryFlag4(self->storyId) ) //  isUnlock self->flag |= 4u; ... } 

Knowing that we need to check the 2nd bit, look at the contents of checkStoryFlag2 and adjust as needed.
A little more detail
 int __fastcall checkStoryFlag2(int a1) { return checkStoryStatus(a1, dword_7D84C); } signed int __fastcall inRange(int value, int start, int end) { signed int result; // r0@1 result = 0; if ( start <= value && value <= end ) result = 1; return result; } signed int __cdecl checkStoryStatus(int story_id, int *table) { signed int ret; // r4@1 ret = 0; if ( table ) { ret = 0; if ( inRange(story_id, 0, 63) ) { ret = 0; if ( sub_xxxx(global_entry1, story_id, table, 's') ) { if ( !memcmp(global_entry1, &table[8 * story_id + 6], 0x20u) ) ret = 1; } } } return ret; } 


It seems that some value table is stored in dword_7D84C (later I found out that there are SHA-256 hashes) with which the newly calculated for the current id is checked. When a coincidence chapter opens. I think this is the place to remove the check and not to think. The unconditional return of the unit did its job and I went through all 16 chapters :).

Jamb fix number 2


At this point, the story could be completed, if not one but:


And where are the remaining 24 tracks? That's right, buy separately. Here I began to dislike the toy a little, but nothing, the previous steps were too easy, there must be something else. However, I was lucky again. Soon, inside one of the methods, helpfully named setupSelMusic, code was found that iterates over the tracks and calls some function: D
  v3 = 0; memset(self->ctrl_music_idx, 0, 0x200u); v1 = 0; self->music_max = 0; do { if ( sub_36C24(v1) ) self->ctrl_music_idx[v3++] = v1; ++v1; } while ( v1 != 128 ); self->music_max = v3; 

those.
  i = 0; memset(self->ctrl_music_idx, 0, 0x200u); track_id = 0; self->music_max = 0; do { if ( trackCheckingFunction(track_id) ) self->ctrl_music_idx[i++] = track_id; ++track_id; } while ( track_id != 128 ); self->music_max = i; 


Based on the context of the checks, I interpreted the function as a set of four confirmations: the track is within the allowable range (0 ~ 127), the track exists in the base of the game, the track is acquired and at least one of the levels is available for the game.
Pseudocode
 signed int __fastcall trackCheckingFunction(int track) { signed int ret; // r5@1 int lvl; // r6@4 char open; // r0@6 ret = 0; if ( inRange(track, 0, 127) ) //   { ret = 0; if ( trackExists2(track) ) //   { ret = 0; if ( checkTrackStatus(track, dword_7D84C) ) //   { lvl = 1; do { ret = 0; if ( lvl > 4 ) break; open = checkTrackPassedLevel(track, lvl++); //   ret = 1; } while ( !open ); } } } return ret; } 

Making checkTrackStatus return the unit without conditions ... I got a bummer. The tracks were no longer displayed in the store as available for purchase, but they were not in the game menu either. Here for some time I broke my head, initially thinking that I was too low and that everything was about to be unlocked. However, the rationalist in me a little later remembered that in a toy, each track has up to 4 difficulty modes, each of which opens when receiving a rather high mark for the previous one. This means that the lvl variable in this function has nothing to do with player points, it simply defines the “openness” of at least one difficulty mode for passing. Further study of the code confirmed this.
Slightly more
 BOOL __fastcall isEntryAvailble(int *table, signed int index) { return (table[index >> 5] & (1 << (index & 0x1F))) != 0; } // -    ... signed int __fastcall checkTrackStatus(int track, int *table) { signed int ret; // r4@1 ret = 0; if ( table ) { ret = 0; if ( inRange(track, 0, 127) ) { ret = 0; if ( loadEntryHash(hash, track, table, 'm') ) { if ( !memcmp(hash, &table[8 * track + 522], 0x20u) ) ret = 1; } } } return ret; } BOOL __fastcall checkTrackPassedLevel(int track, int level) { BOOL ret; // r6@1 ret = 0; if ( inRange(track, 0, 127) ) { ret = 0; if ( inRange(level, 1, 4) ) { ret = 0; if ( checkTrackStatus(track, dword_7D84C) ) ret = isEntryAvailble(&track_status_list, level + 4 * track - 1); } } return ret; } 

Well, um, the diagnosis is made, and what shall we do? It was possible to simply patch checkTrackPassedLevel , but then all the tracks and difficulty modes, regardless of passing, will be available. This option seemed to me too rude, even for personal use, so the replication expert in me was helpful in looking for the initializer. There were no adequate XREFs on track_status_list and already wanted to take such an unloved debugger. At the last moment I had an idea: if the case rests on a generated hash in a certain table, then something must put it there, and where the hash is, there is status. It is unlikely that the developer would use two different functions (although everything about the copy-paste is already clear even from a couple of calculations in this message) for its calculation, and I looked at XREF loadEntryHash . I guess, literally after a few minutes of searching, a function was found with this content:

 int *__fastcall sub_xxxx(int track) { int *result; // r0@1 result = inRange(track, 0, 127); if ( result ) { performLoadHashForTrack(track); sub_36EC0(track, 1); result = dword_7D84C; unk_7D860[0] |= 1u; } return result; } 

In my opinion, and without renaming it is clear that this is a kind of opener, called after the purchase / passage of the track. In any case, these edinichki just shouted about it, and - [MyStoreObserver complete_sub:] above on XREFs agreed with me :) The technology case: insert the call of this function to some convenient place, perhaps, for the first time, an assembler was really useful to me The simplest id track check right at checkTrackStatus did its job, and everything became absolutely good.
Namely




Instead of conclusion


It is obvious that such minor changes hardly claim anything. I know that there are much more demanding and costly situations. Yes, even here, for example, it was possible to add the Russian language (text data is stored in a seemingly simple format with UTF-16 LE encoding, and graphic data is generally in PNG) or your own tracks with dynamic loading from the iTunes library. Moreover, in the case of the latter, in the game resources there were not only binary structures, but also source files for obtaining them.
An example of such a file
 /****************************************************************************** * wav-file : jupiter.mp3 * midi-file : jupiter.mid * create at 2012/8/17 20:54 ******************************************************************************/ //////////////// ヘッダ情報 //////////////// ST_CHDATA_HEAD s_chdata_head = { "OKCH", // 固定値"OKCH" 7 , // メジャーバージョン値(引き継ぎ不可更新) 1~ 0 , //マイナーバージョン値(引き継ぎ可更新) 0~ 294.40034f, // 曲尺 [秒] 625, // オブジェクト総数154.00015f, // 初期スクロールスピード[dot / sec] 154.00015f, // 初期BPM [beat / minutes] E_HAKU_2_4, // 初期拍子0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) 0, // (パディング用ダミー) }; //////////////// 本体データ //////////////// ST_CHOBJ_HAKU s_chdata_main_0000[] = { {E_CHOBJ_HAKU , 16 , 0.00000f , 0.00000f , E_HAKU_2_4}, }; ST_CHOBJ_BPM s_chdata_main_0001[] = { {E_CHOBJ_BPM , 20 , 0.00000f , 0.00000f , 120.00000f , 500000}, }; … 

It is possible, but each hobby has a limit both in time and in fantasy, and several hours spent were enough for me, which led to quite a comparable result without fanaticism. The purpose of this article is to discourage people from being afraid of mobile platforms, which, although they have many limitations, well, we will not dynamically write bytes in TEXT, and they are not particularly different from the big brothers. Feel free to delve into someone else's code, it's not so difficult, and sometimes quite fascinating (although, unlike the recent series of articles about NFS, this is hardly felt in my text).

PS Thanks for getting to the end.
PPS I think distributions should not be laid out for legal reasons, and why, for someone this is a great opportunity to practice.

Disclaimer: This article in no way calls for violations of license agreements, hacking or unfair use of software. Its text is presented solely for educational and informational purposes.

Source: https://habr.com/ru/post/303164/


All Articles