Reversing a Messaging App on iOS (Jailbreak-Free): Red Packets, Message Revoke, and Step Count
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Reversing a Messaging App on iOS (Jailbreak-Free): Red Packets, Message Revoke, and Step Count
Environment
| Environment | Version |
|---|---|
| OS | macOS Mojave 10.14.6 |
| Device | iPhone 6, iOS 12 |
| theos on macOS | latest |
| Xcode | 11.3.1 |
| MonkeyDev | — |
The result
Step count:

Message revoke prevention:

Auto red-packet grabbing:

Decrypting the app
Decrypt with CrackerXI+, or manually with dumpdecrypted. I used app version 8.0.0.

Export the decrypted ipa with scp or a file manager.

MonkeyDev
MonkeyDev integrates into Xcode and lets you develop hooking code quickly, link it into a Mach-O, and install a modified ipa without a jailbreak.
Create a new MonkeyDev project:

Drag the decrypted ipa into the project’s TargetApp directory, then run it for on-device debugging.
Open the app’s settings page and use Xcode’s Debug View Hierarchy to inspect the view tree.

Add a WCTableViewManager cell:
%hook NewSettingViewController
- (void)reloadTableData{
%orig;
WCTableViewManager *tableViewMgr = MSHookIvar<id>(self, "m_tableViewMgr");
MMTableView *tableView = [tableViewMgr getTableView];
WCTableViewNormalCellManager *newCell = [%c(WCTableViewNormalCellManager) normalCellForSel:@selector(setting) target:self title:@"Tweaks"];
[((WCTableViewSectionManager*)tableViewMgr.sections[0]) addCell: newCell];
[tableView reloadData];
}
%new
- (void)setting {
UIViewController *vc = [[MyTweakSettingController alloc] init];
[((UIViewController *)self).navigationController PushViewController:vc animated:true];
}
%end
Add the toggles for auto red-packet, revoke prevention, and step-count modification:
WCTableViewCellManager *autoEnvelopCell = [MyTweak switchCellWithSel:@selector(autoEnvelopSwitchChange:) target:self title:@"Auto grab red packets" switchOn:[MyTweakConfig autoRedEnvelop]];
[tweakSection addCell:autoEnvelopCell];
WCTableViewCellManager *revokeIntercept = [MyTweak switchCellWithSel:@selector(revokeIntercept:) target:self title:@"Prevent message revoke" switchOn:[MyTweakConfig preventRevoke]];
[tweakSection addCell:revokeIntercept];
WCTableViewCellManager *changeStepsCell = [MyTweak switchCellWithSel:@selector(changedSteps:) target:self title:@"Modify step count" switchOn:[MyTweakConfig changeSteps]];
[tweakSection addCell:changeStepsCell];

Hook the red-packet message to auto-grab:
BOOL (^shouldReceiveRedEnvelop)() = ^BOOL() {
if (!MyTweakConfig.autoRedEnvelop) { return NO; }
if (isGroupInBlackList()) { return NO; }
if (isContaintKeyWords()) { return NO; }
return isGroupReceiver() ||
(isGroupSender() && isReceiveSelfRedEnvelop()) ||
(!isGroupReceiver() && MyTweakConfig.personalRedEnvelopEnable);
};
NSDictionary *(^parseNativeUrl)(NSString *nativeUrl) = ^(NSString *nativeUrl) {
nativeUrl = [nativeUrl substringFromIndex:[@"wxpay://c2cbizmessagehandler/hongbao/receivehongbao?" length]];
return [%c(WCBizUtil) dictionaryWithDecodedComponets:nativeUrl separator:@"&"];
};
Revoke prevention:
%hook CMessageMgr
- (void)onRevokeMsg:(CMessageWrap *)arg1 {
if (MyTweakConfig.preventRevoke) {
NSString *msgContent = arg1.m_nsContent;
NSString *(^parseParam)(NSString *, NSString *,NSString *) = ^NSString *(NSString *content, NSString *paramBegin,NSString *paramEnd) {
NSUInteger startIndex = [content rangeOfString:paramBegin].location + paramBegin.length;
NSUInteger endIndex = [content rangeOfString:paramEnd].location;
NSRange range = NSMakeRange(startIndex, endIndex - startIndex);
return [content substringWithRange:range];
};
NSString *session = parseParam(msgContent, @"<session>", @"</session>");
NSString *newmsgid = parseParam(msgContent, @"<newmsgid>", @"</newmsgid>");
NSString *fromUsrName = parseParam(msgContent, @"<![CDATA[", @"撤回了一条消息");
CMessageWrap *revokemsg = [self GetMsg:session n64SvrID:[newmsgid integerValue]];
CContactMgr *contactMgr = [[objc_getClass("MMServiceCenter") defaultCenter] getService:objc_getClass("CContactMgr")];
CContact *selfContact = [contactMgr getSelfContact];
NSString *newMsgContent = @"";
if ([revokemsg.m_nsFromUsr isEqualToString:selfContact.m_nsUsrName]) {
if (revokemsg.m_uiMessageType == 1) { // Check if it's a text message
newMsgContent = [NSString stringWithFormat:@"Intercepted a message you recalled:\n %@",revokemsg.m_nsContent];
} else {
newMsgContent = @"Intercepted a recalled message";
}
} else {
if (revokemsg.m_uiMessageType == 1) {
newMsgContent = [NSString stringWithFormat:@"Intercepted a recalled message from %@:\n %@",fromUsrName, revokemsg.m_nsContent];
} else {
newMsgContent = [NSString stringWithFormat:@"Intercepted a recalled message from %@",fromUsrName];
}
}
CMessageWrap *newWrap = ({
CMessageWrap *msg = [[%c(CMessageWrap) alloc] initWithMsgType:0x2710];
[msg setM_nsFromUsr:revokemsg.m_nsFromUsr];
[msg setM_nsToUsr:revokemsg.m_nsToUsr];
[msg setM_uiStatus:0x4];
[msg setM_nsContent:newMsgContent];
[msg setM_uiCreateTime:[arg1 m_uiCreateTime]];
msg;
});
[self AddLocalMsg:session MsgWrap:newWrap fixTime:0x1 NewMsgArriveNotify:0x0];
return;
}
%orig;
}
%end
Step-count modification:
%hook WCDeviceStepObject
-(NSInteger)m7StepCount {
NSInteger stepCount = %orig;
NSInteger newStepCount = MyTweakConfig.changedSteps;
return MyTweakConfig.changeSteps ? newStepCount : stepCount;
}
-(NSInteger)hkStepCount {
NSInteger stepCount = %orig;
NSInteger newStepCount = MyTweakConfig.changedSteps;
return MyTweakConfig.changeSteps ? newStepCount : stepCount;
}
%end
Self-sign and package to run multiple copies — works even better with AltDeploy + AltStore.

Thanks
MonkeyDev.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch