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