The Xiaohongshu 小红书 REDnote 小红书国际版 "Backdoor"

Jan 16, 2025

8 mins read

The popular social media app “TikTok” is likely facing an iminent ban in the United States in the coming days. This has resulted in a mass migration to the Chinese app 小红书 (meaning “little red book”), Xiaohongshu, or simply “REDnote”.

If you haven’t been made aware of this event and the chaotic hilarity that has ensued, I encourage you to take a peek at the news. Much of it is heart warming.

Among the chaos, a tweet went viral for noting the presence of the word “backdoor” within the REDnote iOS app, a term typically referring to a covert method of bypassing normal operations of a system with a strong connotation of malintent.

Quickly checking the words or strings within a piece of code is a completely valid method of initial analysis. But let’s do the rest of the analysis.

Side Note: I’m _mattata on Twitter and remyhax on Bluesky, you should give me a follow. I do stuff like this often.

Reproducing

In the tweet above, the command strings discover | grep -i "backdoor" is run. I have the latest version 8.69 iOS app, obtained independently of the original author of the tweet. Let’s get caught up.

$ strings discover | grep -i "backdoor"
ImBackdoor
ios_live_trtc_backdoor_support
host_cover_upload_report_backdoor
XYLiveImBackdoor
kXYIM_CUSTOM_LINKMIC_BACKDOOR
kXYIM_CUSTOM_BACKDOOR_PARAM_UPDATE
kXYIM_CUSTOM_BACKDOOR_HOT_SWITCH
kXYIM_CUSTOM_BACKDOOR_AUTO_LINK
kXYIM_CUSTOM_COVER_BACKDOOR
kXYIM_CUSTOM_BACKDOOR_RECORD
kXYIM_CUSTOM_BACKDOOR_FACEV_UPDATE
kBackdoorParamTypeVideoTrtc
kBackdoorParamTypeAudioQualityTrtc
kBackdoorParamTypeExpApiTrtc
ios_live_im_backdoor
backdoor_type
backdoor_params
Backdoor
T@"NSDictionary",C,N,V_kvoBackdoorAutolink
T@"NSDictionary",C,N,V_kvoBackdoorParamUpdate
kXYIM_CUSTOM_BACKDOOR_AUTO_LINK
T@"NSDictionary",C,N,V_kvoBackdoorFaceV
_kvoBackdoorAutolink
_kvoBackdoorFaceV
_kvoBackdoorParamUpdate
backdoor_registerIm
kXYIM_CUSTOM_BACKDOOR_FACEV_UPDATE
kXYIM_CUSTOM_BACKDOOR_HOT_SWITCH
kXYIM_CUSTOM_BACKDOOR_PARAM_UPDATE
kXYIM_CUSTOM_BACKDOOR_RECORD
kXYIM_CUSTOM_COVER_BACKDOOR
kXYIM_CUSTOM_LINKMIC_BACKDOOR
kvoBackdoorAutolink
kvoBackdoorFaceV
kvoBackdoorParamUpdate
registerBackdoorAPIIM
setKvoBackdoorAutolink:
setKvoBackdoorFaceV:
setKvoBackdoorParamUpdate:
startBackdoorIfNeeded:
trtcBackdoorSupport

Sure enough, the word “backdoor” appears in the REDnote iOS app.

Deviating

I also happen to have the exact same version (8.69) of the REDnote android app. Does the backdoor string appear there as well?

$ grep -r -i "backdoor"
grep: classes17.dex: binary file matches
grep: classes13.dex: binary file matches
grep: classes25.dex: binary file matches
grep: classes16.dex: binary file matches
grep: classes14.dex: binary file matches
grep: classes8.dex: binary file matches

It does!

Going forward

I tried to analyze the ~300mb ELF file from iOS in Binary Ninja and it crashed with Out-Of-Memory which is… anomalous, but not noteworthy.

I tried to analyze the Android app in Ghidra and it hemmoraged with errors. Which is anomalous, but also probably not noteworthy for “backdoor” reasons.

I’ve spent the past year building an analysis pipeline for an absolutely massive amount of android apps. This is a testcase for me, not a moral panic alarm.

So, we swap some things up and analyze the specific DEX files independent of the entire app to see if we can find an easy foothold to climb from.

Foothold

Okay, actually I got tired of fighting Out-Of-Memory and Out-Of-Time analysis problems.

  • You’ll need upwards of 64GB for Binary Ninja for iOS.
  • You’ll need upward of 6 hours of analysis for Ghidra iOS.
  • Ghidra will fail to analyze the entire app for Android
  • An incomplete analysis is annoying af for an application this size.
  • Jadx is more of an abstraction, will take upwards of 50GB of RAM, but did complete.

It’s after midnight 1AM and I’ve got kids too.

i got kids

The “Backdoor”

The “backdoor” appears to be a proxy (not a network proxy) that allows loading plugins (backdoors) to enable different features. They are defined in com.xingin.petal.pluginmanager in the Android app.

package com.xingin.petal.pluginmanager.entity;

import androidx.annotation.Keep;
import i78.c;
@Keep
@c
/* loaded from: classes13.dex */
public class PluginConstant {
    public static final PluginConstant INSTANCE = new PluginConstant();
    public static final int ONLINE = 1;
    public static final String PLUGIN_BACKDOOR_INFO_FILE_PATH = "assets/plugin/info.txt";
    public static final int PLUGIN_BACKDOOR_VERSIONCODE = 1000000;
    public static final String PLUGIN_FRAME_INIT_RESULT = "PLUGIN_FRAME_INIT_RESULT";
    public static final long PLUGIN_FRAME_LEAST_SPACE = 1048576;
    public static final String PLUGIN_IS_BUILT_IN = "pluginIsBuiltIn";
    public static final String PLUGIN_NAME = "pluginName";
    public static final String PLUGIN_VERSION_CODE = "pluginVersionCode";
    public static final int TEST = 2;

    private PluginConstant() {
    }
}

These plugins are remotely loaded, checked for compatibility, ABI, etc… Overall a functionality not too dissimilar to that would commonly be used by dynamically loaded malware. However, like many things, it is also a very common benign design pattern. There’s simple explanations for this behavior. If you’re running a small Android phone with limited or out-of-date hardware, it’s unlikely that some of Xiaohongshu’s compute heavy AI plugins would play nicely, for example.

Thankfully, for every “backdoor” plugin remotely installed and registered in the REDnote app, it is entered into a SQLite database an interested analyst could look at.

import com.xingin.robust.PatchProxy;
import com.xingin.robust.PatchProxyResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/* loaded from: classes13.dex */
public class PetalDao_Impl implements PetalDao {
    private final RoomDatabase __db;
    private final EntityDeletionOrUpdateAdapter<PetalPluginPatchRecordEntity> __deletionAdapterOfPetalPluginPatchRecordEntity;
    private final EntityDeletionOrUpdateAdapter<PetalPluginRecordEntity> __deletionAdapterOfPetalPluginRecordEntity;
    private final EntityInsertionAdapter<PetalActionRecordEntity> __insertionAdapterOfPetalActionRecordEntity;
    private final EntityInsertionAdapter<PetalPluginPatchRecordEntity> __insertionAdapterOfPetalPluginPatchRecordEntity;
    private final EntityInsertionAdapter<PetalPluginRecordEntity> __insertionAdapterOfPetalPluginRecordEntity;
    private final SharedSQLiteStatement __preparedStmtOfDeleteAllActionRecords;
    private final SharedSQLiteStatement __preparedStmtOfDeleteAllLocalRecord;
    private final SharedSQLiteStatement __preparedStmtOfDeleteAllPatchRecords;
    private final SharedSQLiteStatement __preparedStmtOfDeleteExpiredActionRecords;
    private final SharedSQLiteStatement __preparedStmtOfDeleteOfflinePluginInfo;
    private final SharedSQLiteStatement __preparedStmtOfDeletePatchInfoOfPlugin;
    private final EntityDeletionOrUpdateAdapter<PetalPluginRecordEntity> __updateAdapterOfPetalPluginRecordEntity;

    public PetalDao_Impl(RoomDatabase roomDatabase) {
        this.__db = roomDatabase;
        this.__insertionAdapterOfPetalPluginRecordEntity = new EntityInsertionAdapter<PetalPluginRecordEntity>(roomDatabase) { // from class: com.xingin.petal.pluginmanager.repo.PetalDao_Impl.1
            @Override // androidx.room.SharedSQLiteStatement
            public String createQuery() {
                PatchProxyResult proxy0Para = PatchProxy.proxy0Para(this, AnonymousClass1.class, 143708);
                return proxy0Para.isSupported ? (String) proxy0Para.result : "INSERT OR REPLACE INTO `petal_plugin_record` (`PLUGIN_ID`,`PLUGIN_NAME`,`PLUGIN_VERSION`,`PLUGIN_VERSION_CODE`,`PLUGIN_MIN_HOST_VERSION`,`PLUGIN_MAX_HOST_VERSION`,`PLUGIN_STATUS`,`PLUGIN_PRIORITY`,`PLUGIN_AUTO_INSTALL`,`PLUGIN_DOWNLOAD_URL`,`DLD_URLS`,`PLUGIN_SIZE`,`PLUGIN_MD5`,`PLUGIN_ABI`,`PLUGIN_IS_LOCAL`,`PLUGIN_DEPS`,`PLUGIN_DEPENDS`,`HOST_BASE_TYPE`,`IS_OFFLINE`,`PLUGIN_APK_DIR`,`PLUGIN_SO_DIR`,`PLUGIN_LAST_LOAD`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
            }

Additionally, all of these hooks appear to be observable without needing a rooted android phone and simply running adb logcat.

Summary

I didn’t actully end up analyzing the iOS app. I analyzed the Android app of the same version, at a slightly deeper level. This revealed a common design pattern for remotely loading modular units of code unfortunately and yet appropriately name “backdoor”, with no evidence discovered of any malintent. I did not evaluate any of the plugins themselves or whether there’s a REDnote “plugin store” where non-小红书 users can publish malicious plugins akin to publishing a malicious npm package.

The existence of “backdoor” in the Xiaohongshu 小红书 REDnote app appears to be a problem in the connotation of the word itself among a global community, and nothing more.

Regardless: Seeing something, saying something, and prompting further analysis is always good.

The iOS follow

After analysis ran overnight, I circled back to the iOS app. The “backdoor” strings appear in a completely different feature here; in the view controller instead of plugins. However, this once again presents as a misunderstood connotation of “backdoor” as used by mandarin developers. This naming scheme does not appear to be unqiue to Xiaohongshu. As spotted by Amitai Cohen the use of “backdoor” appears in mandarin language documentation on Microsoft’s offical documentation, referring to the code common design pattern exactly as previously stated.

As it presents in the iOS app, it’s used in the View Controller to enable various different features in changed scope. Seen below, some of those features are related to payments. Which makes sense that when exchanging money you might want to change the app to a different, more secure scope.

The full XYLivePushDecorateViewModel class is provided below if you are keen to know what other benign "backdoors" feature injection may be enabled in your REDnote app.

struct XYLivePushDecorateViewModel
{

    bool _isClearScreen;
    bool _notifyViewToShowLike;
    bool _notifyViewPausedTimeout;
    bool _notifyViewToUpdateStopUI;
    bool _noUseShouldLeaveRoomCompletion;
    bool _notifyVCToDismiss;
    bool _notifyVCToDismissNotStopRTC;
    bool _notifyVCToDismissWithNoAnimation;
    bool _hasShowedRNCompletionPage;
    bool _dismissWithoutCallServer;
    bool _notifyVCToBreakOffPusher;
    bool _notifyVCToStopPusher;
    bool _notifyVCLiveTerminated;
    bool _notifyVCToShowLinkmicList;
    bool _notifyVCToPresentMorePanelVC;
    bool _notifyVCToPresentInteractPanelVC;
    bool _notifyVCToPresentSettingPanelVC;
    bool _notifyVCToPresentRedpacketGiftListVC;
    bool _notifyVCToPresentPKVC;
    bool _notifyVCToPresentDashboard;
    bool _notifyVCToPresentGroupChat;
    bool _isHostLiveSettingRedDotCleared;
    bool _isHostEffectRedDotCleared;
    bool _notifyAudienceSendGiftEnable;
    bool _notifyVCToPushHostCenterPage;
    bool _notifyVCToPushPublishForenoticePage;
    bool _notifiyIMAccountForcedOffline;
    bool _shouldPresentWebVC;
    bool _shouldStopPush;
    bool _worldNoticeShowing;
    bool _notifyViewToShowLinkMicButton;
    bool _notifyViewToShowLinkHostButton;
    bool _notifyViewToShowTopicSelectVC;
    bool _sendingPKTopicReuqest;
    bool _newHost;
    bool _notifyBottomViewRefreshLottie;
    bool _hasSentLike;
    bool _notifyVCLinkingHostQuit;
    bool _isRequestedCompletionInfo;
    struct XYLiveIMCallbackAdapter* _adapter;
    struct XYLiveRoomInfo* _currentRoomInfo;
    XYLivePushAnnounceViewModel* _announceVM;
    XYLiveShoppingRedPacketManager* _shoppingRedPacketManager;
    struct XYLiveLoginParam* _hostLoginParam;
    struct XYLiveUserInfo* _userInSelectedChatMsg;
    NSString* _chatMsgId;
    struct XYLiveUserInfo* _userInTappedOtherHost;
    struct XYLiveUserInfo* _userInTappedGiftBubbleView;
    struct XYLiveUserInfo* _userInHostCandidateListView;
    CGFloat _livePushStartTimeInterval;
    NSInteger _maxAudienceCountWhenTerminated;
    NSInteger _currentFloatingLikeIconCount;
    CGFloat _secondsToNotifyViewToStartRecordTimer;
    NSString* _warningExplainUrl;
    NSString* _warningTagId;
    NSString* _warningReason;
    NSString* _channelTabName;
    XYLiveAfterStopExtraInfo* _afterStopInfo;
    _TtC9XYLiveKit25XYLivePushCompletionModel* _roomCompletionModel;
    NSInteger _bizType;
    NSString* _extraInfo;
    XYLivePushPanelItemTypeRefContainer* _panelItemsRef;
    NSInteger _applyLinkmicCount;
    NSString* _notifyVCToPushHostCenterPageLink;
    struct XYIMCommonGuideCMInfo* _guideCMInfo;
    struct XYLiveGiftModel* _stickerGift;
    NSString* _notifyVCToPerformActionLink;
    id _getService;
    id _getRegion;
    struct XYLiveRoomImpressionRefreshInfo* _roomImpressionInfo;
    NSString* _commentImageTextURL;
    struct XYLiveGoodsTopRankInfo* _goodsRankInfo;
    NSString* _notifyVCToPresentSilentWebURL;
    NSString* _notifyVCToShowSilentWebURL;
    struct XYLiveGoodsRecordingViewModel* _recordingViewModel;
    struct XYIMWorldNoticeConfigInfo* _worldNoticeInfo;
    struct XYLiveWeakObject* _weakObject;
    CGFloat _rainGiftRemainMessageID;
    NSInteger _networkQualityScore;
    XYLiveCardHandlerManager* _cardHandlerManager;
    XYLiveCardShowStrategy* _cardShowStrategy;
    _TtC12XYLivePusher20StreamQualityChecker* _streamQualityChecker;
    struct XYLiveCardContainerViewModel* _cardContainerViewModel;
    struct XYLiveLinkmicCardContainerViewModel* _linkmicCardContainerViewModel;
    struct XYIMMessageParser* _msgParser;
    CGFloat _timeIntervalDidEnterBackground;
    struct XYLiveFansClubViewModel* _fansViewModel;
    XYLiveLikeResourceCenter* _likeResourceCenter;
    NSString* _isSpecHost;
    NSDictionary* _kvoBackdoorParamUpdate;
    NSDictionary* _kvoBackdoorAutolink;
    _TtC17XYLivePlayManager10PipManager* _pipManager;
    NSDictionary* _kvoBackdoorFaceV;
};

Summary 2

The “backdoors” are a nothing burger.

Sharing is caring!