GitHub avatar

Fox's Blog

Cape Mod: how to steal Jeb_'s cape with RSA signature injection

A Fabric mod that exploits a logical flaw in Minecraft's trust system: a valid Mojang RSA signature but replayed on a wrong account. Code explanation, security implications, and cryptographic lessons.

Cape Mod: how to steal Jeb_'s cape with RSA signature injection

alt text What if I told you that all it takes is a single valid RSA signature -- but for the wrong account -- to make your friends believe you're wearing the official Mojang cape? Welcome to cape-mod, a Fabric exploit that shows how Minecraft trusts a signature without verifying that the profile it belongs to is actually yours.

The context: how Minecraft handles skins and capes

In Java Edition, there is a question we don't often ask: who is responsible for displaying a player's skin and cape -- the client or the server?

The answer is nuanced:

Component Who sends it? Who downloads it?
Skin texture The server sends the signed URL The client downloads from textures.minecraft.net
Cape texture The server sends the signed URL The client downloads from textures.minecraft.net
textures property The server sends the GameProfile from Mojang auth The client verifies the RSA signature

The key point: everything is contained in a property called textures of the GameProfile. This property contains:

  • A base64 JSON payload with the texture URLs
  • An RSA signature made with Mojang's private key

The RSA signature wall

Each textures property looks like this when decoded:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

The client verifies the RSA signature against the public key embedded in the jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

For remote players (not local), the client only accepts skins marked as secure -- that is, with a valid signature:

// SkinManager.createLookup() -- simplified
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← remote players must be secured
    .orElse(defaultSkin);

This check prevents spoofing in theory. But that's where things get interesting.

The flaw: signature replay

The client checks that the RSA signature is valid. But it never checks whether the profileId inside the JSON matches the player's actual UUID.

In other words: a textures property taken from an existing Mojang account (for example, a Mojang employee's) can be replayed onto any other player. The signature remains valid -- it was genuinely made by Mojang -- it just came from a different account.

How to extract a genuine signature?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) has the Mojang Studios cape. From the Mojang session server:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Response:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

The signature of this value field was produced by Mojang. It's RSA-2048 SHA-1. It is absolutely valid, even if you replay it on another UUID -- because Jeb_'s signature remains Jeb_'s signature, and the client never checks that it's supposed to be yours.

The code: how the mod works

The cape-mod mod is tiny -- 65 lines of Java. Here is the core:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Replace the textures property with Jeb_'s
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Steps:

  1. Mixin on Player.getGameProfile() -- the point where the player's profile is returned
  2. Check that this is a local server (Integrated Server)
  3. Check that this is the host (LAN world)
  4. Replace the textures property with Jeb_'s (hardcoded)
  5. Return a new GameProfile with the injected textures

The GameProfile is therefore forged: it is an artificially constructed profile that does not correspond to the real player. The textures properties are replayed from Jeb_ -- the RSA signature is authentic but applied to the wrong profile. The network packet, however, is legitimate: the server normally sends the ClientboundPlayerInfoUpdatePacket with this modified profile. It is the profile that is forged, not the packet.

When the host's friends join via LAN, they receive the ClientboundPlayerInfoUpdatePacket with the modified profile. The client:

  1. Decodes the base64 payload
  2. Verifies the RSA signature -- ✅ valid (it's genuinely Jeb_'s)
  3. Marks the skin as secure=true (because the signature is valid)
  4. Passes the !isRemote || ps.secure() filter -- ✅ passes
  5. Downloads and displays Jeb_'s cape

In-game result: the cape on your skin

Here's what it looks like in-game. First, a front view with Jeb_'s cape displayed on the host:

Cape Mod -- Jeb_ cape displayed on the host

You can clearly see the red/white pattern of the official Mojang Studios cape. No difference from a real Jeb_ who has his own cape -- the client downloads exactly the same texture from textures.minecraft.net.

And in immersive view, in an actual game:

Cape Mod -- in-game view with cape visible

The cape floats behind the player, waving with movement. Perfectly indistinguishable from an authentic skin with an official cape.

Another angle, in a world with lava and terrain:

Cape Mod -- cape in a natural environment

And one last close-up view of actual gameplay, showing the cape in action:

Cape Mod -- cape in classic Minecraft gameplay

For someone joining a LAN without knowing the host has a mod, there is absolutely no way to distinguish this from a real Mojang cape. That's precisely the point: the signature is valid, the client has no reason to doubt.

Why this is a flaw (and why it isn't)

It's ironic: the exploit works precisely because the signature is valid. There is no cryptographic bypass here -- it's worse, it's a logical flaw in the trust model.

Check Result
RSA signature validity ✅ Valid (signed by Mojang for Jeb_)
Does the profileId in the payload match the host's UUID? ❌ No (Jeb_'s UUID != host's UUID)
Does the client verify the match? ❌ No. Only the RSA signature is verified.

Minecraft trusts the signature, not the identity of who carries it. As long as the signature comes from Mojang, the client accepts it. It's like showing a fake passport signed by the government -- the seal is legitimate, even though the passport doesn't belong to you.

Security implications

Limited scope to LAN

The mod only works on an integrated server (LAN). The attacker must:

  • Have a Fabric mod installed
  • Be the host of a LAN world
  • Friends connect without a mod (vanilla)

But the possibilities go further

In theory, with the same technique, one could:

  • Reinject other signed data: heads, illegal enchantments, malicious chat components
  • Combine with a LAN tunnel (NGROK, playit.gg, Radmin VPN) to affect players over the internet
  • Extend to other profile properties that rely on signatures

Why Mojang probably won't patch this

There is no "vulnerability" in the strict sense -- the signature is valid. Patching this would require Mojang to change the entire authentication model, which is complex. For now, it's an edge case: LAN players are assumed to trust each other.

The philosophical trap

Cape Mod is an excellent proof of concept of a broader truth: you must never trust a signature without verifying who signed it and for what subject.

It's a lesson in basic cryptography. RSA signs a message, not an identity. If I give you a valid RSA signature from Mojang, you know that Mojang has signed something. You don't know for whom, and you can't assume it just by looking at the message.

This is exactly what happened with SSL/TLS certificates in the 2000s when CAs would sign anything -- the signature was valid, but it applied to the wrong domain.

Conclusion

Cape Mod is not a hack in the classic sense -- it is an elegant exploitation of a missing validation check in Minecraft. It shows that:

  1. A valid signature does not guarantee the identity of the person carrying it
  2. On LAN, trust is weaker than we believe
  3. Minecraft's textures properties are essentially injected content -- they must be checked to match the player who carries them

If you join a LAN world on an "unknown" server (or rather, one whose host has a suspicious mod), you already have a security problem well before the cape. But it's symptomatic: Minecraft assumes everyone on a LAN trusts each other. That's true... until it isn't.


Resources

3 key points

  1. RSA signatures validate a message, not an identity -- a detail that has cost many systems dearly.
  2. Minecraft does not check that the player's profile matches the signature it receives -- a logical flaw, not a cryptographic one.
  3. On LAN or through a tunnel, anything goes for a mod that controls the integrated server.

Cape Mod : comment voler la cape de Jeb_ avec une injection de signature RSA

Un mod Fabric qui exploite une faille logique dans le système de confiance de Minecraft : une signature RSA valide de Mojang mais replayée sur un mauvais compte. Explication du code, implications de sécurité et leçons cryptographiques.

Cape Mod : comment voler la cape de Jeb_ avec une injection de signature RSA

alt text

Et si je te disais qu'il suffisait d'une signature RSA valide -- mais pour le mauvais compte -- pour faire croire à tes amis que tu portes la cape officielle de Mojang ? Bienvenue dans cape-mod, un exploit Fabric qui montre comment Minecraft fait confiance à une signature sans vérifier que le profil auquel elle appartient est effectivement le tien.

Le contexte : comment Minecraft gère les skins et les capes

Dans Java Edition, il y a une question qu'on ne se pose pas souvent : qui est responsable d'afficher le skin et la cape d'un joueur -- le client ou le serveur ?

La réponse est nuancée :

Composant Qui l'envoie ? Qui le télécharge ?
Texture de skin Le serveur envoie l'URL signée Le client télécharge depuis textures.minecraft.net
Texture de cape Le serveur envoie l'URL signée Le client télécharge depuis textures.minecraft.net
Propriété textures Le serveur envoie le GameProfile depuis l'auth Mojang Le client vérifie la signature RSA

Le point clé : tout est contenu dans une propriété appelée textures du GameProfile. Cette propriété contient :

  • Un payload JSON en base64 avec les URLs des textures
  • Une signature RSA faite avec la clé privée de Mojang

Le mur de la signature RSA

Chaque propriété textures ressemble à ça quand on la décode :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Le client vérifie la signature RSA contre la clé publique embarquée dans le jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Pour les joueurs distants (pas en local), le client n'accepte que les skins marqués comme secure -- c'est-à-dire avec une signature valide :

// SkinManager.createLookup() -- simplifié
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← les joueurs distants doivent être sécurisés
    .orElse(defaultSkin);

Ce check empêche les spoofing en théorie. Mais c'est là que les choses deviennent intéressantes.

La faille : signature replay

Le client vérifie que la signature RSA est valide. Mais il ne vérifie jamais que le profileId contenu dans le JSON correspond à l'UUID réel du joueur.

Autrement dit : une propriété textures prise d'un compte Mojang existant (par exemple celui d'un employé Mojang) peut être replay sur n'importe quel autre joueur. La signature reste valide -- elle a été genuinement faite par Mojang -- elle vient juste d'un autre compte.

Comment extraire une vraie signature ?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) a la cape Mojang Studios. Depuis le serveur de session Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Réponse :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

La signature de ce champ value a été produite par Mojang. C'est RSA-2048 SHA-1. Elle est absolument valide, même si tu la replays sur un autre UUID -- parce que la signature de Jeb_ reste une signature de Jeb_, et le client ne vérifie jamais que c'est censée être la tienne.

Le code : comment le mod fonctionne

Le mod cape-mod est minuscule -- 65 lignes de Java. Voici le cœur :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Remplace la propriété textures par celle de Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Étapes :

  1. Mixin sur Player.getGameProfile() -- le point où le profil du joueur est retourné
  2. Vérifie que c'est un serveur local (Integrated Server)
  3. Vérifie que c'est le host (LAN world)
  4. Remplace la propriété textures par celle de Jeb_ (hardcodée)
  5. Retourne un nouveau GameProfile avec les textures injectées

Le GameProfile est donc forgé : c'est un profil construit artificiellement, qui ne correspond pas au vrai joueur. Les propriétés textures sont replayées depuis Jeb_ -- la signature RSA est authentique mais appliquée au mauvais profil. Le paquet réseau, lui, est légitime : le serveur envoie normalement le ClientboundPlayerInfoUpdatePacket avec ce profil modifié. C'est le profil qui est forgé, pas le paquet.

Quand les amis du host rejoignent via LAN, ils reçoivent le ClientboundPlayerInfoUpdatePacket avec le profil modifié. Le client :

  1. Décode le payload base64
  2. Vérifie la signature RSA → ✅ valide (c'est genuinement celle de Jeb_)
  3. Marque le skin comme secure=true (car la signature est valide)
  4. Passe le filtre !isRemote || ps.secure() → ✅ passe
  5. Télécharge et affiche la cape de Jeb_

Résultat en jeu : la cape sur ton skin

Voici ce que ça donne in-game. D'abord, vue de face avec la cape de Jeb_ affichée sur le host :

Cape Mod -- Jeb_ cape affichée sur le host

On voit clairement le motif rouge/blanc de la cape officielle Mojang Studios. Aucune différence avec un vrai Jeb_ qui aurait sa propre cape -- le client télécharge exactement la même texture depuis textures.minecraft.net.

Et en vue immersive, dans une vraie partie :

Cape Mod -- vue en jeu avec cape visible

La cape flotte derrière le joueur, ondule avec le mouvement. Parfaitement indistinguible d'un skin authentique avec cape officielle.

Autre angle, dans un monde avec lave et terrain :

Cape Mod -- cape dans un environnement naturel

Et une dernier vue rapprochée du gameplay réel, où on voit la cape en action :

Cape Mod -- cape en gameplay classique Minecraft

Pour quelqu'un qui rejoindrait un LAN sans savoir que le host a un mod, il n'y a absolument aucun moyen de distinguer ça d'une vraie cape Mojang. C'est précisément le point : la signature est valide, le client n'a aucune raison de douter.

Pourquoi c'est une faille (et pourquoi ce n'en est pas une)

C'est ironique : l'exploit fonctionne précisément parce que la signature est valide. Il n'y a pas de bypass cryptographique ici -- c'est pire, c'est une faille logique dans le modèle de confiance.

Check Résultat
Validité de la signature RSA ✅ Valide (signée par Mojang pour Jeb_)
Le profileId dans le payload correspond-il à l'UUID du host ? ❌ Non (Jeb_'s UUID ≠ UUID du host)
Le client vérifie-t-il la correspondance ? ❌ Non. Seule la signature RSA est vérifiée.

Minecraft fait confiance à la signature, pas à l'identité de celui qui la porte. Tant que la signature vient de Mojang, le client l'accepte. C'est comme montrer un faux passeport signé par le gouvernement -- le sceau est légitime, même si le passeport ne te appartient pas.

Les implications de sécurité

Portée limitée au LAN

Le mod ne fonctionne que sur un serveur intégré (LAN). L'attaquant doit :

  • Avoir un mod Fabric installé
  • Être le host d'un monde LAN
  • Ses amis se connectent sans mod (vanilla)

Mais les possibilités s'élargissent

En théorie, avec la même technique, on pourrait :

  • Réinjecter d'autres données signées : heads, enchantements illégaux, composants de chat malveillants
  • Combiner avec un tunnel LAN (NGROK, playit.gg, Radmin VPN) pour affecter des joueurs sur internet
  • Étendre à d'autres propriétés du profil qui dépendent de signatures

Pourquoi Mojang ne va probablement pas patcher

Il n'y a pas de "vulnérabilité" au sens strict -- la signature est valide. Patcher ça demanderait à Mojang de modifier le modèle d'authentification complet, ce qui est complexe. Pour l'instant, c'est un edge case : les joueurs LAN sont supposés se faire confiance.

Le piège philosophique

Cape Mod est un excellent proof of concept d'une vérité plus large : tu ne dois jamais faire confiance à une signature sans vérifier qui l'a signée et à quel sujet.

C'est une leçon en cryptographie de base. RSA signe un message, pas une identité. Si je te donne une signature RSA valide de Mojang, tu sais que Mojang a signé quelque chose. Tu ne sais pas pour qui, et tu ne peux pas le supposer juste en regardant le message.

C'est exactement ce qui s'est passé avec les certificats SSL/TLS dans les années 2000 quand les CAs acceptaient n'importe quoi -- la signature était valide, mais elle s'appliquait au mauvais domaine.

Conclusion

Cape Mod n'est pas un hack au sens classique -- c'est une exploitation élégante d'un manque de validation logique dans Minecraft. Il montre que :

  1. Une signature valide ne garantit pas l'identité de celui qui la porte
  2. En LAN, la confiance est plus faible qu'on ne le croit
  3. Les propriétés textures de Minecraft sont essentiellement du contenu injecté -- il faut vérifier qu'elles correspondent au joueur qui les porte

Si tu rejoins un monde LAN sur un serveur "inconnu" (ou plutôt, dont le host a un mod suspect), tu as déjà un problème de sécurité bien avant la cape. Mais c'est symptomal : Minecraft suppose que tout le monde sur un LAN se fait confiance. C'est vrai... jusqu'à ce qu'ça ne le soit plus.


Ressources

3 points clés

  1. Les signatures RSA valident un message, pas une identité -- un détail qui a coûté cher à de nombreux systèmes.
  2. Minecraft ne vérifie pas que le profil du joueur correspond à la signature qu'il reçoit -- une faille logique, pas cryptographique.
  3. En LAN ou en tunnel, tout est open bar pour un mod qui contrôle le serveur intégré.

Cape Mod:如何通过 RSA 签名注入窃取 Jeb_ 的披风

一个 Fabric Mod,利用 Minecraft 信任系统中的逻辑漏洞:将 Mojang 的有效 RSA 签名重放到错误的账户上。代码解析、安全影响及加密学教训。

Cape Mod:如何通过 RSA 签名注入窃取 Jeb_ 的披风

alt text 如果我告诉你,只需要一个有效的 RSA 签名----但属于错误的账户----就能让你的朋友相信你穿着 Mojang 的官方披风?欢迎来到 cape-mod,一个 Fabric 漏洞利用模组,展示 Minecraft 如何信任签名而不验证该签名所属的配置文件是否确实是你的。

背景:Minecraft 如何处理皮肤和披风

在 Java Edition 中,有一个我们通常不会问的问题:谁负责显示玩家的皮肤和披风----客户端还是服务器?

答案是微妙的:

组件 谁发送? 谁下载?
皮肤纹理 服务器发送签名 URL 客户端从 textures.minecraft.net 下载
披风纹理 服务器发送签名 URL 客户端从 textures.minecraft.net 下载
textures 属性 服务器从 Mojang 认证服务发送 GameProfile 客户端验证 RSA 签名

关键点:一切都在 GameProfile 的一个名为 textures 的属性中。该属性包含:

  • 一个 base64 编码的 JSON payload,包含纹理 URL
  • 一个使用 Mojang 私钥生成的 RSA 签名

RSA 签名之墙

每个 textures 属性解码后看起来像这样:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

客户端根据 JAR 中内置的公钥(yggdrasil_session_pubkey.der)验证 RSA 签名:

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

对于远程玩家(非本地),客户端只接受标记为 secure 的皮肤----即有有效签名的皮肤:

// SkinManager.createLookup() -- 简化
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← 远程玩家必须安全
    .orElse(defaultSkin);

这个检查在理论上防止了欺骗。但接下来的事情就变得有趣了。

漏洞:签名重放

客户端验证 RSA 签名是否有效。但客户端从不检查 JSON 中的 profileId 是否与玩家的实际 UUID 匹配。

换句话说:从一个现有的 Mojang 账户(例如 Mojang 员工的账户)获取的 textures 属性可以重放到任意其他玩家身上。签名仍然有效----它确实是 Mojang 签署的----只是它来自另一个账户。

如何提取真实签名?

Jeb_(UUID 853c80ef-3c37-49fd-aa49-938b674adae6)拥有 Mojang Studios 披风。从 Mojang 会话服务器获取:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

响应:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

这个 value 字段的 signature 是由 Mojang 生成的。它是 RSA-2048 SHA-1 签名。即使你将其重放到另一个 UUID 上,它也绝对有效----因为 Jeb_ 的签名始终是 Jeb_ 的签名,而客户端从不验证它本应是你的签名。

代码:模组如何工作

cape-mod 模组很小----只有 65 行 Java 代码。核心如下:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // 将 textures 属性替换为 Jeb_ 的
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

步骤:

  1. Mixin Player.getGameProfile()----返回玩家配置文件的方法
  2. 检查是否为本地服务器(Integrated Server)
  3. 检查是否为主机(LAN 世界)
  4. 替换 textures 属性为 Jeb_ 的(硬编码)
  5. 返回一个新的 GameProfile,其中包含注入的纹理

GameProfile 因此是伪造的:这是一个人工构建的配置文件,与实际玩家不符。textures 属性是从 Jeb_ 重放的----RSA 签名是真实的,但应用到了错误的配置文件上。网络数据包本身是合法的:服务器正常发送 ClientboundPlayerInfoUpdatePacket,其中包含这个被修改过的配置文件。被伪造的是配置文件,而不是数据包。

当主机的朋友通过 LAN 加入时,他们会收到包含修改后配置文件的 ClientboundPlayerInfoUpdatePacket。客户端:

  1. 解码 base64 payload
  2. 验证 RSA 签名 → ✅ 有效(确实是 Jeb_ 的)
  3. 将皮肤标记为 secure=true(因为签名有效)
  4. 通过 !isRemote || ps.secure() 过滤器 → ✅ 通过
  5. 下载并显示 Jeb_ 的披风

游戏中的效果:你的皮肤上的披风

以下是在游戏中的效果。首先,正面视图,主机显示 Jeb_ 的披风:

Cape Mod -- Jeb_ 的披风显示在主机上

可以清晰地看到官方 Mojang Studios 披风的红白图案。与真正的 Jeb_ 拥有自己的披风没有任何区别----客户端从 textures.minecraft.net 下载完全相同的纹理。

沉浸式视角,在真实游戏中:

Cape Mod -- 游戏中披风可见

披风在玩家身后飘动,随动作摆动。与带有官方披风的真实皮肤完全无法区分。

另一个角度,在有岩浆和地形的世界中:

Cape Mod -- 自然环境中的披风

最后一张游戏玩法的近距离视图,展示披风在动作中的效果:

Cape Mod -- 经典 Minecraft 游戏中的披风

对于一个不知道主机安装了模组的人来说,完全无法区分这与真正的 Mojang 披风。这正是关键所在:签名是有效的,客户端没有理由怀疑。

为什么这是一个漏洞(以及为什么又不是)

具有讽刺意味的是:这个漏洞之所以有效,恰恰是因为签名是有效的。这里没有加密绕过----更糟糕的是,这是一个信任模型中的逻辑漏洞。

检查项 结果
RSA 签名有效性 ✅ 有效(由 Mojang 为 Jeb_ 签署)
payload 中的 profileId 是否匹配主机 UUID? ❌ 不匹配(Jeb_ 的 UUID ≠ 主机的 UUID)
客户端是否检查匹配? ❌ 不检查。只验证 RSA 签名。

Minecraft 信任签名,而不是携带签名者的身份。只要签名来自 Mojang,客户端就接受。这就像出示一份由政府签署的假护照----印章是合法的,即使护照不属于你。

安全影响

范围限于 LAN

该模组仅能在集成服务器(LAN)上工作。攻击者必须:

  • 安装 Fabric 模组
  • 成为 LAN 世界的主机
  • 朋友使用原版客户端加入(无需模组)

但可能性不止于此

理论上,使用同样的技术,还可以:

  • 重放其他已签名数据:头颅、非法附魔、恶意聊天组件
  • 结合 LAN 隧道(NGROK、playit.gg、Radmin VPN)来影响互联网上的玩家
  • 扩展到配置文件的其他依赖签名的属性

为什么 Mojang 可能不会修复

严格来说,这不算"漏洞"----签名是有效的。要修复这个问题,Mojang 需要修改完整的认证模型,这非常复杂。目前,这只是一个边缘情况:LAN 玩家本应彼此信任。

哲学陷阱

Cape Mod 是一个绝佳的概念验证,揭示了一个更广泛的真理:永远不要在不验证签名者和签名对象的情况下信任签名。

这是基础密码学的一课。RSA 签署的是消息,而不是身份。如果我给你一个 Mojang 的有效 RSA 签名,你知道 Mojang 签署了某个东西。但你不知道是为谁签署的,也不能仅仅通过查看消息来假设。

这与 2000 年代 SSL/TLS 证书的情况完全一样----当时 CA 接受任何请求----签名有效,但它应用到了错误的域名上。

结论

Cape Mod 不是传统意义上的黑客攻击----它是对 Minecraft 中缺乏逻辑验证的优雅利用。它表明:

  1. 有效的签名并不保证携带者的身份
  2. 在 LAN 环境中,信任比想象中更脆弱
  3. Minecraft 的 textures 属性本质上是注入的内容----需要验证它们是否与携带它们的玩家匹配

如果你加入一个"陌生"LAN 世界(或者说,主机安装了可疑模组的世界),你在披风问题之前就已经有了安全问题。但这具有警示意义:Minecraft 假设 LAN 上的所有人都互相信任。这通常是成立的……直到不再成立。


资源

3 个关键点

  1. RSA 签名验证的是消息,而不是身份----这个细节曾让许多系统付出代价。
  2. Minecraft 不验证玩家配置文件是否与收到的签名匹配----这是一个逻辑漏洞,而非加密漏洞。
  3. 在 LAN 或隧道中,对于控制集成服务器的模组来说,一切皆可为之。

Cape Mod:RSA署名注入でJeb_のケープを奪う方法

Minecraftの信頼システムにおける論理的欠陥を悪用するFabric Mod:Mojangの正当なRSA署名を別のアカウントで使い回す。コードの解説、セキュリティへの影響、暗号の教訓。

Cape Mod:RSA署名注入でJeb_のケープを奪う方法

alt text もし、有効なRSA署名が1つあれば――ただし誤ったアカウントのものでも――友達にあなたがMojangの公式ケープを着けていると思い込ませられるとしたら? cape-modへようこそ。これは、Minecraftが署名を信頼する一方で、その署名が本当に自分のプロフィールに属するものかを検証しないことを利用したFabricのエクスプロイトです。

背景:Minecraftはスキンとケープをどう扱うか

Java Editionでは、あまり疑問に思われないことがあります:プレイヤーのスキンとケープを表示する責任はクライアントとサーバーのどちらにあるのか?

答えは微妙です:

コンポーネント 誰が送信するか? 誰がダウンロードするか?
スキンテクスチャ サーバーが署名付きURLを送信 クライアントが textures.minecraft.net からダウンロード
ケープテクスチャ サーバーが署名付きURLを送信 クライアントが textures.minecraft.net からダウンロード
textures プロパティ サーバーがMojang認証からの GameProfile を送信 クライアントがRSA署名を検証

重要なポイント:すべては GameProfile の textures と呼ばれるプロパティに含まれています。このプロパティには以下が含まれます:

  • テクスチャのURLを含むbase64エンコードされたJSONペイロード
  • Mojangの秘密鍵で作成されたRSA署名

RSA署名の壁

デコードすると、各 textures プロパティは次のようになります:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

クライアントはjarに埋め込まれた公開鍵(yggdrasil_session_pubkey.der)に対してRSA署名を検証します:

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

リモートプレイヤー(ローカル以外)の場合、クライアントは**secureとマークされた**スキン――つまり有効な署名のあるもの――だけを受け入れます:

// SkinManager.createLookup() -- 簡略化
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← リモートプレイヤーはセキュアである必要がある
    .orElse(defaultSkin);

このチェックは理論上スプーフィングを防ぎます。しかし、ここからが面白くなるところです。

脆弱性:署名のリプレイ

クライアントはRSA署名が有効かどうかを検証します。しかし、JSONに含まれる profileId が実際のプレイヤーのUUIDと一致するかどうかは決して検証しません。

言い換えれば:既存のMojangアカウント(例えばMojang社員のもの)から取得した textures プロパティを、他のどのプレイヤーにでもリプレイできます。署名は有効なままです――Mojangによって正しく作られたものです――ただ別のアカウントからのものにすぎません。

実際の署名を抽出する方法

Jeb_(UUID 853c80ef-3c37-49fd-aa49-938b674adae6)はMojang Studiosのケープを持っています。Mojangのセッションサーバーから:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

レスポンス:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

この value フィールドの signature はMojangによって生成されました。RSA-2048 SHA-1です。別のUUIDでリプレイしても、それは完全に有効です――なぜならJeb_の署名はJeb_の署名であり続け、クライアントはそれがあなたのものであるべきかを決して検証しないからです。

コード:Modの仕組み

cape-mod は非常に小さい――65行のJavaです。核心部分はこちら:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // texturesプロパティをJeb_のものに置き換える
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

手順:

  1. Player.getGameProfile() にMixin――プレイヤーのプロフィールが返されるポイント
  2. ローカルサーバー(Integrated Server)であることを確認
  3. host(LANワールド)であることを確認
  4. textures プロパティをJeb_のものに置換(ハードコード)
  5. 注入されたテクスチャで新しい GameProfile を返す

GameProfile は偽造されています:人為的に構築されたプロフィールで、実際のプレイヤーとは一致しません。textures プロパティはJeb_からリプレイされています――RSA署名は本物ですが、誤ったプロフィールに適用されています。ネットワークパケット自体は正当です:サーバーは通常通り、この変更されたプロフィールを含む ClientboundPlayerInfoUpdatePacket を送信します。偽造されているのはパケットではなくプロフィールです。

hostの友達がLAN経由で接続すると、変更されたプロフィールを含む ClientboundPlayerInfoUpdatePacket を受信します。クライアントは:

  1. base64ペイロードをデコード
  2. RSA署名を検証 → ✅ 有効(Jeb_のもので正真正銘)
  3. 署名が有効なためスキンを secure=true とマーク
  4. !isRemote || ps.secure() フィルターを通過 → ✅ 通過
  5. Jeb_のケープをダウンロードして表示

ゲーム内の結果:あなたのスキンにケープが

ゲーム内での見え方はこちらです。まず、正面から見たhostに表示されたJeb_のケープ:

Cape Mod -- Jeb_ cape affichée sur le host

公式Mojang Studiosケープの赤と白の模様がはっきりと見えます。自分のケープを持っている本物のJeb_と何ら変わりません――クライアントは textures.minecraft.net からまったく同じテクスチャをダウンロードします。

実際のゲームプレイでの没入感のあるビュー:

Cape Mod -- vue en jeu avec cape visible

ケープはプレイヤーの後ろでなびき、動きに合わせて揺れます。公式ケープ付きの本物のスキンと完全に見分けがつきません。

別の角度から、溶岩と地形のあるワールドで:

Cape Mod -- cape dans un environnement naturel

そして実際のゲームプレイの最後の接写、ケープが動作している様子:

Cape Mod -- cape en gameplay classique Minecraft

hostがModを入れていることを知らずにLANに参加した人には、本物のMojangケープと区別する方法はまったくありません。それがまさにポイントです:署名は有効であり、クライアントが疑う理由は何もありません。

なぜこれが脆弱性なのか(そしてなぜ脆弱性でないのか)

皮肉なことに:このエクスプロイトは署名が有効であるからこそ機能します。暗号のバイパスがあるわけではありません――もっと悪質で、これは信頼モデルにおける論理的欠陥です。

チェック 結果
RSA署名の有効性 ✅ 有効(MojangがJeb_に対して署名)
ペイロード内の profileId はhostのUUIDと一致するか? ❌ いいえ(Jeb_のUUID ≠ hostのUUID)
クライアントはその一致を検証するか? ❌ しない。RSA署名のみが検証される。

Minecraftは署名を信頼し、それを保持する者の身元を信頼しません。署名がMojangからのものである限り、クライアントはそれを受け入れます。これは、政府が署名した偽のパスポートを示すようなものです――印鑑は合法でも、パスポートはあなたのものではありません。

セキュリティへの影響

LANに限定された範囲

このModは統合サーバー(LAN)でのみ機能します。攻撃者は以下が必要です:

  • Fabric Modがインストールされていること
  • LANワールドのhostであること
  • 友達がModなし(バニラ)で接続すること

しかし可能性は広がる

理論上、同じ技術で以下のことが可能です:

  • 他の署名付きデータの再注入:ヘッド、不正なエンチャント、悪意のあるチャットコンポーネント
  • LANトンネルとの組み合わせ(NGROK、playit.gg、Radmin VPN)でインターネット上のプレイヤーに影響を与える
  • 署名に依存するプロフィールの他のプロパティに拡張

Mojangがおそらくパッチを当てない理由

厳密な意味での「脆弱性」はありません――署名は有効です。これを修正するには、Mojangが認証モデル全体を変更する必要があり、それは複雑です。現時点ではエッジケースです:LANプレイヤーはお互いを信頼しているという前提があります。

哲学的な罠

Cape Modは、より広い真理の優れた概念実証です:署名が誰によって、何に対して行われたかを検証せずに、署名を決して信頼してはならない。

これは基本的な暗号の教訓です。RSAはメッセージに署名し、身元に署名するわけではありません。もし私があなたにMojangの有効なRSA署名を渡したら、あなたはMojangが何かに署名したことを知ります。それが誰のためのものかはわかりませんし、メッセージを見ただけでそれを推測することはできません。

これは2000年代にCAが何でも受け入れていた時のSSL/TLS証明書で起きたこととまったく同じです――署名は有効でしたが、それが誤ったドメインに適用されていました。

結論

Cape Modは従来の意味でのハックではありません――Minecraftにおける論理的検証の欠如をエレガントに悪用したものです。これが示すこと:

  1. 有効な署名は、それを保持する者の身元を保証しない
  2. LANでは、信頼は思われているよりも弱い
  3. Minecraftの textures プロパティは本質的に注入されたコンテンツである――それを保持するプレイヤーと一致することを検証する必要がある

もし「未知の」(というより、hostが疑わしいModを入れている)LANワールドに参加するなら、ケープ以前にすでにセキュリティ問題があります。しかしこれは象徴的です:MinecraftはLAN上の全員がお互いを信頼していると想定しています。それは真実です...そうでなくなるまでは。


リソース

3つの重要ポイント

  1. RSA署名はメッセージを検証するものであり、身元を検証するものではない――この詳細は多くのシステムに大きな代償をもたらしてきた。
  2. Minecraftは受け取った署名がプレイヤーのプロフィールと一致するかを検証しない――暗号ではなく論理の脆弱性である。
  3. LANでもトンネルでも、統合サーバーを制御するModにとってはすべてが自由行為である。

Cape Mod: RSA 서명 주입으로 Jeb_의 망토를 훔치는 방법

Fabric 모드로, Mojang의 유효한 RSA 서명을 다른 계정에 재사용하는 Minecraft 신뢰 시스템의 논리적 허점을 파헤친다. 코드 설명, 보안 영향, 암호학적 교훈.

Cape Mod: RSA 서명 주입으로 Jeb_의 망토를 훔치는 방법

alt text 유효한 RSA 서명 하나면 -- 비록 잘못된 계정의 것이라도 -- 친구들에게 내가 공식 Mojang 망토를 착용하고 있는 것처럼 속일 수 있다면? cape-mod에 오신 것을 환영합니다. 이 Fabric 익스플로잇은 Minecraft가 서명 자체는 신뢰하면서 정작 그 서명이 속한 프로필이 실제로 자신의 것인지는 확인하지 않는 방식을 보여줍니다.

배경: Minecraft가 스킨과 망토를 처리하는 방식

Java Edition에서 우리가 자주 묻지 않는 질문이 하나 있습니다: 플레이어의 스킨과 망토를 렌더링할 책임은 누구에게 있는가 -- 클라이언트인가 서버인가?

정답은 미묘합니다:

구성요소 누가 보내는가? 누가 다운로드하는가?
스킨 텍스처 서버가 서명된 URL을 전송 클라이언트가 textures.minecraft.net에서 다운로드
망토 텍스처 서버가 서명된 URL을 전송 클라이언트가 textures.minecraft.net에서 다운로드
textures 속성 서버가 Mojang 인증의 GameProfile을 전송 클라이언트가 RSA 서명을 검증

핵심은 모든 것이 GameProfile의 textures라는 속성 안에 담겨 있다는 점입니다. 이 속성은 다음을 포함합니다:

  • 텍스처 URL이 담긴 base64 JSON 페이로드
  • Mojang의 개인키로 생성된 RSA 서명

RSA 서명의 벽

디코딩하면 각 textures 속성은 다음과 같은 형태입니다:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

클라이언트는 jar에 내장된 공개키(yggdrasil_session_pubkey.der)로 RSA 서명을 검증합니다:

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

원격 플레이어(로컬이 아닌 경우)의 경우 클라이언트는 secure로 표시된 스킨만 허용합니다. 즉, 유효한 서명이 있어야 합니다:

// SkinManager.createLookup() -- 간략화
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← 원격 플레이어는 보안 스킨이어야 함
    .orElse(defaultSkin);

이 검증은 이론상 스푸핑을 막아줍니다. 하지만 여기서부터 흥미진진해집니다.

취약점: 서명 재사용 (Signature Replay)

클라이언트는 RSA 서명이 유효한지만 확인합니다. 하지만 JSON 안에 있는 profileId가 실제 플레이어의 UUID와 일치하는지는 절대 확인하지 않습니다.

다시 말해, 기존 Mojang 계정(예: Mojang 직원의 계정)에서 가져온 textures 속성을 아무 다른 플레이어에 재사용해도 됩니다. 서명은 여전히 유효합니다 -- Mojang이 진짜로 서명했으니까요 -- 단지 다른 계정의 것일 뿐입니다.

진짜 서명을 추출하는 방법?

Jeb_(UUID 853c80ef-3c37-49fd-aa49-938b674adae6)은 Mojang Studios 망토를 보유하고 있습니다. Mojang 세션 서버에서 가져옵니다:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

응답:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

이 value 필드의 signature는 Mojang이 생성한 것입니다. RSA-2048 SHA-1입니다. 다른 UUID에 재사용해도 절대적으로 유효합니다. Jeb_의 서명은 여전히 Jeb_의 서명이고, 클라이언트는 이 서명이 원래 당신 것인지 전혀 확인하지 않기 때문입니다.

코드: 모드의 작동 방식

cape-mod 모드는 아주 작습니다 -- Java 65줄. 핵심은 이렇습니다:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Jeb_의 textures 속성으로 교체
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

단계:

  1. Player.getGameProfile()에 Mixin -- 플레이어 프로필이 반환되는 지점
  2. 로컬 서버(Integrated Server)인지 확인
  3. LAN 세계의 host인지 확인
  4. textures 속성을 Jeb_의 것으로 교체(하드코딩됨)
  5. 주입된 텍스처가 포함된 새 GameProfile 반환

이렇게 GameProfile은 위조됩니다: 실제 플레이어와 일치하지 않는 인공적으로 구성된 프로필입니다. textures 속성은 Jeb_에서 재사용된 것입니다 -- RSA 서명은 진짜지만 잘못된 프로필에 적용되어 있습니다. 네트워크 패킷 자체는 정상입니다: 서버는 수정된 이 프로필로 ClientboundPlayerInfoUpdatePacket을 정상적으로 전송합니다. 위조된 것은 패킷이 아니라 프로필입니다.

host의 친구들이 LAN으로 접속하면 수정된 프로필이 담긴 ClientboundPlayerInfoUpdatePacket을 받습니다. 클라이언트는:

  1. base64 페이로드를 디코딩
  2. RSA 서명 검증 → ✅ 유효 (진짜 Jeb_의 서명)
  3. secure=true로 스킨 표시 (서명이 유효하므로)
  4. !isRemote || ps.secure() 필터 통과 → ✅ 통과
  5. Jeb_의 망토를 다운로드하여 표시

게임 내 결과: 내 스킨 위에 망토가

다음은 게임 내 모습입니다. 먼저, host에게 Jeb_의 망토가 표시된 정면 모습:

Cape Mod -- host에게 표시된 Jeb_ 망토

공식 Mojang Studios 망토의 빨간색/흰색 패턴이 선명하게 보입니다. 진짜 Jeb_가 자신의 망토를 보여주는 것과 전혀 다를 바 없습니다 -- 클라이언트는 textures.minecraft.net에서 정확히 동일한 텍스처를 다운로드하기 때문입니다.

실제 게임 플레이에서의 몰입형 모습:

Cape Mod -- 게임 내 망토가 보이는 모습

망토가 플레이어 뒤에서 펄럭이며 움직임에 따라 출렁입니다. 공식 망토가 있는 진짜 스킨과 완전히 구분할 수 없습니다.

용암과 지형이 있는 세계에서 다른 각도:

Cape Mod -- 자연 환경 속의 망토

그리고 실제 게임플레이의 마지막 근접 샷, 망토가 살아 움직이는 모습:

Cape Mod -- 일반 Minecraft 게임플레이 속 망토

host에 모드가 있는지 모르고 LAN에 접속하는 사람에게는 이것이 진짜 Mojang 망토와 전혀 구별할 방법이 없습니다. 이것이 바로 핵심입니다: 서명이 유효하므로, 클라이언트는 의심할 이유가 전혀 없습니다.

왜 이것이 취약점인가 (그리고 왜 아닐 수도 있는가)

아이러니하게도 이 익스플로잇은 서명이 유효하기 때문에 정확히 작동합니다. 여기에는 암호학적 우회가 없습니다 -- 더 심각하게, 이는 신뢰 모델의 논리적 결함입니다.

검증 항목 결과
RSA 서명의 유효성 ✅ 유효 (Mojang이 Jeb_을 위해 서명함)
페이로드의 profileId가 host의 UUID와 일치하는가? ❌ 아님 (Jeb_의 UUID ≠ host의 UUID)
클라이언트가 일치 여부를 확인하는가? ❌ 아니요. 오직 RSA 서명만 검증됩니다.

Minecraft는 서명 자체를 신뢰하지, 그것을 가진 사람의 신원을 신뢰하지 않습니다. 서명이 Mojang에서 왔다면 클라이언트는 그것을 수용합니다. 마치 정부가 서명한 가짜 여권을 보여주는 것과 같습니다 -- 도장은 진짜지만, 여권은 당신 것이 아닙니다.

보안 영향

LAN으로 제한된 범위

이 모드는 통합 서버(LAN)에서만 작동합니다. 공격자는 다음 조건을 충족해야 합니다:

  • Fabric 모드 설치
  • LAN 세계의 host
  • 친구들은 모드 없이(바닐라로) 접속

그러나 확장 가능성

이론적으로, 같은 기술으로 다음도 가능합니다:

  • 서명된 다른 데이터를 재주입: 헤드, 불법 마법 부여, 악성 채팅 컴포넌트
  • LAN 터널과 결합(NGROK, playit.gg, Radmin VPN)하여 인터넷 상의 플레이어에게 영향
  • 서명에 의존하는 프로필의 다른 속성으로 확장

Mojang이 아마 패치하지 않을 이유

엄밀한 의미의 "취약점"은 아닙니다 -- 서명은 유효하니까요. 이를 패치하려면 Mojang이 인증 모델 전체를 수정해야 하며, 이는 복잡한 작업입니다. 현재로서는 에지 케이스입니다: LAN 플레이어는 서로를 신뢰한다고 가정합니다.

철학적 함정

Cape Mod는 더 큰 진실에 대한 훌륭한 개념 증명입니다: 서명을 확인할 때 누가 서명했는지와 어떤 대상에 대해 서명했는지도 반드시 검증해야 합니다.

이것은 기초 암호학의 교훈입니다. RSA는 메시지에 서명하는 것이지, 신원에 서명하는 것이 아닙니다. 내가 당신에게 Mojang의 유효한 RSA 서명을 준다면, 당신은 Mojang이 무언가에 서명했다는 것을 압니다. 당신은 그것이 누구를 위한 것인지 알지 못하며, 메시지만 보고 추정할 수도 없습니다.

2000년대에 CA들이 아무거나 승인하던 SSL/TLS 인증서에서 정확히 같은 일이 일어났습니다 -- 서명은 유효했지만, 잘못된 도메인에 적용되어 있었습니다.

결론

Cape Mod는 고전적인 의미의 해킹이 아닙니다 -- 이는 Minecraft의 논리적 검증 부재를 우아하게 활용한 것입니다. 이것이 보여주는 것은:

  1. 유효한 서명이 서명을 가진 사람의 신원을 보장하지 않는다
  2. LAN에서는 우리가 생각하는 것보다 신뢰가 더 약하다
  3. Minecraft의 textures 속성은 본질적으로 주입된 콘텐츠다 -- 이것이 그것을 가진 플레이어와 일치하는지 확인해야 한다

"알 수 없는"(또는 host가 수상한 모드를 가진) LAN 세계에 접속한다면, 망토 문제를 떠나 이미 보안 문제가 있는 것입니다. 하지만 이는 증상에 불과합니다: Minecraft는 LAN 상의 모든 사람이 서로를 신뢰한다고 가정합니다. 그것은 사실입니다... 더 이상 아니게 될 때까지는.


자료

3가지 핵심 포인트

  1. RSA 서명은 메시지를 검증할 뿐 신원을 검증하지 않는다 -- 수많은 시스템에 큰 대가를 치르게 한 세부사항.
  2. Minecraft는 받은 서명이 플레이어의 프로필과 일치하는지 확인하지 않는다 -- 암호학적 결함이 아닌 논리적 결함.
  3. LAN이나 터널 환경에서는 통합 서버를 제어하는 모드에게 모든 것이 열려 있다.

Cape Mod : RSA imza enjeksiyonuyla Jeb_'nin capesini çalmak

Minecraft'ın güven sistemindeki mantıksal bir açığı sömüren bir Fabric modu: Mojang'dan geçerli bir RSA imzası ama yanlış hesaba replay edilmiş. Kod açıklaması, güvenlik etkileri ve kriptografik dersler.

Cape Mod : RSA imza enjeksiyonuyla Jeb_'nin capesini çalmak

alt text Sana geçerli bir RSA imzasının -- ama yanlış hesap için -- arkadaşlarını resmi Mojang capesi taktığına inandırmaya yettiğini söylesem? cape-mod'a hoş geldin, Minecraft'ın bir imzaya güvendiğini ama imzanın ait olduğu profilin gerçekten senin olup olmadığını kontrol etmediğini gösteren bir Fabric exploit'i.

Arka plan : Minecraft skin ve capeleri nasıl yönetiyor?

Java Edition'da sık sorulmayan bir soru var: Bir oyuncunun skin ve capesini görüntülemekten kim sorumlu -- istemci mi yoksa sunucu mu?

Cevap incelikli:

Bileşen Kim gönderiyor? Kim indiriyor?
Skin dokusu Sunucu imzalı URL'yi gönderir İstemci textures.minecraft.net'ten indirir
Cape dokusu Sunucu imzalı URL'yi gönderir İstemci textures.minecraft.net'ten indirir
textures özelliği Sunucu, Mojang auth'tan GameProfile'i gönderir İstemci RSA imzasını doğrular

Kilit nokta: her şey GameProfile'ın textures adlı bir özelliğinde bulunur. Bu özellik şunları içerir:

  • Doku URL'lerini içeren base64 JSON payload
  • Mojang'ın özel anahtarıyla yapılmış bir RSA imzası

RSA imza duvarı

Her textures özelliği decode edildiğinde şuna benzer:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

İstemci RSA imzasını jar'a gömülü ortak anahtara (yggdrasil_session_pubkey.der) karşı doğrular:

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Uzaktaki oyuncular (yerel değil) için istemci yalnızca secure olarak işaretlenmiş skinleri kabul eder -- yani geçerli bir imzaya sahip olanları:

// SkinManager.createLookup() -- basitleştirilmiş
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← uzaktaki oyuncular güvenli olmalı
    .orElse(defaultSkin);

Bu kontrol teoride spoofing'i engeller. Ama işte tam bu noktada işler ilginçleşiyor.

Açık : imza replay

İstemci RSA imzasının geçerli olup olmadığını kontrol eder. Ama JSON içindeki profileId'nin oyuncunun gerçek UUID'siyle eşleşip eşleşmediğini asla kontrol etmez.

Başka bir deyişle: var olan bir Mojang hesabından (örneğin bir Mojang çalışanının hesabı) alınan bir textures özelliği herhangi bir başka oyuncuya replay edilebilir. İmza geçerlidir -- Mojang tarafından gerçekten üretilmiştir -- sadece başka bir hesaptan gelmektedir.

Gerçek bir imza nasıl çıkarılır?

Jeb_'nin (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) Mojang Studios capesi var. Mojang oturum sunucusundan:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Yanıt:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

Bu value alanının signature'ı Mojang tarafından üretilmiştir. RSA-2048 SHA-1'dir. Bunu başka bir UUID'de replay etsen bile kesinlikle geçerlidir -- çünkü Jeb_'nin imzası Jeb_'nin imzası olarak kalır ve istemci bunun senin olması gerektiğini asla kontrol etmez.

Kod : mod nasıl çalışıyor?

cape-mod modu minicik -- 65 satır Java. İşte kalbi:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // textures özelliğini Jeb_ninkiyle değiştir
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Adımlar:

  1. Player.getGameProfile() üzerinde Mixin -- oyuncu profilinin döndürüldüğü nokta
  2. Bunun bir yerel sunucu (Integrated Server) olduğunu kontrol et
  3. Host (LAN dünyası) olduğunu kontrol et
  4. textures özelliğini Jeb_ninkiyle (hardcoded) değiştir
  5. Enjekte edilmiş dokularla yeni bir GameProfile döndür

GameProfile forged'dir: gerçek oyuncuya uymayan yapay olarak oluşturulmuş bir profildir. textures özellikleri Jeb_'den replay edilmiştir -- RSA imzası gerçektir ama yanlış profile uygulanmıştır. Ağ paketinin kendisi meşrudur: sunucu normalde ClientboundPlayerInfoUpdatePacket'i bu değiştirilmiş profille gönderir. Forged olan profil, paket değil.

Host'un arkadaşları LAN üzerinden katıldığında, değiştirilmiş profille ClientboundPlayerInfoUpdatePacket'i alırlar. İstemci:

  1. Base64 payload'u decode eder
  2. RSA imzasını doğrular → ✅ geçerli (gerçekten Jeb_nin imzası)
  3. Skin'i secure=true olarak işaretler (imza geçerli olduğu için)
  4. !isRemote || ps.secure() filtresinden geçer → ✅ geçer
  5. Jeb_nin capesini indirir ve görüntüler

Oyunda sonuç : skininde cape

Oyunda böyle görünüyor. Önce hostta Jeb_nin capesinin görüntülendiği önden görünüm:

Cape Mod -- Jeb_ capesi hostta görüntülenmiş

Resmi Mojang Studios capesinin kırmızı/beyaz deseni açıkça görülüyor. Kendi capesine sahip gerçek bir Jeb_'den hiçbir farkı yok -- istemci tamamen aynı dokuyu textures.minecraft.net'ten indiriyor.

Ve sürükleyici görünümde, gerçek bir oyunda:

Cape Mod -- cape görünür şekilde oyun içi görünüm

Cape oyuncunun arkasında dalgalanıyor, hareketle birlikte sallanıyor. Resmi capeli gerçek bir skin'den ayırt edilemez.

Başka bir açı, lav ve arazili bir dünyada:

Cape Mod -- doğal ortamda cape

Ve capenin aksiyonda görüldüğü gerçek oynanışa yakın bir çekim daha:

Cape Mod -- klasik Minecraft oynanışında cape

Hostta bir mod olduğunu bilmeden LAN'a katılan biri için bunu gerçek bir Mojang capesinden ayırt etmenin kesinlikle hiçbir yolu yok. İşte püf nokta tam da bu: imza geçerli, istemcinin şüphelenmesi için hiçbir neden yok.

Bu neden bir açık (ve neden değil)

İronik: exploit tam da imza geçerli olduğu için çalışıyor. Burada kriptografik bir bypass yok -- daha kötüsü, bu güven modelinde bir mantıksal açık.

Kontrol Sonuç
RSA imzasının geçerliliği ✅ Geçerli (Mojang tarafından Jeb_ için imzalanmış)
Payload'daki profileId host UUID'siyle eşleşiyor mu? ❌ Hayır (Jeb_'nin UUID'si ≠ host UUID'si)
İstemci eşleşmeyi kontrol ediyor mu? ❌ Hayır. Sadece RSA imzası kontrol ediliyor.

Minecraft imzaya güvenir, onu taşıyanın kimliğine değil. İmza Mojang'dan geldiği sürece istemci kabul eder. Bu, hükümet tarafından imzalanmış sahte bir pasaport göstermek gibi -- mühür geçerli, pasaport sana ait olmasa bile.

Güvenlik etkileri

LAN ile sınırlı kapsam

Mod yalnızca entegre sunucuda (LAN) çalışır. Saldırgan şunlara sahip olmalıdır:

  • Kurulu bir Fabric modu
  • Bir LAN dünyasının hostu olmak
  • Arkadaşları modsuz bağlanır (vanilla)

Ama olasılıklar genişliyor

Teoride, aynı teknikle şunlar yapılabilir:

  • Diğer imzalı verileri yeniden enjekte etmek: heads, yasadışı büyüler, kötü niyetli sohbet bileşenleri
  • Bir LAN tüneliyle (NGROK, playit.gg, Radmin VPN) birleştirerek internet üzerindeki oyuncuları etkilemek
  • Profille ilgili diğer imza bağımlı özelliklere genişletmek

Mojang neden muhtemelen yamamayacak

Kesin anlamda bir "güvenlik açığı" yok -- imza geçerli. Bunu düzeltmek Mojang'ın tüm kimlik doğrulama modelini değiştirmesini gerektirir ki bu karmaşıktır. Şimdilik bu bir edge case: LAN oyuncularının birbirine güvendiği varsayılır.

Felsefi tuzak

Cape Mod, daha geniş bir gerçeğin mükemmel bir proof of concept'idir: bir imzaya, onu kimin imzaladığını ve hangi konuda olduğunu kontrol etmeden asla güvenmemelisin.

Bu temel kriptografi dersidir. RSA bir mesajı imzalar, bir kimliği değil. Sana Mojang'dan geçerli bir RSA imzası verirsem, Mojang'ın bir şeyi imzaladığını bilirsin. Kimin için olduğunu bilmezsin ve sadece mesaja bakarak bunu varsayamazsın.

2000'lerde CA'ların her şeyi kabul ettiği dönemde SSL/TLS sertifikalarında olan tam olarak buydu -- imza geçerliydi ama yanlış alana uygulanıyordu.

Sonuç

Cape Mod klasik anlamda bir hack değil -- Minecraft'taki mantıksal doğrulama eksikliğinin zarif bir sömürüsüdür. Şunları gösterir:

  1. Geçerli bir imza, onu taşıyanın kimliğini garanti etmez
  2. LAN'da güven sandığımızdan daha zayıftır
  3. Minecraft'ın textures özellikleri aslında enjekte edilmiş içeriktir -- onları taşıyan oyuncuya ait oldukları doğrulanmalıdır

"Bilinmeyen" (ya da daha doğrusu hostunda şüpheli bir mod olan) bir LAN dünyasına katılırsan, capeden çok önce bir güvenlik sorunun var demektir. Ama bu semptomatik: Minecraft bir LAN'daki herkesin birbirine güvendiğini varsayar. Bu doğru... ta ki doğru olmaktan çıkana kadar.


Kaynaklar

3 kilit nokta

  1. RSA imzaları bir mesajı doğrular, bir kimliği değil -- birçok sisteme pahalıya patlamış bir detay.
  2. Minecraft, oyuncu profilinin aldığı imzayla eşleşip eşleşmediğini kontrol etmez -- kriptografik değil, mantıksal bir açık.
  3. LAN'da veya tünelde, entegre sunucuyu kontrol eden bir mod için her şey açıktır.

Cape Mod: come rubare il mantello di Jeb_ con un'iniezione di firma RSA

Un mod Fabric che sfrutta una falla logica nel sistema di fiducia di Minecraft: una firma RSA valida di Mojang ma riutilizzata su un account sbagliato. Spiegazione del codice, implicazioni di sicurezza e lezioni crittografiche.

Cape Mod: come rubare il mantello di Jeb_ con un'iniezione di firma RSA

alt text E se ti dicessi che basta una firma RSA valida -- ma per l'account sbagliato -- per far credere ai tuoi amici che indossi il mantello ufficiale di Mojang? Benvenuto in cape-mod, un exploit Fabric che mostra come Minecraft si fidi di una firma senza verificare che il profilo a cui appartiene sia effettivamente il tuo.

Il contesto: come Minecraft gestisce skin e mantelli

Nella Java Edition, c'è una domanda che non ci si pone spesso: chi è responsabile di mostrare la skin e il mantello di un giocatore -- il client o il server?

La risposta è sfumata:

Componente Chi lo invia? Chi lo scarica?
Texture della skin Il server invia l'URL firmato Il client scarica da textures.minecraft.net
Texture del mantello Il server invia l'URL firmato Il client scarica da textures.minecraft.net
Proprietà textures Il server invia il GameProfile dall'auth Mojang Il client verifica la firma RSA

Il punto chiave: tutto è contenuto in una proprietà chiamata textures del GameProfile. Questa proprietà contiene:

  • Un payload JSON in base64 con gli URL delle texture
  • Una firma RSA fatta con la chiave privata di Mojang

Il muro della firma RSA

Ogni proprietà textures assomiglia a questa una volta decodificata:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Il client verifica la firma RSA contro la chiave pubblica incorporata nel jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Per i giocatori remoti (non in locale), il client accetta solo le skin marcate come secure -- cioè con una firma valida:

// SkinManager.createLookup() -- semplificato
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← i giocatori remoti devono essere sicuri
    .orElse(defaultSkin);

Questo controllo impedisce lo spoofing in teoria. Ma è qui che le cose si fanno interessanti.

La falla: signature replay

Il client verifica che la firma RSA sia valida. Ma non verifica mai che il profileId contenuto nel JSON corrisponda all'UUID reale del giocatore.

In altre parole: una proprietà textures presa da un account Mojang esistente (per esempio quello di un dipendente Mojang) può essere riutilizzata su qualsiasi altro giocatore. La firma rimane valida -- è stata genuinamente creata da Mojang -- proviene solo da un altro account.

Come estrarre una firma vera?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) ha il mantello Mojang Studios. Dal server di sessione Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Risposta:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

La signature di questo campo value è stata prodotta da Mojang. È RSA-2048 SHA-1. È assolutamente valida, anche se la riutilizzi su un altro UUID -- perché la firma di Jeb_ rimane una firma di Jeb_, e il client non verifica mai che sia supposta essere la tua.

Il codice: come funziona il mod

Il mod cape-mod è minuscolo -- 65 righe di Java. Ecco il cuore:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Sostituisce la proprietà textures con quella di Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Fasi:

  1. Mixin su Player.getGameProfile() -- il punto in cui il profilo del giocatore viene restituito
  2. Verifica che sia un server locale (Integrated Server)
  3. Verifica che sia l'host (mondo LAN)
  4. Sostituisce la proprietà textures con quella di Jeb_ (hardcodata)
  5. Restituisce un nuovo GameProfile con le texture iniettate

Il GameProfile è quindi forgiato: è un profilo costruito artificialmente, che non corrisponde al vero giocatore. Le proprietà textures sono riutilizzate da Jeb_ -- la firma RSA è autentica ma applicata al profilo sbagliato. Il pacchetto di rete, invece, è legittimo: il server invia normalmente il ClientboundPlayerInfoUpdatePacket con questo profilo modificato. È il profilo che è forgiato, non il pacchetto.

Quando gli amici dell'host si connettono via LAN, ricevono il ClientboundPlayerInfoUpdatePacket con il profilo modificato. Il client:

  1. Decodifica il payload base64
  2. Verifica la firma RSA → ✅ valida (è genuinamente quella di Jeb_)
  3. Marca la skin come secure=true (perché la firma è valida)
  4. Supera il filtro !isRemote || ps.secure() → ✅ passa
  5. Scarica e mostra il mantello di Jeb_

Risultato in gioco: il mantello sulla tua skin

Ecco come appare in-game. Prima, vista frontale con il mantello di Jeb_ mostrato sull'host:

Cape Mod -- mantello di Jeb_ mostrato sull'host

Si vede chiaramente il motivo rosso/bianco del mantello ufficiale Mojang Studios. Nessuna differenza con un vero Jeb_ che avrebbe il suo mantello -- il client scarica esattamente la stessa texture da textures.minecraft.net.

E in visuale immersiva, in una vera partita:

Cape Mod -- vista in gioco con mantello visibile

Il mantello fluttua dietro il giocatore, ondeggia con il movimento. Perfettamente indistinguibile da una skin autentica con mantello ufficiale.

Un'altra angolazione, in un mondo con lava e terreno:

Cape Mod -- mantello in ambiente naturale

E un'ultima vista ravvicinata del gameplay reale, dove si vede il mantello in azione:

Cape Mod -- mantello in gameplay classico Minecraft

Per qualcuno che si unisse a un LAN senza sapere che l'host ha un mod, non c'è assolutamente modo di distinguere questo da un vero mantello Mojang. È proprio questo il punto: la firma è valida, il client non ha alcun motivo di dubitare.

Perché è una falla (e perché non lo è)

È ironico: l'exploit funziona proprio perché la firma è valida. Non c'è un bypass crittografico qui -- è peggio, è una falla logica nel modello di fiducia.

Controllo Risultato
Validità della firma RSA ✅ Valida (firmata da Mojang per Jeb_)
Il profileId nel payload corrisponde all'UUID dell'host? ❌ No (UUID di Jeb_ ≠ UUID dell'host)
Il client verifica la corrispondenza? ❌ No. Solo la firma RSA viene verificata.

Minecraft si fida della firma, non dell'identità di chi la porta. Finché la firma proviene da Mojang, il client l'accetta. È come mostrare un passaporto falso firmato dal governo -- il sigillo è legittimo, anche se il passaporto non ti appartiene.

Le implicazioni di sicurezza

Portata limitata al LAN

Il mod funziona solo su un server integrato (LAN). L'attaccante deve:

  • Avere un mod Fabric installato
  • Essere l'host di un mondo LAN
  • I suoi amici si connettono senza mod (vanilla)

Ma le possibilità si ampliano

In teoria, con la stessa tecnica, si potrebbe:

  • Reiniettare altri dati firmati: teste, incantesimi illegali, componenti di chat maliziose
  • Combinare con un tunnel LAN (NGROK, playit.gg, Radmin VPN) per colpire giocatori su internet
  • Estendere ad altre proprietà del profilo che dipendono da firme

Perché Mojang probabilmente non correggerà

Non c'è una "vulnerabilità" in senso stretto -- la firma è valida. Correggere questo richiederebbe a Mojang di modificare il modello di autenticazione completo, il che è complesso. Per ora, è un case limite: i giocatori LAN sono supposti fidarsi l'uno dell'altro.

Il tranello filosofico

Cape Mod è un eccellente proof of concept di una verità più ampia: non devi mai fidarti di una firma senza verificare chi l'ha firmata e a quale scopo.

È una lezione di crittografia di base. RSA firma un messaggio, non un'identità. Se ti do una firma RSA valida di Mojang, sai che Mojang ha firmato qualcosa. Non sai per chi, e non puoi presumerlo solo guardando il messaggio.

È esattamente ciò che è successo con i certificati SSL/TLS negli anni 2000 quando le CA accettavano qualsiasi cosa -- la firma era valida, ma si applicava al dominio sbagliato.

Conclusione

Cape Mod non è un hack in senso classico -- è uno sfruttamento elegante di una mancanza di validazione logica in Minecraft. Mostra che:

  1. Una firma valida non garantisce l'identità di chi la porta
  2. In LAN, la fiducia è più debole di quanto si creda
  3. Le proprietà textures di Minecraft sono essenzialmente contenuto iniettato -- bisogna verificare che corrispondano al giocatore che le porta

Se ti unisci a un mondo LAN su un server "sconosciuto" (o meglio, il cui host ha un mod sospetto), hai già un problema di sicurezza ben prima del mantello. Ma è sintomatico: Minecraft presume che tutti su un LAN si fidino l'uno dell'altro. È vero... finché non smette di esserlo.


Risorse

3 punti chiave

  1. Le firme RSA validano un messaggio, non un'identità -- un dettaglio costato caro a molti sistemi.
  2. Minecraft non verifica che il profilo del giocatore corrisponda alla firma che riceve -- una falla logica, non crittografica.
  3. In LAN o in tunnel, tutto è possibile per un mod che controlla il server integrato.

Cape Mod: Wie man Jeb_s Cape mit einer RSA-Signatur-Injektion stiehlt

Ein Fabric-Mod, der eine logische Schwachstelle im Vertrauenssystem von Minecraft ausnutzt: eine gültige RSA-Signatur von Mojang, die jedoch auf das falsche Konto replayiert wird. Code-Erklärung, Sicherheitsimplikationen und kryptografische Lektionen.

Cape Mod: Wie man Jeb_s Cape mit einer RSA-Signatur-Injektion stiehlt

alt text Was wäre, wenn ich dir sagte, dass eine gültige RSA-Signatur -- aber für den falschen Account -- völlig ausreicht, um deine Freunde glauben zu lassen, du trägst den offiziellen Mojang-Umhang? Willkommen bei cape-mod, einem Fabric-Exploit, der zeigt, wie Minecraft einer Signatur vertraut, ohne zu überprüfen, ob das Profil, zu dem sie gehört, tatsächlich deines ist.

Der Kontext: Wie Minecraft Skins und Capes verwaltet

In der Java Edition stellt sich eine Frage, die man sich nicht oft stellt: Wer ist dafür verantwortlich, den Skin und den Cape eines Spielers anzuzeigen -- der Client oder der Server?

Die Antwort ist nuanciert:

Komponente Wer sendet sie? Wer lädt sie herunter?
Skin-Textur Der Server sendet die signierte URL Der Client lädt von textures.minecraft.net
Cape-Textur Der Server sendet die signierte URL Der Client lädt von textures.minecraft.net
Eigenschaft textures Der Server sendet das GameProfile von der Mojang-Auth Der Client prüft die RSA-Signatur

Der entscheidende Punkt: Alles ist in einer Eigenschaft namens textures des GameProfile enthalten. Diese Eigenschaft enthält:

  • Ein Base64-JSON-Payload mit den URLs der Texturen
  • Eine RSA-Signatur, erstellt mit dem privaten Schlüssel von Mojang

Die RSA-Signatur-Mauer

Jede textures-Eigenschaft sieht nach dem Dekodieren so aus:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Der Client prüft die RSA-Signatur gegen den im Jar eingebetteten öffentlichen Schlüssel (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Für entfernte Spieler (nicht lokal) akzeptiert der Client nur Skins, die als secure markiert sind -- also mit einer gültigen Signatur:

// SkinManager.createLookup() -- vereinfacht
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← entfernte Spieler müssen sicher sein
    .orElse(defaultSkin);

Diese Prüfung verhindert theoretisch Spoofing. Aber hier wird es interessant.

Die Schwachstelle: Signatur-Replay

Der Client prüft, ob die RSA-Signatur gültig ist. Aber er prüft nie, ob die im JSON enthaltene profileId mit der tatsächlichen UUID des Spielers übereinstimmt.

Mit anderen Worten: Eine textures-Eigenschaft, die von einem bestehenden Mojang-Account (z. B. dem eines Mojang-Mitarbeiters) stammt, kann auf jeden anderen Spieler replayiert werden. Die Signatur bleibt gültig -- sie wurde echt von Mojang erstellt -- sie stammt nur von einem anderen Account.

Wie extrahiert man eine echte Signatur?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) hat den Mojang-Studios-Umhang. Vom Mojang-Session-Server:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Antwort:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

Die signature dieses value-Feldes wurde von Mojang erstellt. Es ist RSA-2048 SHA-1. Sie ist absolut gültig, selbst wenn du sie auf eine andere UUID replays -- denn Jeb_s Signatur bleibt Jeb_s Signatur, und der Client prüft nie, ob sie angeblich deine sein soll.

Der Code: Wie der Mod funktioniert

Der cape-mod ist winzig -- 65 Zeilen Java. Hier ist der Kern:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Ersetzt die textures-Eigenschaft durch die von Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Schritte:

  1. Mixin auf Player.getGameProfile() -- der Punkt, an dem das Spielerprofil zurückgegeben wird
  2. Prüft, ob es sich um einen lokalen Server handelt (Integrated Server)
  3. Prüft, ob es der Host ist (LAN-Welt)
  4. Ersetzt die textures-Eigenschaft durch die von Jeb_ (hartkodiert)
  5. Gibt ein neues GameProfile mit den injizierten Texturen zurück

Das GameProfile ist also gekünstelt: Es ist ein künstlich erstelltes Profil, das nicht dem echten Spieler entspricht. Die textures-Eigenschaften sind von Jeb_ replayed -- die RSA-Signatur ist authentisch, wird aber auf das falsche Profil angewendet. Das Netzwerkpaket selbst ist legitim: Der Server sendet normalerweise das ClientboundPlayerInfoUpdatePacket mit diesem modifizierten Profil. Es ist das Profil, das gekünstelt ist, nicht das Paket.

Wenn die Freunde des Hosts über LAN beitreten, erhalten sie das ClientboundPlayerInfoUpdatePacket mit dem modifizierten Profil. Der Client:

  1. Dekodiert das Base64-Payload
  2. Prüft die RSA-Signatur → ✅ gültig (es ist echt die von Jeb_)
  3. Markiert den Skin als secure=true (da die Signatur gültig ist)
  4. Passiert den Filter !isRemote || ps.secure() → ✅ bestanden
  5. Lädt Jeb_s Cape herunter und zeigt ihn an

Ergebnis im Spiel: Der Cape auf deinem Skin

So sieht es in-Game aus. Zuerst die Frontansicht mit Jeb_s Cape auf dem Host:

Cape Mod -- Jeb_s Cape angezeigt auf dem Host

Man erkennt deutlich das rot/weiße Muster des offiziellen Mojang-Studios-Umhangs. Kein Unterschied zu einem echten Jeb_, der seinen eigenen Umhang hätte -- der Client lädt exakt dieselbe Textur von textures.minecraft.net.

Und in immersiver Ansicht, in einer echten Spielsitzung:

Cape Mod -- In-Game-Ansicht mit sichtbarem Cape

Der Cape weht hinter dem Spieler her, bewegt sich mit der Bewegung. Perfekt nicht unterscheidbar von einem authentischen Skin mit offiziellem Cape.

Ein anderer Winkel, in einer Welt mit Lava und Gelände:

Cape Mod -- Cape in einer natürlichen Umgebung

Und eine letzte Nahaufnahme des tatsächlichen Gameplays, die den Cape in Aktion zeigt:

Cape Mod -- Cape im klassischen Minecraft-Gameplay

Für jemanden, der einem LAN beitritt, ohne zu wissen, dass der Host einen Mod hat, gibt es absolut keine Möglichkeit, dies von einem echten Mojang-Cape zu unterscheiden. Das ist genau der Punkt: Die Signatur ist gültig, der Client hat keinen Grund zu zweifeln.

Warum das eine Schwachstelle ist (und warum nicht)

Ironisch: Der Exploit funktioniert gerade weil die Signatur gültig ist. Es gibt hier keinen kryptografischen Bypass -- es ist schlimmer, es ist eine logische Schwachstelle im Vertrauensmodell.

Prüfung Ergebnis
Gültigkeit der RSA-Signatur ✅ Gültig (von Mojang für Jeb_ signiert)
Entspricht die profileId im Payload der UUID des Hosts? ❌ Nein (Jeb_s UUID ≠ UUID des Hosts)
Prüft der Client die Übereinstimmung? ❌ Nein. Nur die RSA-Signatur wird geprüft.

Minecraft vertraut der Signatur, nicht der Identität des Trägers. Solange die Signatur von Mojang stammt, akzeptiert sie der Client. Es ist, als würde man einen gefälschten Reisepass zeigen, der von der Regierung signiert wurde -- das Siegel ist echt, auch wenn der Pass nicht dir gehört.

Die Sicherheitsimplikationen

Begrenzter Umfang auf LAN

Der Mod funktioniert nur auf einem integrierten Server (LAN). Der Angreifer muss:

  • Einen installierten Fabric-Mod haben
  • Der Host einer LAN-Welt sein
  • Seine Freunde verbinden sich ohne Mod (Vanilla)

Aber die Möglichkeiten erweitern sich

Theoretisch könnte man mit derselben Technik:

  • Andere signierte Daten reinjizieren: Köpfe, illegale Verzauberungen, bösartige Chat-Komponenten
  • Mit einem LAN-Tunnel kombinieren (NGROK, playit.gg, Radmin VPN), um Spieler über das Internet zu beeinflussen
  • Auf andere Profil-Eigenschaften ausweiten, die von Signaturen abhängen

Warum Mojang wahrscheinlich nicht patchen wird

Es gibt keine "Verwundbarkeit" im engeren Sinne -- die Signatur ist gültig. Dies zu patchen würde von Mojang verlangen, das gesamte Authentifizierungsmodell zu ändern, was komplex ist. Derzeit ist dies ein Edge Case: LAN-Spieler sollen sich vertrauen.

Die philosophische Falle

Cape Mod ist ein hervorragender Proof of Concept einer umfassenderen Wahrheit: Du darfst niemals einer Signatur vertrauen, ohne zu prüfen, wer sie signiert hat und zu welchem Gegenstand.

Es ist eine Lektion in grundlegender Kryptografie. RSA signiert eine Nachricht, keine Identität. Wenn ich dir eine gültige RSA-Signatur von Mojang gebe, weißt du, dass Mojang etwas signiert hat. Du weißt nicht für wen, und du kannst es nicht einfach durch Betrachten der Nachricht annehmen.

Genau das ist in den 2000er Jahren mit SSL/TLS-Zertifikaten passiert, als CAs alles akzeptierten -- die Signatur war gültig, aber sie bezog sich auf die falsche Domain.

Fazit

Cape Mod ist kein Hack im klassischen Sinne -- es ist eine elegante Ausnutzung eines Mangels an logischer Validierung in Minecraft. Er zeigt, dass:

  1. Eine gültige Signatur nicht die Identität ihres Trägers garantiert
  2. Im LAN ist das Vertrauen schwächer, als man glaubt
  3. Minecrafts textures-Eigenschaften sind im Wesentlichen injizierte Inhalte -- man muss prüfen, ob sie mit dem sie tragenden Spieler übereinstimmen

Wenn du einer LAN-Welt auf einem "unbekannten" Server beitrittst (oder besser gesagt, einem, dessen Host einen verdächtigen Mod hat), hast du bereits ein Sicherheitsproblem, lange bevor der Umhang ins Spiel kommt. Aber es ist symptomatisch: Minecraft geht davon aus, dass sich alle im LAN vertrauen. Das stimmt... bis es das nicht mehr tut.


Ressourcen

3 Kernpunkte

  1. RSA-Signaturen validieren eine Nachricht, nicht eine Identität -- ein Detail, das viele Systeme teuer zu stehen kam.
  2. Minecraft prüft nicht, ob das Spielerprofil mit der empfangenen Signatur übereinstimmt -- eine logische, keine kryptografische Schwachstelle.
  3. Im LAN oder per Tunnel ist alles offen für einen Mod, der den integrierten Server kontrolliert.

Cape Mod: как украсть плащ Jeb_ с помощью инъекции подписи RSA

Мод Fabric, эксплуатирующий логическую уязвимость в системе доверия Minecraft: действительная подпись RSA от Mojang, но воспроизведённая на чужой учётной записи. Объяснение кода, последствия для безопасности и криптографические уроки.

Cape Mod: как украсть плащ Jeb_ с помощью инъекции подписи RSA

alt text Что если я скажу тебе, что достаточно одной действительной подписи RSA -- но для неправильной учётной записи -- чтобы убедить друзей, что ты носишь официальный плащ Mojang? Добро пожаловать в cape-mod, эксплойт для Fabric, показывающий, как Minecraft доверяет подписи, не проверяя, принадлежит ли профиль, для которого она создана, действительно тебе.

Контекст: как Minecraft управляет скинами и плащами

В Java Edition есть вопрос, который редко задают: кто отвечает за отображение скина и плаща игрока -- клиент или сервер?

Ответ неоднозначен:

Компонент Кто отправляет? Кто загружает?
Текстура скина Сервер отправляет подписанный URL Клиент загружает с textures.minecraft.net
Текстура плаща Сервер отправляет подписанный URL Клиент загружает с textures.minecraft.net
Свойство textures Сервер отправляет GameProfile от аутентификации Mojang Клиент проверяет подпись RSA

Ключевой момент: всё содержится в свойстве textures объекта GameProfile. Это свойство содержит:

  • Полезную нагрузку JSON в base64 с URL текстур
  • Подпись RSA, созданную закрытым ключом Mojang

Стена подписи RSA

Каждое свойство textures при декодировании выглядит так:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Клиент проверяет подпись RSA с помощью открытого ключа, встроенного в jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Для удалённых игроков (не локальных) клиент принимает только скины, помеченные как secure -- то есть с действительной подписью:

// SkinManager.createLookup() -- упрощённо
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← удалённые игроки должны быть безопасными
    .orElse(defaultSkin);

Эта проверка теоретически предотвращает спуфинг. Но тут начинается самое интересное.

Уязвимость: повторное использование подписи

Клиент проверяет, что подпись RSA действительна. Но он никогда не проверяет, что profileId в JSON соответствует реальному UUID игрока.

Иными словами: свойство textures, взятое от существующей учётной записи Mojang (например, сотрудника Mojang), можно воспроизвести на любом другом игроке. Подпись остаётся действительной -- она была создана Mojang легитимно -- просто она взята от другого аккаунта.

Как извлечь настоящую подпись?

У Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) есть плащ Mojang Studios. С сервера сессий Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Ответ:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature этого поля value была создана Mojang. Это RSA-2048 SHA-1. Она абсолютно действительна, даже если воспроизвести её на другом UUID -- потому что подпись Jeb_ остаётся подписью Jeb_, и клиент никогда не проверяет, что она должна быть твоей.

Код: как работает мод

Мод cape-mod крошечный -- 65 строк Java. Вот его ядро:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Заменяет свойство textures на свойство Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Шаги:

  1. Mixin на Player.getGameProfile() -- точка, где возвращается профиль игрока
  2. Проверяет, что это локальный сервер (Integrated Server)
  3. Проверяет, что это хост (мир по LAN)
  4. Заменяет свойство textures на свойство Jeb_ (зашитое в коде)
  5. Возвращает новый GameProfile с внедрёнными текстурами

GameProfile таким образом подделан: это искусственно сконструированный профиль, не соответствующий реальному игроку. Свойства textures воспроизведены от Jeb_ -- подпись RSA подлинная, но применена к неправильному профилю. Сетевой пакет при этом легитимен: сервер обычно отправляет ClientboundPlayerInfoUpdatePacket с этим изменённым профилем. Подделан профиль, а не пакет.

Когда друзья хоста подключаются по LAN, они получают ClientboundPlayerInfoUpdatePacket с изменённым профилем. Клиент:

  1. Декодирует полезную нагрузку base64
  2. Проверяет подпись RSA → ✅ действительна (это действительно подпись Jeb_)
  3. Помечает скин как secure=true (так как подпись действительна)
  4. Проходит фильтр !isRemote || ps.secure() → ✅ пройден
  5. Загружает и отображает плащ Jeb_

Результат в игре: плащ на твоём скине

Вот как это выглядит в игре. Сначала вид спереди с плащом Jeb_ на хосте:

Cape Mod -- плащ Jeb_, отображаемый на хосте

Отчётливо виден красно-белый узор официального плаща Mojang Studios. Никакого отличия от настоящего Jeb_ с собственным плащом -- клиент загружает ту же текстуру с textures.minecraft.net.

А в погружающем виде, в реальной игре:

Cape Mod -- вид в игре с видимым плащом

Плащ развевается за игроком, колышется при движении. Абсолютно неотличим от подлинного скина с официальным плащом.

Ещё один ракурс, в мире с лавой и ландшафтом:

Cape Mod -- плащ в естественной среде

И последний крупный план реального геймплея, где видно плащ в действии:

Cape Mod -- плащ в обычном геймплее Minecraft

Для того, кто подключился бы по LAN, не зная, что у хоста установлен мод, нет абсолютно никакого способа отличить это от настоящего плаща Mojang. В этом и суть: подпись действительна, у клиента нет причин сомневаться.

Почему это уязвимость (и почему это не уязвимость)

Ирония в том, что эксплойт работает именно потому, что подпись действительна. Здесь нет криптографического обхода -- это хуже, это логическая уязвимость в модели доверия.

Проверка Результат
Действительность подписи RSA ✅ Действительна (подписана Mojang для Jeb_)
Соответствует ли profileId в полезной нагрузке UUID хоста? ❌ Нет (UUID Jeb_ ≠ UUID хоста)
Проверяет ли клиент это соответствие? ❌ Нет. Проверяется только подпись RSA.

Minecraft доверяет подписи, а не личности того, кто её предъявляет. Пока подпись от Mojang, клиент её принимает. Это как показать поддельный паспорт, подписанный правительством -- печать подлинная, даже если паспорт не твой.

Последствия для безопасности

Ограниченная область действия -- LAN

Мод работает только на встроенном сервере (LAN). Атакующий должен:

  • Иметь установленный мод Fabric
  • Быть хостом мира по LAN
  • Его друзья подключаются без мода (ванильно)

Но возможности расширяются

Теоретически, с той же техникой можно:

  • Внедрять другие подписанные данные: головы, нелегальные зачарования, вредоносные компоненты чата
  • Комбинировать с LAN-туннелем (NGROK, playit.gg, Radmin VPN) для воздействия на игроков через интернет
  • Расширить на другие свойства профиля, зависящие от подписей

Почему Mojang, скорее всего, не будет это исправлять

Строго говоря, это не "уязвимость" -- подпись действительна. Исправление потребовало бы от Mojang изменения всей модели аутентификации, что сложно. Пока это крайний случай: предполагается, что игроки по LAN доверяют друг другу.

Философская ловушка

Cape Mod -- отличный proof of concept более широкой истины: никогда не доверяй подписи, не проверив, кто её создал и к чему она относится.

Это урок элементарной криптографии. RSA подписывает сообщение, а не личность. Если я дам тебе действительную подпись RSA от Mojang, ты будешь знать, что Mojang подписала что-то. Ты не будешь знать для кого, и не сможешь это предполагать, просто глядя на сообщение.

Именно это произошло с SSL/TLS сертификатами в 2000-х, когда ЦС принимали что угодно -- подпись была действительной, но применялась к неправильному домену.

Заключение

Cape Mod -- не взлом в классическом смысле. Это элегантная эксплуатация отсутствия логической проверки в Minecraft. Он показывает, что:

  1. Действительная подпись не гарантирует личность предъявителя
  2. По LAN доверие слабее, чем кажется
  3. Свойства textures в Minecraft -- это, по сути, внедрённый контент -- необходимо проверять, что они соответствуют игроку, который их носит

Если ты подключаешься к миру по LAN на "незнакомом" сервере (или, скорее, где у хоста подозрительный мод), у тебя уже есть проблема безопасности задолго до плаща. Но это симптоматично: Minecraft предполагает, что все в LAN доверяют друг другу. Это верно... пока не перестаёт быть таковым.


Ресурсы

3 ключевых момента

  1. Подписи RSA подтверждают сообщение, а не личность -- деталь, дорого обошедшаяся многим системам.
  2. Minecraft не проверяет, что профиль игрока соответствует полученной подписи -- логическая, а не криптографическая уязвимость.
  3. По LAN или через туннель всё открыто для мода, контролирующего встроенный сервер.

Cape Mod: cómo robar la capa de Jeb_ con una inyección de firma RSA

Un mod Fabric que explota una falla lógica en el sistema de confianza de Minecraft: una firma RSA válida de Mojang pero reutilizada en la cuenta equivocada. Explicación del código, implicaciones de seguridad y lecciones criptográficas.

Cape Mod: cómo robar la capa de Jeb_ con una inyección de firma RSA

alt text ¿Y si te dijera que solo basta una firma RSA válida -- pero para la cuenta equivocada -- para hacer creer a tus amigos que llevas la capa oficial de Mojang? Bienvenido a cape-mod, un exploit Fabric que muestra cómo Minecraft confía en una firma sin verificar que el perfil al que pertenece sea efectivamente el tuyo.

El contexto: cómo Minecraft maneja las skins y las capas

En Java Edition, hay una pregunta que no nos hacemos a menudo: ¿quién es responsable de mostrar la skin y la capa de un jugador -- el cliente o el servidor?

La respuesta es matizada:

Componente ¿Quién lo envía? ¿Quién lo descarga?
Textura de skin El servidor envía la URL firmada El cliente descarga desde textures.minecraft.net
Textura de capa El servidor envía la URL firmada El cliente descarga desde textures.minecraft.net
Propiedad textures El servidor envía el GameProfile desde el auth de Mojang El cliente verifica la firma RSA

El punto clave: todo está contenido en una propiedad llamada textures del GameProfile. Esta propiedad contiene:

  • Un payload JSON en base64 con las URLs de las texturas
  • Una firma RSA hecha con la clave privada de Mojang

El muro de la firma RSA

Cada propiedad textures se ve así cuando la decodificamos:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

El cliente verifica la firma RSA contra la clave pública embebida en el jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Para los jugadores remotos (no locales), el cliente solo acepta las skins marcadas como secure -- es decir, con una firma válida:

// SkinManager.createLookup() -- simplificado
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← los jugadores remotos deben estar asegurados
    .orElse(defaultSkin);

Este check previene el spoofing en teoría. Pero aquí es donde las cosas se ponen interesantes.

La falla: reutilización de firma (signature replay)

El cliente verifica que la firma RSA es válida. Pero nunca verifica que el profileId contenido en el JSON corresponda al UUID real del jugador.

En otras palabras: una propiedad textures tomada de una cuenta Mojang existente (por ejemplo la de un empleado de Mojang) puede reutilizarse en cualquier otro jugador. La firma sigue siendo válida -- fue genuinamente hecha por Mojang -- solo que viene de otra cuenta.

Cómo extraer una firma real

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) tiene la capa de Mojang Studios. Desde el servidor de sesión de Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Respuesta:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

La signature de este campo value fue producida por Mojang. Es RSA-2048 SHA-1. Es absolutamente válida, incluso si la reutilizas en otro UUID -- porque la firma de Jeb_ sigue siendo una firma de Jeb_, y el cliente nunca verifica que se supone que sea la tuya.

El código: cómo funciona el mod

El mod cape-mod es diminuto -- 65 líneas de Java. Aquí está el corazón:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Reemplaza la propiedad textures por la de Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Pasos:

  1. Mixin sobre Player.getGameProfile() -- el punto donde se devuelve el perfil del jugador
  2. Verifica que es un servidor local (Integrated Server)
  3. Verifica que es el host (mundo LAN)
  4. Reemplaza la propiedad textures por la de Jeb_ (hardcodeada)
  5. Devuelve un nuevo GameProfile con las texturas inyectadas

El GameProfile está forjado: es un perfil construido artificialmente, que no corresponde al jugador real. Las propiedades textures están reutilizadas de Jeb_ -- la firma RSA es auténtica pero aplicada al perfil equivocado. El paquete de red, en cambio, es legítimo: el servidor envía normalmente el ClientboundPlayerInfoUpdatePacket con este perfil modificado. Es el perfil el que está forjado, no el paquete.

Cuando los amigos del host se conectan por LAN, reciben el ClientboundPlayerInfoUpdatePacket con el perfil modificado. El cliente:

  1. Decodifica el payload base64
  2. Verifica la firma RSA → ✅ válida (es genuinamente la de Jeb_)
  3. Marca la skin como secure=true (porque la firma es válida)
  4. Pasa el filtro !isRemote || ps.secure() → ✅ pasa
  5. Descarga y muestra la capa de Jeb_

Resultado en el juego: la capa sobre tu skin

Esto es lo que se ve in-game. Primero, vista frontal con la capa de Jeb_ mostrada en el host:

Cape Mod -- Capa de Jeb_ mostrada en el host

Se ve claramente el patrón rojo/blanco de la capa oficial de Mojang Studios. Sin diferencia con un verdadero Jeb_ que tuviera su propia capa -- el cliente descarga exactamente la misma textura desde textures.minecraft.net.

Y en vista inmersiva, dentro de una partida real:

Cape Mod -- Vista en juego con capa visible

La capa flota detrás del jugador, ondea con el movimiento. Perfectamente indistinguible de una skin auténtica con capa oficial.

Otro ángulo, en un mundo con lava y terreno:

Cape Mod -- Capa en un entorno natural

Y una última vista cercana del gameplay real, donde se ve la capa en acción:

Cape Mod -- Capa en gameplay clásico de Minecraft

Para alguien que se uniera a un LAN sin saber que el host tiene un mod, no hay absolutamente ninguna forma de distinguir esto de una capa real de Mojang. Eso es precisamente el punto: la firma es válida, el cliente no tiene ninguna razón para dudar.

Por qué es una falla (y por qué no lo es)

Es irónico: el exploit funciona precisamente porque la firma es válida. No hay un bypass criptográfico aquí -- es peor, es una falla lógica en el modelo de confianza.

Check Resultado
Validez de la firma RSA ✅ Válida (firmada por Mojang para Jeb_)
¿El profileId en el payload corresponde al UUID del host? ❌ No (UUID de Jeb_ ≠ UUID del host)
¿El cliente verifica la correspondencia? ❌ No. Solo se verifica la firma RSA.

Minecraft confía en la firma, no en la identidad de quien la porta. Mientras la firma venga de Mojang, el cliente la acepta. Es como mostrar un pasaporte falso firmado por el gobierno -- el sello es legítimo, aunque el pasaporte no te pertenezca.

Las implicaciones de seguridad

Alcance limitado al LAN

El mod solo funciona en un servidor integrado (LAN). El atacante debe:

  • Tener un mod Fabric instalado
  • Ser el host de un mundo LAN
  • Sus amigos se conectan sin mod (vanilla)

Pero las posibilidades se amplían

En teoría, con la misma técnica, se podría:

  • Reinyectar otros datos firmados: cabezas, encantamientos ilegales, componentes de chat maliciosos
  • Combinar con un túnel LAN (NGROK, playit.gg, Radmin VPN) para afectar jugadores por internet
  • Extender a otras propiedades del perfil que dependen de firmas

Por qué Mojang probablemente no lo parcheará

No hay una "vulnerabilidad" en el sentido estricto -- la firma es válida. Parchear esto requeriría que Mojang modificara el modelo de autenticación completo, lo cual es complejo. Por ahora, es un caso extremo: se supone que los jugadores LAN confían entre sí.

La trampa filosófica

Cape Mod es una excelente prueba de concepto de una verdad más amplia: nunca debes confiar en una firma sin verificar quién la firmó y sobre qué asunto.

Es una lección de criptografía básica. RSA firma un mensaje, no una identidad. Si te doy una firma RSA válida de Mojang, sabes que Mojang firmó algo. No sabes para quién, y no puedes asumirlo solo mirando el mensaje.

Es exactamente lo que pasó con los certificados SSL/TLS en los años 2000 cuando las CA aceptaban cualquier cosa -- la firma era válida, pero se aplicaba al dominio equivocado.

Conclusión

Cape Mod no es un hack en el sentido clásico -- es una explotación elegante de una falta de validación lógica en Minecraft. Muestra que:

  1. Una firma válida no garantiza la identidad de quien la porta
  2. En LAN, la confianza es más débil de lo que creemos
  3. Las propiedades textures de Minecraft son esencialmente contenido inyectado -- hay que verificar que correspondan al jugador que las porta

Si te unes a un mundo LAN en un servidor "desconocido" (o más bien, cuyo host tiene un mod sospechoso), ya tienes un problema de seguridad mucho antes de la capa. Pero es sintomático: Minecraft asume que todos en un LAN confían entre sí. Es cierto... hasta que deja de serlo.


Recursos

3 puntos clave

  1. Las firmas RSA validan un mensaje, no una identidad -- un detalle que le ha costado caro a muchos sistemas.
  2. Minecraft no verifica que el perfil del jugador corresponda a la firma que recibe -- una falla lógica, no criptográfica.
  3. En LAN o en túnel, todo está abierto de par en par para un mod que controla el servidor integrado.

Cape Mod: como roubar a capa do Jeb_ com injeção de assinatura RSA

Um mod Fabric que explora uma falha lógica no sistema de confiança do Minecraft: uma assinatura RSA válida da Mojang mas repetida em uma conta errada. Explicação do código, implicações de segurança e lições criptográficas.

Cape Mod: como roubar a capa do Jeb_ com injeção de assinatura RSA

alt text E se eu te dissesse que basta uma assinatura RSA válida -- mas para a conta errada -- para fazer seus amigos acreditarem que você está usando a capa oficial da Mojang? Bem-vindo ao cape-mod, um exploit Fabric que mostra como o Minecraft confia em uma assinatura sem verificar se o perfil ao qual ela pertence é realmente o seu.

O contexto: como o Minecraft gerencia skins e capas

No Java Edition, há uma pergunta que não fazemos com frequência: quem é responsável por exibir a skin e a capa de um jogador -- o cliente ou o servidor?

A resposta é sutil:

Componente Quem envia? Quem baixa?
Textura da skin O servidor envia a URL assinada O cliente baixa de textures.minecraft.net
Textura da capa O servidor envia a URL assinada O cliente baixa de textures.minecraft.net
Propriedade textures O servidor envia o GameProfile da autenticação Mojang O cliente verifica a assinatura RSA

O ponto chave: tudo está contido em uma propriedade chamada textures do GameProfile. Essa propriedade contém:

  • Um payload JSON em base64 com as URLs das texturas
  • Uma assinatura RSA feita com a chave privada da Mojang

O muro da assinatura RSA

Cada propriedade textures se parece com isso quando decodificada:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

O cliente verifica a assinatura RSA contra a chave pública embutida no jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Para jogadores remotos (não locais), o cliente só aceita skins marcadas como secure -- isto é, com uma assinatura válida:

// SkinManager.createLookup() -- simplificado
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← jogadores remotos precisam ser seguros
    .orElse(defaultSkin);

Essa verificação impede spoofing em teoria. Mas é aqui que as coisas ficam interessantes.

A falha: repetição de assinatura (signature replay)

O cliente verifica se a assinatura RSA é válida. Mas ele nunca verifica se o profileId contido no JSON corresponde ao UUID real do jogador.

Em outras palavras: uma propriedade textures extraída de uma conta Mojang existente (por exemplo, a de um funcionário da Mojang) pode ser repetida em qualquer outro jogador. A assinatura continua válida -- ela foi genuinamente feita pela Mojang -- só veio de outra conta.

Como extrair uma assinatura verdadeira?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) tem a capa Mojang Studios. Do servidor de sessão da Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Resposta:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

A signature desse campo value foi produzida pela Mojang. É RSA-2048 SHA-1. Ela é absolutamente válida, mesmo se você a repetir em outro UUID -- porque a assinatura do Jeb_ continua sendo uma assinatura do Jeb_, e o cliente nunca verifica se ela é supostamente sua.

O código: como o mod funciona

O mod cape-mod é minúsculo -- 65 linhas de Java. Aqui está o coração:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Substitui a propriedade textures pela do Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Etapas:

  1. Mixin em Player.getGameProfile() -- o ponto onde o perfil do jogador é retornado
  2. Verifica se é um servidor local (Integrated Server)
  3. Verifica se é o host (mundo LAN)
  4. Substitui a propriedade textures pela do Jeb_ (hardcoded)
  5. Retorna um novo GameProfile com as texturas injetadas

O GameProfile é portanto forjado: é um perfil construído artificialmente, que não corresponde ao jogador real. As propriedades textures são repetidas do Jeb_ -- a assinatura RSA é autêntica, mas aplicada ao perfil errado. O pacote de rede, por sua vez, é legítimo: o servidor envia normalmente o ClientboundPlayerInfoUpdatePacket com esse perfil modificado. É o perfil que é forjado, não o pacote.

Quando os amigos do host entram via LAN, eles recebem o ClientboundPlayerInfoUpdatePacket com o perfil modificado. O cliente:

  1. Decodifica o payload base64
  2. Verifica a assinatura RSA → ✅ válida (é genuinamente a do Jeb_)
  3. Marca a skin como secure=true (pois a assinatura é válida)
  4. Passa pelo filtro !isRemote || ps.secure() → ✅ passa
  5. Baixa e exibe a capa do Jeb_

Resultado no jogo: a capa na sua skin

Aqui está o resultado in-game. Primeiro, vista frontal com a capa do Jeb_ exibida no host:

Cape Mod -- capa do Jeb_ exibida no host

Vê-se claramente o padrão vermelho/branco da capa oficial Mojang Studios. Nenhuma diferença de um Jeb_ real que teria sua própria capa -- o cliente baixa exatamente a mesma textura de textures.minecraft.net.

E em vista imersiva, em uma partida real:

Cape Mod -- vista em jogo com capa visível

A capa flutua atrás do jogador, ondula com o movimento. Perfeitamente indistinguível de uma skin autêntica com capa oficial.

Outro ângulo, em um mundo com lava e terreno:

Cape Mod -- capa em ambiente natural

E uma última vista aproximada do gameplay real, onde se vê a capa em ação:

Cape Mod -- capa em gameplay clássico Minecraft

Para alguém que entrasse em um LAN sem saber que o host tem um mod, não há absolutamente nenhuma maneira de distinguir isso de uma capa Mojang verdadeira. É precisamente esse o ponto: a assinatura é válida, o cliente não tem motivo para duvidar.

Por que isso é uma falha (e por que não é)

É irônico: o exploit funciona precisamente porque a assinatura é válida. Não há bypass criptográfico aqui -- é pior, é uma falha lógica no modelo de confiança.

Verificação Resultado
Validade da assinatura RSA ✅ Válida (assinada pela Mojang para o Jeb_)
O profileId no payload corresponde ao UUID do host? ❌ Não (UUID do Jeb_ ≠ UUID do host)
O cliente verifica a correspondência? ❌ Não. Apenas a assinatura RSA é verificada.

O Minecraft confia na assinatura, não na identidade de quem a carrega. Contanto que a assinatura venha da Mojang, o cliente a aceita. É como mostrar um passaporte falso assinado pelo governo -- o selo é legítimo, mesmo que o passaporte não lhe pertença.

As implicações de segurança

Escopo limitado ao LAN

O mod só funciona em um servidor integrado (LAN). O atacante precisa:

  • Ter um mod Fabric instalado
  • Ser o host de um mundo LAN
  • Seus amigos conectam sem mod (vanilla)

Mas as possibilidades se ampliam

Em teoria, com a mesma técnica, poderíamos:

  • Reinjetar outros dados assinados: cabeças, encantamentos ilegais, componentes de chat maliciosos
  • Combinar com um túnel LAN (NGROK, playit.gg, Radmin VPN) para afetar jogadores na internet
  • Estender para outras propriedades do perfil que dependem de assinaturas

Por que a Mojang provavelmente não vai corrigir

Não há "vulnerabilidade" no sentido estrito -- a assinatura é válida. Corrigir isso exigiria que a Mojang modificasse o modelo de autenticação completo, o que é complexo. Por enquanto, é um caso extremo: presume-se que jogadores LAN confiam uns nos outros.

A armadilha filosófica

O Cape Mod é um excelente prova de conceito de uma verdade mais ampla: você nunca deve confiar em uma assinatura sem verificar quem a assinou e para que propósito.

É uma lição de criptografia básica. RSA assina uma mensagem, não uma identidade. Se eu te der uma assinatura RSA válida da Mojang, você sabe que a Mojang assinou alguma coisa. Você não sabe para quem, e não pode presumir isso apenas olhando a mensagem.

É exatamente o que aconteceu com certificados SSL/TLS nos anos 2000, quando as CAs aceitavam qualquer coisa -- a assinatura era válida, mas se aplicava ao domínio errado.

Conclusão

O Cape Mod não é um hack no sentido clássico -- é uma exploração elegante de uma falta de validação lógica no Minecraft. Ele mostra que:

  1. Uma assinatura válida não garante a identidade de quem a carrega
  2. Em LAN, a confiança é mais frágil do que se pensa
  3. As propriedades textures do Minecraft são essencialmente conteúdo injetado -- é preciso verificar se correspondem ao jogador que as carrega

Se você entra em um mundo LAN em um servidor "desconhecido" (ou melhor, cujo host tem um mod suspeito), você já tem um problema de segurança muito antes da capa. Mas é sintomático: o Minecraft presume que todos em um LAN confiam uns nos outros. É verdade... até que não seja mais.


Recursos

3 pontos-chave

  1. Assinaturas RSA validam uma mensagem, não uma identidade -- um detalhe que custou caro a muitos sistemas.
  2. O Minecraft não verifica se o perfil do jogador corresponde à assinatura que recebe -- uma falha lógica, não criptográfica.
  3. Em LAN ou túnel, tudo é permitido para um mod que controla o servidor integrado.

Cape Mod : cara mencuri cape Jeb_ dengan injeksi tanda tangan RSA

Mod Fabric yang mengeksploitasi celah logika dalam sistem kepercayaan Minecraft: tanda tangan RSA Mojang yang valid tetapi diputar ulang pada akun yang salah. Penjelasan kode, implikasi keamanan, dan pelajaran kriptografi.

Cape Mod : cara mencuri cape Jeb_ dengan injeksi tanda tangan RSA

alt text Bagaimana jika cukup dengan tanda tangan RSA yang valid -- tetapi untuk akun yang salah -- untuk membuat teman-temanmu percaya bahwa kamu memakai cape resmi Mojang? Selamat datang di cape-mod, sebuah eksploitasi Fabric yang menunjukkan bagaimana Minecraft mempercayai tanda tangan tanpa memverifikasi bahwa profil yang dimilikinya benar-benar milikmu.

Konteks : bagaimana Minecraft mengelola skin dan cape

Di Java Edition, ada pertanyaan yang jarang kita tanyakan : siapa yang bertanggung jawab untuk menampilkan skin dan cape pemain -- klien atau server?

Jawabannya bernuansa :

Komponen Siapa yang mengirim? Siapa yang mengunduh?
Tekstur Skin Server mengirim URL yang ditandatangani Klien mengunduh dari textures.minecraft.net
Tekstur Cape Server mengirim URL yang ditandatangani Klien mengunduh dari textures.minecraft.net
Properti textures Server mengirim GameProfile dari auth Mojang Klien memverifikasi tanda tangan RSA

Poin kuncinya : semuanya terkandung dalam properti bernama textures dari GameProfile. Properti ini berisi :

  • Payload JSON dalam base64 dengan URL tekstur
  • Tanda tangan RSA yang dibuat dengan kunci privat Mojang

Tembok tanda tangan RSA

Setiap properti textures terlihat seperti ini jika didekode :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Klien memverifikasi tanda tangan RSA terhadap kunci publik yang tertanam di dalam jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Untuk pemain jarak jauh (bukan lokal), klien hanya menerima skin yang ditandai sebagai secure -- yaitu dengan tanda tangan yang valid :

// SkinManager.createLookup() -- disederhanakan
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← pemain jarak jauh harus aman
    .orElse(defaultSkin);

Pemeriksaan ini mencegah spoofing secara teori. Tapi di sinilah segalanya menjadi menarik.

Celahnya : replay tanda tangan

Klien memverifikasi bahwa tanda tangan RSA valid. Tapi klien tidak pernah memeriksa apakah profileId yang terkandung dalam JSON cocok dengan UUID asli pemain.

Dengan kata lain : properti textures yang diambil dari akun Mojang yang sudah ada (misalnya milik seorang pegawai Mojang) dapat diputar ulang ke pemain lain mana pun. Tanda tangannya tetap valid -- tanda tangan itu benar-benar dibuat oleh Mojang -- hanya saja berasal dari akun lain.

Bagaimana cara mengekstrak tanda tangan asli?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) memiliki cape Mojang Studios. Dari server sesi Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Respon :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature dari kolom value ini diproduksi oleh Mojang. Itu adalah RSA-2048 SHA-1. Tanda tangan itu benar-benar valid, bahkan jika kamu memutarnya pada UUID lain -- karena tanda tangan Jeb_ tetaplah tanda tangan Jeb_, dan klien tidak pernah memeriksa bahwa itu seharusnya milikmu.

Kode : bagaimana mod ini bekerja

Mod cape-mod sangat kecil -- 65 baris Java. Berikut intinya :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Ganti properti textures dengan milik Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Langkah-langkah :

  1. Mixin pada Player.getGameProfile() -- titik di mana profil pemain dikembalikan
  2. Memeriksa apakah ini server lokal (Integrated Server)
  3. Memeriksa apakah ini host (dunia LAN)
  4. Mengganti properti textures dengan milik Jeb_ (di-hardcode)
  5. Mengembalikan GameProfile baru dengan tekstur yang diinjeksi

GameProfile tersebut direkayasa : itu adalah profil yang dibangun secara artifisial, yang tidak sesuai dengan pemain asli. Properti textures diputar ulang dari Jeb_ -- tanda tangan RSA-nya asli tetapi diterapkan pada profil yang salah. Paket jaringan itu sendiri sah : server mengirim ClientboundPlayerInfoUpdatePacket secara normal dengan profil yang dimodifikasi ini. Profillah yang direkayasa, bukan paketnya.

Ketika teman-teman host bergabung melalui LAN, mereka menerima ClientboundPlayerInfoUpdatePacket dengan profil yang dimodifikasi. Klien :

  1. Mendekode payload base64
  2. Memverifikasi tanda tangan RSA → ✅ valid (itu benar-benar milik Jeb_)
  3. Menandai skin sebagai secure=true (karena tanda tangan valid)
  4. Melewati filter !isRemote || ps.secure() → ✅ lolos
  5. Mengunduh dan menampilkan cape Jeb_

Hasil dalam game : cape di skinmu

Berikut hasilnya di dalam game. Pertama, tampak depan dengan cape Jeb_ yang ditampilkan pada host :

Cape Mod -- Cape Jeb_ ditampilkan pada host

Terlihat jelas motif merah/putih dari cape resmi Mojang Studios. Tidak ada perbedaan dengan Jeb_ asli yang memiliki cape-nya sendiri -- klien mengunduh tekstur yang persis sama dari textures.minecraft.net.

Dan dalam tampilan imersif, di sesi permainan sungguhan :

Cape Mod -- tampilan dalam game dengan cape terlihat

Cape melambai di belakang pemain, bergerak mengikuti gerakan. Sempurna tidak bisa dibedakan dari skin asli dengan cape resmi.

Sudut lain, di dunia dengan lava dan medan :

Cape Mod -- cape di lingkungan alami

Dan satu tampilan jarak dekat dari gameplay nyata, di mana cape terlihat dalam aksi :

Cape Mod -- cape dalam gameplay klasik Minecraft

Bagi seseorang yang bergabung ke LAN tanpa tahu bahwa host memiliki mod, sama sekali tidak ada cara untuk membedakan ini dari cape Mojang asli. Itulah tepatnya intinya : tanda tangan itu valid, klien tidak punya alasan untuk meragukannya.

Mengapa ini celah (dan mengapa ini bukan)

Ironisnya : eksploitasi ini berhasil tepat karena tanda tangan itu valid. Tidak ada bypass kriptografi di sini -- lebih buruk lagi, ini adalah celah logika dalam model kepercayaan.

Pemeriksaan Hasil
Validitas tanda tangan RSA ✅ Valid (ditandatangani oleh Mojang untuk Jeb_)
Apakah profileId dalam payload cocok dengan UUID host? ❌ Tidak (UUID Jeb_ ≠ UUID host)
Apakah klien memeriksa kecocokan ini? ❌ Tidak. Hanya tanda tangan RSA yang diperiksa.

Minecraft mempercayai tanda tangan, bukan identitas orang yang membawanya. Selama tanda tangan berasal dari Mojang, klien menerimanya. Ini seperti menunjukkan paspor palsu yang ditandatangani oleh pemerintah -- stempelnya sah, meskipun paspor itu bukan milikmu.

Implikasi keamanan

Jangkauan terbatas pada LAN

Mod ini hanya berfungsi pada server terintegrasi (LAN). Penyerang harus :

  • Memasang mod Fabric
  • Menjadi host dunia LAN
  • Teman-temannya terhubung tanpa mod (vanilla)

Tapi kemungkinannya meluas

Secara teori, dengan teknik yang sama, seseorang bisa :

  • Menyuntikkan data bertanda tangan lainnya : kepala, enchantment ilegal, komponen chat berbahaya
  • Menggabungkan dengan tunnel LAN (NGROK, playit.gg, Radmin VPN) untuk memengaruhi pemain di internet
  • Memperluas ke properti profil lain yang bergantung pada tanda tangan

Mengapa Mojang mungkin tidak akan memperbaikinya

Tidak ada "kerentanan" dalam arti sebenarnya -- tanda tangan itu valid. Memperbaiki ini akan mengharuskan Mojang mengubah model autentikasi secara menyeluruh, yang rumit. Untuk saat ini, ini adalah edge case : pemain LAN dianggap saling percaya.

Perangkap filosofis

Cape Mod adalah proof of concept yang sangat baik dari kebenaran yang lebih luas : kamu tidak boleh pernah mempercayai tanda tangan tanpa memverifikasi siapa yang menandatanganinya dan untuk subjek apa.

Ini adalah pelajaran dalam kriptografi dasar. RSA menandatangani sebuah pesan, bukan sebuah identitas. Jika aku memberimu tanda tangan RSA yang valid dari Mojang, kamu tahu bahwa Mojang telah menandatangani sesuatu. Kamu tidak tahu untuk siapa, dan kamu tidak bisa menganggapnya hanya dengan melihat pesannya.

Persis seperti apa yang terjadi dengan sertifikat SSL/TLS di tahun 2000-an ketika CA menerima apa saja -- tanda tangan itu valid, tetapi diterapkan pada domain yang salah.

Kesimpulan

Cape Mod bukanlah peretasan dalam arti klasik -- ini adalah eksploitasi elegan dari kurangnya validasi logika di Minecraft. Ini menunjukkan bahwa :

  1. Tanda tangan yang valid tidak menjamin identitas pembawanya
  2. Di LAN, kepercayaan lebih lemah dari yang kita kira
  3. Properti textures Minecraft pada dasarnya adalah konten yang diinjeksi -- kita perlu memverifikasi bahwa properti itu sesuai dengan pemain yang membawanya

Jika kamu bergabung ke dunia LAN di server "yang tidak dikenal" (atau lebih tepatnya, yang host-nya memiliki mod mencurigakan), kamu sudah memiliki masalah keamanan jauh sebelum cape. Tapi ini gejala : Minecraft berasumsi bahwa semua orang di LAN saling percaya. Itu benar... sampai tidak lagi.


Sumber Daya

3 Poin Penting

  1. Tanda tangan RSA memvalidasi sebuah pesan, bukan identitas -- detail yang telah merugikan banyak sistem.
  2. Minecraft tidak memeriksa apakah profil pemain sesuai dengan tanda tangan yang diterima -- celah logika, bukan kriptografi.
  3. Di LAN atau tunnel, semuanya terbuka lebar untuk mod yang mengontrol server terintegrasi.

Cape Mod : RSA हस्ताक्षर इंजेक्शन से Jeb_ की केप कैसे चुराएं

एक Fabric मॉड जो Minecraft के विश्वास मॉडल में एक तार्किक खामी का शोषण करता है : Mojang का एक वैध RSA हस्ताक्षर लेकिन गलत खाते पर रीप्ले किया गया। कोड स्पष्टीकरण, सुरक्षा निहितार्थ और क्रिप्टोग्राफ़िक सबक।

Cape Mod : RSA हस्ताक्षर इंजेक्शन से Jeb_ की केप कैसे चुराएं

alt text क्या होगा अगर मैं तुमसे कहूँ कि बस एक वैध RSA हस्ताक्षर -- लेकिन गलत खाते के लिए -- तुम्हारे दोस्तों को यकीन दिलाने के लिए काफी है कि तुम Mojang की आधिकारिक केप पहन रहे हो? आओ मिलते हैं cape-mod से, एक Fabric एक्सप्लॉइट जो दिखाता है कि Minecraft बिना यह जाँचे हस्ताक्षर पर भरोसा कैसे करता है कि जिस प्रोफ़ाइल से वह संबंधित है वह वास्तव में तुम्हारी है।

संदर्भ : Minecraft स्किन और केप कैसे प्रबंधित करता है

Java Edition में, एक सवाल है जो हम अक्सर नहीं पूछते : कौन जिम्मेदार है कि खिलाड़ी की स्किन और केप प्रदर्शित हो -- क्लाइंट या सर्वर?

जवाब बारीक है :

घटक कौन भेजता है? कौन डाउनलोड करता है?
स्किन टेक्सचर सर्वर हस्ताक्षरित URL भेजता है क्लाइंट textures.minecraft.net से डाउनलोड करता है
केप टेक्सचर सर्वर हस्ताक्षरित URL भेजता है क्लाइंट textures.minecraft.net से डाउनलोड करता है
textures प्रॉपर्टी सर्वर Mojang ऑथ से GameProfile भेजता है क्लाइंट RSA हस्ताक्षर सत्यापित करता है

मुख्य बिंदु : सब कुछ GameProfile की textures नामक प्रॉपर्टी में समाहित है। इस प्रॉपर्टी में शामिल है :

  • टेक्सचर URLs के साथ एक base64 JSON payload
  • Mojang की निजी कुंजी से बना एक RSA हस्ताक्षर

RSA हस्ताक्षर की दीवार

हर textures प्रॉपर्टी डिकोड करने पर ऐसी दिखती है :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

क्लाइंट jar में एम्बेडेड सार्वजनिक कुंजी (yggdrasil_session_pubkey.der) के विरुद्ध RSA हस्ताक्षर सत्यापित करता है :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

दूरस्थ खिलाड़ियों (लोकल नहीं) के लिए, क्लाइंट केवल उन स्किन को स्वीकार करता है जो secure के रूप में चिह्नित हैं -- यानी वैध हस्ताक्षर के साथ :

// SkinManager.createLookup() -- सरलीकृत
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← दूरस्थ खिलाड़ियों को सुरक्षित होना चाहिए
    .orElse(defaultSkin);

यह जाँच सैद्धांतिक रूप से स्पूफिंग को रोकती है। लेकिन यहीं चीज़ें दिलचस्प हो जाती हैं।

खामी : हस्ताक्षर रीप्ले

क्लाइंट जाँचता है कि RSA हस्ताक्षर वैध है। लेकिन वह कभी नहीं जाँचता कि JSON में मौजूद profileId खिलाड़ी के वास्तविक UUID से मेल खाता है या नहीं।

दूसरे शब्दों में : किसी मौजूदा Mojang खाते (जैसे किसी Mojang कर्मचारी के खाते) से ली गई textures प्रॉपर्टी किसी भी अन्य खिलाड़ी पर रीप्ले की जा सकती है। हस्ताक्षर वैध रहता है -- यह वास्तव में Mojang द्वारा बनाया गया था -- यह बस किसी दूसरे खाते से आया है।

असली हस्ताक्षर कैसे निकालें?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) के पास Mojang Studios केप है। Mojang सत्र सर्वर से :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

जवाब :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

इस value फ़ील्ड का signature Mojang द्वारा बनाया गया है। यह RSA-2048 SHA-1 है। यह पूरी तरह वैध है, भले ही तुम इसे किसी दूसरे UUID पर रीप्ले करो -- क्योंकि Jeb_ का हस्ताक्षर Jeb_ का ही हस्ताक्षर रहता है, और क्लाइंट कभी जाँच नहीं करता कि यह तुम्हारा होना चाहिए।

कोड : मॉड कैसे काम करता है

cape-mod मॉड बहुत छोटा है -- Java की 65 पंक्तियाँ। यहाँ मुख्य भाग है :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Jeb_ की textures प्रॉपर्टी से बदलें
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

चरण :

  1. Player.getGameProfile() पर Mixin -- वह बिंदु जहाँ खिलाड़ी का प्रोफ़ाइल लौटाया जाता है
  2. जाँचता है कि यह एक स्थानीय सर्वर (Integrated Server) है
  3. जाँचता है कि यह होस्ट (LAN वर्ल्ड) है
  4. textures प्रॉपर्टी को Jeb_ की (हार्डकोडेड) से बदलता है
  5. इंजेक्टेड टेक्सचर के साथ एक नया GameProfile लौटाता है

GameProfile इस प्रकार जाली है : यह एक कृत्रिम रूप से बनाया गया प्रोफ़ाइल है, जो असली खिलाड़ी से मेल नहीं खाता। textures प्रॉपर्टी Jeb_ से रीप्ले की गई है -- RSA हस्ताक्षर प्रामाणिक है लेकिन गलत प्रोफ़ाइल पर लागू किया गया है। नेटवर्क पैकेट, हालांकि, वैध है : सर्वर सामान्य रूप से इस संशोधित प्रोफ़ाइल के साथ ClientboundPlayerInfoUpdatePacket भेजता है। जाली प्रोफ़ाइल है, पैकेट नहीं।

जब होस्ट के दोस्त LAN के माध्यम से जुड़ते हैं, तो उन्हें संशोधित प्रोफ़ाइल के साथ ClientboundPlayerInfoUpdatePacket प्राप्त होता है। क्लाइंट :

  1. base64 payload डिकोड करता है
  2. RSA हस्ताक्षर सत्यापित करता है → ✅ वैध (यह वास्तव में Jeb_ का है)
  3. स्किन को secure=true चिह्नित करता है (क्योंकि हस्ताक्षर वैध है)
  4. फ़िल्टर !isRemote || ps.secure() पास करता है → ✅ पास
  5. Jeb_ की केप डाउनलोड और प्रदर्शित करता है

खेल में परिणाम : तुम्हारी स्किन पर केप

यहाँ देखो यह इन-गेम कैसा दिखता है। पहले, होस्ट पर Jeb_ की केप दिखाने वाला सामने का दृश्य :

Cape Mod -- Jeb_ केप होस्ट पर प्रदर्शित

आधिकारिक Mojang Studios केप का लाल/सफेद पैटर्न साफ दिखता है। एक वास्तविक Jeb_ से कोई अंतर नहीं जिसके पास अपनी केप होगी -- क्लाइंट बिल्कुल वही टेक्सचर textures.minecraft.net से डाउनलोड करता है।

और इमर्सिव दृश्य में, एक वास्तविक गेम में :

Cape Mod -- केप के साथ इन-गेम दृश्य

केप खिलाड़ी के पीछे लहराती है, हरकत के साथ हिलती है। आधिकारिक केप वाली प्रामाणिक स्किन से पूरी तरह अविभेद्य।

दूसरा कोण, लावा और इलाके वाली दुनिया में :

Cape Mod -- प्राकृतिक वातावरण में केप

और एक अंतिम नज़दीकी दृश्य वास्तविक गेमप्ले का, जहाँ केप एक्शन में दिखती है :

Cape Mod -- क्लासिक Minecraft गेमप्ले में केप

जो कोई बिना यह जाने LAN में शामिल होता है कि होस्ट के पास मॉड है, उसके लिए इसे असली Mojang केप से अलग करने का बिल्कुल कोई तरीका नहीं है। यही सटीक बात है : हस्ताक्षर वैध है, क्लाइंट के पास संदेह करने का कोई कारण नहीं है।

यह खामी क्यों है (और क्यों नहीं है)

व्यंग्यात्मक बात : एक्सप्लॉइट ठीक इसलिए काम करता है क्योंकि हस्ताक्षर वैध है। यहाँ कोई क्रिप्टोग्राफ़िक बाइपास नहीं है -- यह उससे भी बुरा है, यह विश्वास मॉडल में एक तार्किक खामी है।

जाँच परिणाम
RSA हस्ताक्षर की वैधता ✅ वैध (Mojang द्वारा Jeb_ के लिए हस्ताक्षरित)
क्या payload में profileId होस्ट के UUID से मेल खाता है? ❌ नहीं (Jeb_ का UUID ≠ होस्ट का UUID)
क्या क्लाइंट मिलान की जाँच करता है? ❌ नहीं। केवल RSA हस्ताक्षर सत्यापित किया जाता है।

Minecraft हस्ताक्षर पर भरोसा करता है, उसे धारण करने वाले की पहचान पर नहीं। जब तक हस्ताक्षर Mojang से आता है, क्लाइंट उसे स्वीकार करता है। यह सरकार द्वारा हस्ताक्षरित नकली पासपोर्ट दिखाने जैसा है -- मुहर वैध है, भले ही पासपोर्ट तुम्हारा न हो।

सुरक्षा निहितार्थ

LAN तक सीमित दायरा

मॉड केवल एकीकृत सर्वर (LAN) पर काम करता है। हमलावर को चाहिए :

  • Fabric मॉड इंस्टॉल हो
  • LAN वर्ल्ड का होस्ट हो
  • उसके दोस्त बिना मॉड (वैनिला) जुड़ें

लेकिन संभावनाएँ बढ़ती हैं

सैद्धांतिक रूप से, उसी तकनीक से, हम यह कर सकते हैं :

  • अन्य हस्ताक्षरित डेटा रीइंजेक्ट करना : हेड, अवैध एन्चैंटमेंट, दुर्भावनापूर्ण चैट घटक
  • LAN टनल (NGROK, playit.gg, Radmin VPN) के साथ जोड़कर इंटरनेट पर खिलाड़ियों को प्रभावित करना
  • प्रोफ़ाइल के अन्य गुणों तक विस्तार करना जो हस्ताक्षर पर निर्भर करते हैं

Mojang शायद पैच क्यों नहीं करेगा

सख्त अर्थों में कोई "भेद्यता" नहीं है -- हस्ताक्षर वैध है। इसे पैच करने के लिए Mojang को पूर्ण प्रमाणीकरण मॉडल बदलना होगा, जो जटिल है। फिलहाल, यह एक एज केस है : LAN खिलाड़ियों से एक-दूसरे पर भरोसा करने की अपेक्षा की जाती है।

दार्शनिक जाल

Cape Mod एक व्यापक सत्य का एक उत्कृष्ट प्रूफ ऑफ कॉन्सेप्ट है : तुम्हें कभी भी बिना यह जाँचे कि किसने और किस विषय पर हस्ताक्षर किया है, हस्ताक्षर पर भरोसा नहीं करना चाहिए।

यह बुनियादी क्रिप्टोग्राफी का सबक है। RSA एक संदेश पर हस्ताक्षर करता है, पहचान पर नहीं। अगर मैं तुम्हें Mojang का एक वैध RSA हस्ताक्षर दूँ, तो तुम जानते हो कि Mojang ने किसी चीज़ पर हस्ताक्षर किया है। तुम नहीं जानते कि किसके लिए, और तुम केवल संदेश देखकर यह नहीं मान सकते।

ठीक यही 2000 के दशक में SSL/TLS प्रमाणपत्रों के साथ हुआ था जब CAs कुछ भी स्वीकार कर लेते थे -- हस्ताक्षर वैध था, लेकिन वह गलत डोमेन पर लागू होता था।

निष्कर्ष

Cape Mod शास्त्रीय अर्थों में हैक नहीं है -- यह Minecraft में तार्किक सत्यापन की कमी का एक सुरुचिपूर्ण शोषण है। यह दिखाता है कि :

  1. एक वैध हस्ताक्षर उसे धारण करने वाले की पहचान की गारंटी नहीं देता
  2. LAN पर, भरोसा हमारी सोच से कमज़ोर है
  3. Minecraft की textures प्रॉपर्टी अनिवार्य रूप से इंजेक्टेड सामग्री है -- यह सुनिश्चित करना आवश्यक है कि वे उस खिलाड़ी से मेल खाती हों जो उन्हें धारण कर रहा है

अगर तुम किसी "अज्ञात" सर्वर पर LAN वर्ल्ड में शामिल होते हो (या यूँ कहें, जिसके होस्ट के पास संदिग्ध मॉड है), तो केप से बहुत पहले ही तुम्हें सुरक्षा समस्या है। लेकिन यह लक्षणात्मक है : Minecraft मानता है कि LAN पर हर कोई एक-दूसरे पर भरोसा करता है। यह सच है... जब तक सच न रहे।


संसाधन

3 मुख्य बिंदु

  1. RSA हस्ताक्षर एक संदेश को मान्य करते हैं, पहचान को नहीं -- एक विवरण जिसने कई सिस्टमों को भारी नुकसान पहुँचाया है।
  2. Minecraft यह नहीं जाँचता कि खिलाड़ी का प्रोफ़ाइल उसे मिले हस्ताक्षर से मेल खाता है या नहीं -- एक तार्किक खामी, क्रिप्टोग्राफ़िक नहीं।
  3. LAN या टनल में, एक मॉड जो एकीकृत सर्वर को नियंत्रित करता है, उसके लिए सब कुछ खुला है।

مود الكيب: كيف تسرق كيب Jeb_ بحقن توقيع RSA

مود Fabric يستغل ثغرة منطقية في نظام الثقة في ماينكرافت: توقيع RSA صحيح من Mojang لكنه معاد استخدامه على حساب خاطئ. شرح الكود، تداعيات أمنية ودروس تشفيرية.

مود الكيب: كيف تسرق كيب Jeb_ بحقن توقيع RSA

alt text ماذا لو أخبرتك أن توقيع RSA صحيحًا واحدًا -- لكن للحساب الخطأ -- يكفي لجعل أصدقائك يعتقدون أنك ترتدي الكيب الرسمي من Mojang؟ مرحبًا بك في cape-mod، exploit من نوع Fabric يوضح كيف تثق ماينكرافت بالتوقيع دون التحقق من أن الملف الشخصي الذي ينتمي إليه هو ملفك بالفعل.

السياق: كيف تدير ماينكرافت skins والكيب

في Java Edition، هناك سؤال لا نطرحه كثيرًا: من المسؤول عن عرض skin وكيب اللاعب -- العميل أم الخادم؟

الإجابة دقيقة:

المكون من يرسله؟ من ينزّله؟
نسيج skin الخادم يرسل الرابط الموقّع العميل ينزّل من textures.minecraft.net
نسيج كيب الخادم يرسل الرابط الموقّع العميل ينزّل من textures.minecraft.net
خاصية textures الخادم يرسل GameProfile من مصادقة Mojang العميل يتحقق من توقيع RSA

النقطة الأساسية: كل شيء محتوى في خاصية تُسمى textures ضمن GameProfile. تحتوي هذه الخاصية على:

  • Payload JSON مع روابط textures بصيغة base64
  • توقيع RSA مصنوع بالمفتاح الخاص لـ Mojang

جدار توقيع RSA

كل خاصية textures تبدو هكذا عند فك تشفيرها:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

يتحقق العميل من توقيع RSA مقابل المفتاح العام المضمن في ملف الجرة (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

بالنسبة للاعبين عن بُعد (ليسوا محليين)، لا يقبل العميل سوى skins الموسومة كـ secure -- أي ذات توقيع صحيح:

// SkinManager.createLookup() -- مبسط
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← اللاعبون عن بُعد يجب أن يكونوا secure
    .orElse(defaultSkin);

هذا الفحص يمنع spoofing نظريًا. لكن هنا تصبح الأمور مثيرة للاهتمام.

الثغرة: إعادة استخدام التوقيع

العميل يتحقق من أن توقيع RSA صحيح. لكنه لا يتحقق أبدًا من أن profileId الموجود في JSON يطابق UUID الحقيقي للاعب.

بعبارة أخرى: خاصية textures مأخوذة من حساب Mojang موجود (مثل حساب موظف في Mojang) يمكن إعادة استخدامها على أي لاعب آخر. يبقى التوقيع صحيحًا -- فقد تم إنشاؤه بشكل أصيل بواسطة Mojang -- لكنه فقط من حساب آخر.

كيف تستخرج توقيعًا حقيقيًا؟

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) لديه كيب Mojang Studios. من خادم جلسات Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

الرد:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

التوقيع signature لحقل value هذا تم إنتاجه بواسطة Mojang. إنه RSA-2048 SHA-1. إنه صحيح تمامًا، حتى لو أعدت استخدامه على UUID آخر -- لأن توقيع Jeb_ يبقى توقيع Jeb_، والعميل لا يتحقق أبدًا من أنه مفترض أن يكون لك.

الكود: كيف يعمل المود

مود cape-mod صغير جدًا -- 65 سطرًا من Java. إليك الجوهر:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // يستبدل خاصية textures بخاصية Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

الخطوات:

  1. Mixin على Player.getGameProfile() -- النقطة التي يُرجع فيها ملف اللاعب
  2. يتحقق من أنه خادم محلي (Integrated Server)
  3. يتحقق من أنه host (عالم LAN)
  4. يستبدل خاصية textures بخاصية Jeb_ (مضمنة في الكود)
  5. يُرجع GameProfile جديدًا مع textures المحقونة

الـ GameProfile إذًا مُزيّف: إنه ملف شخصي مبني اصطناعيًا، لا يطابق اللاعب الحقيقي. خصائص textures مُعاد استخدامها من Jeb_ -- توقيع RSA أصلي لكنه مطبّق على الملف الخطأ. حزمة الشبكة نفسها شرعية: الخادم يرسل ClientboundPlayerInfoUpdatePacket بشكل طبيعي مع هذا الملف المعدّل. الملف هو المُزيّف، وليس الحزمة.

عندما ينضم أصدقاء host عبر LAN، يستقبلون ClientboundPlayerInfoUpdatePacket مع الملف المعدّل. العميل:

  1. يفك تشفير payload base64
  2. يتحقق من توقيع RSA → ✅ صحيح (إنه توقيع Jeb_ الأصلي)
  3. يوسم skin كـ secure=true (لأن التوقيع صحيح)
  4. يجتاز المرشح !isRemote || ps.secure() → ✅ يجتاز
  5. ينزّل ويعرض كيب Jeb_

النتيجة في اللعبة: الكيب على skinك

إليك ما يبدو عليه in-game. أولاً، منظر أمامي مع كيب Jeb_ معروض على host:

Cape Mod -- كيب Jeb_ معروض على host

نرى بوضوح نمط الأحمر/الأبيض للكيب الرسمي لـ Mojang Studios. لا فرق عن Jeb_ الحقيقي الذي يملك كيبه الخاص -- العميل ينزّل نفس النسيج بالضبط من textures.minecraft.net.

وفي منظر غامر، داخل لعبة حقيقية:

Cape Mod -- منظر في اللعبة مع كيب مرئي

الكيب يطفو خلف اللاعب، يتموج مع الحركة. لا يمكن تمييزه تمامًا عن skin أصلي بكيب رسمي.

زاوية أخرى، في عالم مع حمم وتضاريس:

Cape Mod -- كيب في بيئة طبيعية

وآخر منظر مقرّب من gameplay الفعلي، حيث نرى الكيب أثناء الحركة:

Cape Mod -- كيب في لعبة ماينكرافت كلاسيكية

لشخص ينضم إلى LAN دون علمه أن host لديه مود، لا توجد absolutely أي طريقة لتمييز هذا عن كيب Mojang حقيقي. هذه هي النقطة بالضبط: التوقيع صحيح، العميل ليس لديه أي سبب للشك.

لماذا هذه ثغرة (ولماذا ليست كذلك)

المفارقة: الـ exploit يعمل بالضبط لأن التوقيع صحيح. لا يوجد bypass تشفيري هنا -- بل أسوأ، إنها ثغرة منطقية في نموذج الثقة.

الفحص النتيجة
صحة توقيع RSA ✅ صحيح (موقّع من Mojang لـ Jeb_)
هل profileId في payload يطابق UUID الـ host؟ ❌ لا (UUID Jeb_ ≠ UUID الـ host)
هل يتحقق العميل من التطابق؟ ❌ لا. يتم التحقق فقط من توقيع RSA.

ماينكرافت تثق بالتوقيع، وليس بهوية حامله. طالما أن التوقيع من Mojang، يقبله العميل. الأمر أشبه بإظهار جواز سفر مزوّر موقّع من الحكومة -- الختم شرعي، حتى لو كان جواز السفر ليس لك.

التداعيات الأمنية

النطاق محدود بـ LAN

المود يعمل فقط على خادم مدمج (LAN). المهاجم يجب أن:

  • يكون لديه مود Fabric مثبت
  • يكون host لعالم LAN
  • يتصل أصدقاؤه بدون مود (vanilla)

لكن الاحتمالات تتسع

نظريًا، بنفس التقنية، يمكن:

  • إعادة حقن بيانات أخرى موقعة: رؤوس، enchantments غير قانونية، مكونات دردشة خبيثة
  • الدمج مع نفق LAN (NGROK، playit.gg، Radmin VPN) للتأثير على لاعبين عبر الإنترنت
  • التوسع لخصائص أخرى من الملف الشخصي تعتمد على التوقيعات

لماذا لن تصحح Mojang هذا على الأرجح

لا توجد "ثغرة أمنية" بالمعنى الدقيق -- التوقيع صحيح. تصحيح هذا يتطلب من Mojang تغيير نموذج المصادقة بالكامل، وهو أمر معقد. حاليًا، إنها حالة حافة: من المفترض أن يثق لاعبو LAN ببعضهم البعض.

الفخ الفلسفي

مود الكيب هو proof of concept ممتاز لحقيقة أوسع: يجب ألا تثق أبدًا بتوقيع دون التحقق من وقّعه ولماذا وقّعه.

هذا درس في التشفير الأساسي. RSA يوقع رسالة، وليس هوية. إذا أعطيتك توقيع RSA صحيحًا من Mojang، فأنت تعلم أن Mojang وقّعت شيئًا ما. لا تعلم لمن، ولا يمكنك افتراض ذلك بمجرد النظر إلى الرسالة.

هذا بالضبط ما حدث مع شهادات SSL/TLS في العقد 2000 عندما كانت الـ CAs تقبل أي شيء -- التوقيع كان صحيحًا، لكنه كان مُطبّقًا على النطاق الخطأ.

الخاتمة

مود الكيب ليس اختراقًا بالمعنى الكلاسيكي -- إنه استغلال أنيق لغياب التحقق المنطقي في ماينكرافت. يوضح أن:

  1. التوقيع الصحيح لا يضمن هوية حامله
  2. في LAN، الثقة أضعف مما نعتقد
  3. خصائص textures في ماينكرافت هي أساسًا محتوى محقون -- يجب التحقق من أنها تطابق اللاعب الذي يحملها

إذا انضممت إلى عالم LAN على خادم "غير معروف" (أو بالأحرى، host لديه مود مشبوه)، فلديك مشكلة أمنية قبل الكيب بوقت طويل. لكن هذا عرضي: ماينكرافت تفترض أن الجميع على LAN يثقون ببعضهم البعض. هذا صحيح... إلى أن لا يعود كذلك.


موارد

3 نقاط رئيسية

  1. توقيعات RSA تتحقق من رسالة، وليس هوية -- تفصيل كلّف العديد من الأنظمة غاليًا.
  2. ماينكرافت لا تتحقق من أن ملف اللاعب يطابق التوقيع الذي يستقبله -- ثغرة منطقية، لا تشفيرية.
  3. في LAN أو عبر نفق، كل شيء مفتوح لمود يتحكم في الخادم المدمج.

Cape Mod: cách đánh cắp cape của Jeb_ bằng cách chèn chữ ký RSA

Một mod Fabric khai thác lỗ hổng logic trong hệ thống tin cậy của Minecraft: chữ ký RSA hợp lệ từ Mojang nhưng được phát lại trên tài khoản sai. Giải thích code, tác động bảo mật và bài học về mật mã.

Cape Mod: cách đánh cắp cape của Jeb_ bằng cách chèn chữ ký RSA

alt text Nếu tôi nói với bạn rằng chỉ cần một chữ ký RSA hợp lệ -- nhưng dành cho tài khoản sai -- để khiến bạn bè tin rằng bạn đang đeo cape chính thức của Mojang? Chào mừng đến với cape-mod, một exploit Fabric cho thấy cách Minecraft tin tưởng một chữ ký mà không kiểm tra xem hồ sơ sở hữu nó có thực sự là của bạn hay không.

Bối cảnh: Minecraft quản lý skin và cape như thế nào

Trong Java Edition, có một câu hỏi hiếm khi được đặt ra: ai chịu trách nhiệm hiển thị skin và cape của người chơi -- client hay server?

Câu trả lời có sắc thái:

Thành phần Ai gửi? Ai tải về?
Texture skin Server gửi URL đã ký Client tải từ textures.minecraft.net
Texture cape Server gửi URL đã ký Client tải từ textures.minecraft.net
Thuộc tính textures Server gửi GameProfile từ auth Mojang Client xác minh chữ ký RSA

Điểm mấu chốt: mọi thứ nằm trong một thuộc tính gọi là textures của GameProfile. Thuộc tính này chứa:

  • Payload JSON mã hóa base64 chứa URL của các texture
  • Một chữ ký RSA được tạo bằng khóa riêng của Mojang

Bức tường chữ ký RSA

Mỗi thuộc tính textures trông như thế này khi giải mã:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Client xác minh chữ ký RSA dựa trên khóa công khai được nhúng trong jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Đối với người chơi từ xa (không phải local), client chỉ chấp nhận skin được đánh dấu là secure -- tức là có chữ ký hợp lệ:

// SkinManager.createLookup() -- simplified
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← người chơi từ xa phải được bảo mật
    .orElse(defaultSkin);

Về lý thuyết, kiểm tra này ngăn chặn spoofing. Nhưng đây là lúc mọi thứ trở nên thú vị.

Lỗ hổng: phát lại chữ ký (signature replay)

Client xác minh rằng chữ ký RSA hợp lệ. Nhưng nó không bao giờ kiểm tra xem profileId trong JSON có khớp với UUID thực của người chơi hay không.

Nói cách khác: một thuộc tính textures lấy từ tài khoản Mojang hiện có (ví dụ của một nhân viên Mojang) có thể được phát lại cho bất kỳ người chơi nào khác. Chữ ký vẫn hợp lệ -- nó thực sự do Mojang tạo ra -- chỉ là nó đến từ một tài khoản khác.

Làm thế nào để trích xuất chữ ký thật?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) có cape Mojang Studios. Từ máy chủ session của Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Phản hồi:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

Chữ ký signature của trường value này được tạo bởi Mojang. Đó là RSA-2048 SHA-1. Nó hoàn toàn hợp lệ, ngay cả khi bạn phát lại nó trên một UUID khác -- bởi vì chữ ký của Jeb_ vẫn là chữ ký của Jeb_, và client không bao giờ kiểm tra xem nó có đáng lẽ phải là của bạn hay không.

Code: cách mod hoạt động

Mod cape-mod rất nhỏ -- 65 dòng Java. Đây là phần lõi:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Thay thế thuộc tính textures bằng của Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Các bước:

  1. Mixin trên Player.getGameProfile() -- điểm mà hồ sơ người chơi được trả về
  2. Kiểm tra đó là server local (Integrated Server)
  3. Kiểm tra đó là host (LAN world)
  4. Thay thế thuộc tính textures bằng của Jeb_ (được hardcode)
  5. Trả về GameProfile mới với texture đã được chèn

GameProfile vì thế bị làm giả: đó là một hồ sơ được xây dựng nhân tạo, không khớp với người chơi thực. Các thuộc tính textures được phát lại từ Jeb_ -- chữ ký RSA là xác thực nhưng được áp dụng cho hồ sơ sai. Gói tin mạng thì hợp lệ: server gửi ClientboundPlayerInfoUpdatePacket bình thường với hồ sơ đã sửa đổi này. Hồ sơ bị làm giả, không phải gói tin.

Khi bạn bè của host tham gia qua LAN, họ nhận được ClientboundPlayerInfoUpdatePacket với hồ sơ đã sửa đổi. Client:

  1. Giải mã payload base64
  2. Xác minh chữ ký RSA → ✅ hợp lệ (thực sự là của Jeb_)
  3. Đánh dấu skin là secure=true (vì chữ ký hợp lệ)
  4. Vượt qua bộ lọc !isRemote || ps.secure() → ✅ vượt qua
  5. Tải về và hiển thị cape của Jeb_

Kết quả trong game: cape trên skin của bạn

Đây là kết quả in-game. Đầu tiên, nhìn từ phía trước với cape của Jeb_ hiển thị trên host:

Cape Mod -- Cape Jeb_ hiển thị trên host

Có thể thấy rõ họa tiết đỏ/trắng của cape Mojang Studios chính thức. Không có khác biệt nào so với Jeb_ thật đang đeo cape của chính mình -- client tải chính xác cùng một texture từ textures.minecraft.net.

Và trong góc nhìn nhập vai, trong một phiên chơi thực tế:

Cape Mod -- góc nhìn trong game với cape hiển thị

Cape bay phía sau người chơi, đung đưa theo chuyển động. Hoàn toàn không thể phân biệt với một skin xác thực có cape chính thức.

Góc khác, trong một thế giới với dung nham và địa hình:

Cape Mod -- cape trong môi trường tự nhiên

Và một góc nhìn cận cảnh khác từ gameplay thực tế, cho thấy cape đang hoạt động:

Cape Mod -- cape trong gameplay Minecraft cổ điển

Đối với người tham gia LAN mà không biết host có mod, hoàn toàn không có cách nào phân biệt điều này với cape Mojang thật. Đó chính xác là vấn đề: chữ ký hợp lệ, client không có lý do gì để nghi ngờ.

Tại sao đây là lỗ hổng (và tại sao nó không phải)

Trớ trêu thay: exploit hoạt động chính xác bởi vì chữ ký hợp lệ. Không có sự phá vỡ mật mã nào ở đây -- tệ hơn, đó là một lỗ hổng logic trong mô hình tin cậy.

Kiểm tra Kết quả
Tính hợp lệ của chữ ký RSA ✅ Hợp lệ (ký bởi Mojang cho Jeb_)
profileId trong payload có khớp với UUID của host không? ❌ Không (UUID của Jeb_ ≠ UUID của host)
Client có kiểm tra sự tương ứng này không? ❌ Không. Chỉ chữ ký RSA được xác minh.

Minecraft tin tưởng chữ ký, không phải danh tính của người mang nó. Miễn là chữ ký đến từ Mojang, client chấp nhận nó. Giống như đưa ra một hộ chiếu giả được chính phủ ký -- con dấu hợp lệ, mặc dù hộ chiếu không phải của bạn.

Tác động bảo mật

Phạm vi giới hạn ở LAN

Mod chỉ hoạt động trên server tích hợp (LAN). Kẻ tấn công phải:

  • Có mod Fabric được cài đặt
  • Là host của một thế giới LAN
  • Bạn bè kết nối mà không cần mod (vanilla)

Nhưng khả năng có thể mở rộng

Về lý thuyết, với kỹ thuật tương tự, ta có thể:

  • Chèn lại dữ liệu đã ký khác: heads, enchantments bất hợp pháp, thành phần chat độc hại
  • Kết hợp với tunnel LAN (NGROK, playit.gg, Radmin VPN) để ảnh hưởng đến người chơi trên internet
  • Mở rộng sang các thuộc tính khác của hồ sơ phụ thuộc vào chữ ký

Tại sao Mojang có thể sẽ không vá

Không có "lỗ hổng" theo nghĩa chặt chẽ -- chữ ký hợp lệ. Việc vá lỗi này sẽ yêu cầu Mojang thay đổi toàn bộ mô hình xác thực, điều rất phức tạp. Hiện tại, đây là một edge case: người chơi LAN được cho là tin tưởng nhau.

Cái bẫy triết học

Cape Mod là một proof of concept tuyệt vời cho một chân lý rộng hơn: bạn không bao giờ được tin tưởng một chữ ký mà không kiểm tra ai đã ký nó và cho mục đích gì.

Đây là một bài học về mật mã cơ bản. RSA ký một thông điệp, không phải một danh tính. Nếu tôi đưa bạn một chữ ký RSA hợp lệ từ Mojang, bạn biết Mojang đã ký một cái gì đó. Bạn không biết cho ai, và bạn không thể giả định điều đó chỉ bằng cách nhìn vào thông điệp.

Đây chính xác là những gì đã xảy ra với chứng chỉ SSL/TLS vào những năm 2000 khi các CA chấp nhận bất cứ thứ gì -- chữ ký hợp lệ, nhưng nó được áp dụng cho tên miền sai.

Kết luận

Cape Mod không phải là hack theo nghĩa cổ điển -- đó là sự khai thác tinh tế của việc thiếu kiểm tra logic trong Minecraft. Nó cho thấy rằng:

  1. Một chữ ký hợp lệ không đảm bảo danh tính của người mang nó
  2. Trong LAN, sự tin cậy yếu hơn người ta vẫn nghĩ
  3. Các thuộc tính textures của Minecraft về cơ bản là nội dung được chèn -- cần kiểm tra chúng khớp với người chơi mang chúng

Nếu bạn tham gia một thế giới LAN trên server "không rõ" (hay đúng hơn, host có mod đáng ngờ), bạn đã có vấn đề bảo mật trước cả khi nói đến cape. Nhưng điều này mang tính triệu chứng: Minecraft giả định mọi người trong LAN tin tưởng nhau. Điều đó đúng... cho đến khi nó không còn đúng nữa.


Tài nguyên

3 điểm chính

  1. Chữ ký RSA xác thực một thông điệp, không phải danh tính -- một chi tiết đã gây tổn thất cho nhiều hệ thống.
  2. Minecraft không kiểm tra hồ sơ người chơi có khớp với chữ ký nhận được hay không -- một lỗ hổng logic, không phải mật mã.
  3. Trong LAN hoặc tunnel, mọi thứ đều cởi mở cho một mod kiểm soát server tích hợp.

Cape Mod : วิธีขโมยเคปของ Jeb_ ด้วยการฉีดลายเซ็น RSA

ม็อด Fabric ที่ใช้ประโยชน์จากช่องโหว่เชิงตรรกะในระบบความเชื่อถือของ Minecraft : ลายเซ็น RSA ที่ถูกต้องของ Mojang แต่ถูกนำมาใช้ซ้ำกับบัญชีผิด คำอธิบายโค้ด, ผลกระทบด้านความปลอดภัย และบทเรียนด้านการเข้ารหัส

Cape Mod : วิธีขโมยเคปของ Jeb_ ด้วยการฉีดลายเซ็น RSA

alt text จะเกิดอะไรขึ้นถ้าบอกว่า แค่มีลายเซ็น RSA ที่ถูกต้อง -- แต่สำหรับบัญชีผิด -- ก็ทำให้เพื่อนคุณเชื่อว่าคุณสวมเคปทางการของ Mojang ได้แล้ว ยินดีต้อนรับสู่ cape-mod เอ็กซ์พลอยต์ Fabric ที่แสดงให้เห็นว่า Minecraft เชื่อถือลายเซ็นโดยไม่ตรวจสอบว่าโปรไฟล์ที่เป็นเจ้าของลายเซ็นนั้นเป็นของคุณจริงหรือไม่

บริบท : Minecraft จัดการสกินและเคปอย่างไร

ใน Java Edition มีคำถามที่เราไม่ค่อยได้ถามกัน : ใครเป็นผู้รับผิดชอบในการแสดงสกินและเคปของผู้เล่น -- ไคลเอ็นต์หรือเซิร์ฟเวอร์?

คำตอบมีรายละเอียดปลีกย่อย :

ส่วนประกอบ ใครส่ง? ใครดาวน์โหลด?
Texture สกิน เซิร์ฟเวอร์ส่ง URL ที่เซ็นชื่อแล้ว ไคลเอ็นต์ดาวน์โหลดจาก textures.minecraft.net
Texture เคป เซิร์ฟเวอร์ส่ง URL ที่เซ็นชื่อแล้ว ไคลเอ็นต์ดาวน์โหลดจาก textures.minecraft.net
พร็อพเพอร์ตี้ textures เซิร์ฟเวอร์ส่ง GameProfile จากการยืนยันตัวตนของ Mojang ไคลเอ็นต์ตรวจสอบลายเซ็น RSA

จุดสำคัญ : ทุกอย่างอยู่ในพร็อพเพอร์ตี้ที่ชื่อ textures ของ GameProfile พร็อพเพอร์ตี้นี้ประกอบด้วย :

  • Payload JSON ในรูปแบบ base64 ที่มี URL ของ texture ต่าง ๆ
  • ลายเซ็น RSA ที่สร้างด้วยคีย์ส่วนตัวของ Mojang

กำแพงลายเซ็น RSA

พร็อพเพอร์ตี้ textures แต่ละอันเมื่อถอดรหัสจะมีลักษณะแบบนี้ :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

ไคลเอ็นต์ตรวจสอบลายเซ็น RSA เทียบกับคีย์สาธารณะที่ฝังอยู่ใน jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

สำหรับผู้เล่นระยะไกล (ไม่ใช่ในเครื่อง) ไคลเอ็นต์จะยอมรับเฉพาะสกินที่ถูกทำเครื่องหมายว่า secure -- นั่นคือมีลายเซ็นที่ถูกต้อง :

// SkinManager.createLookup() -- แบบย่อ
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← ผู้เล่นระยะไกลต้องมีความปลอดภัย
    .orElse(defaultSkin);

การตรวจสอบนี้ป้องกันการปลอมแปลงในทางทฤษฎี แต่นี่คือจุดที่เริ่มน่าสนใจ

ช่องโหว่ : การนำลายเซ็นมาใช้ซ้ำ (signature replay)

ไคลเอ็นต์ตรวจสอบว่าลายเซ็น RSA ถูกต้อง แต่มันไม่เคยตรวจสอบว่า profileId ที่อยู่ใน JSON ตรงกับ UUID จริงของผู้เล่น

กล่าวอีกนัยหนึ่ง : พร็อพเพอร์ตี้ textures ที่นำมาจากบัญชี Mojang ที่มีอยู่จริง (เช่นของพนักงาน Mojang) สามารถนำมาเล่นซ้ำกับผู้เล่นคนอื่นได้ ลายเซ็นยังคงถูกต้อง -- มันถูกสร้างขึ้นโดย Mojang จริง ๆ -- แค่มาจากอีกบัญชีหนึ่ง

จะแยกลายเซ็นจริงออกมาได้อย่างไร?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) มีเคป Mojang Studios จากเซิร์ฟเวอร์เซสชันของ Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

ผลลัพธ์ :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature ของฟิลด์ value นี้ถูกสร้างขึ้นโดย Mojang เป็นลายเซ็น RSA-2048 SHA-1 ที่ถูกต้องอย่างสมบูรณ์ แม้คุณจะนำไปเล่นซ้ำกับ UUID อื่น -- เพราะลายเซ็นของ Jeb_ ก็ยังคงเป็นลายเซ็นของ Jeb_ และไคลเอ็นต์ไม่เคยตรวจสอบว่ามันควรจะเป็นของคุณ

โค้ด : ม็อดทำงานอย่างไร

ม็อด cape-mod มีขนาดเล็กมาก -- แค่ 65 บรรทัดของ Java นี่คือหัวใจสำคัญ :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // แทนที่พร็อพเพอร์ตี้ textures ด้วยของ Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

ขั้นตอน :

  1. Mixin บน Player.getGameProfile() -- จุดที่โปรไฟล์ของผู้เล่นถูกส่งกลับ
  2. ตรวจสอบว่าเป็นเซิร์ฟเวอร์ภายในเครื่อง (Integrated Server)
  3. ตรวจสอบว่าเป็นโฮสต์ (โลก LAN)
  4. แทนที่ พร็อพเพอร์ตี้ textures ด้วยของ Jeb_ (แบบ hardcode)
  5. ส่งคืน GameProfile ใหม่ที่มี texture ที่ถูกฉีดเข้าไป

GameProfile จึงถูกปลอมแปลงขึ้นมา : เป็นโปรไฟล์ที่สร้างขึ้นโดยไม่ได้ตรงกับผู้เล่นจริง พร็อพเพอร์ตี้ textures ถูกนำมาเล่นซ้ำจาก Jeb_ -- ลายเซ็น RSA เป็นของแท้ แต่ถูกใช้กับโปรไฟล์ผิด แพ็กเก็ตเครือข่ายนั้นถูกต้องตามกฎหมาย : เซิร์ฟเวอร์ส่ง ClientboundPlayerInfoUpdatePacket ตามปกติกับโปรไฟล์ที่ถูกแก้ไขนี้ สิ่งที่ถูกปลอมคือโปรไฟล์ ไม่ใช่แพ็กเก็ต

เมื่อเพื่อนของโฮสต์เข้าร่วมผ่าน LAN พวกเขาจะได้รับ ClientboundPlayerInfoUpdatePacket พร้อมกับโปรไฟล์ที่ถูกแก้ไข ไคลเอ็นต์ :

  1. ถอดรหัส payload base64
  2. ตรวจสอบลายเซ็น RSA → ✅ ถูกต้อง (เป็นของ Jeb_ จริง ๆ)
  3. ทำเครื่องหมายสกินว่า secure=true (เพราะลายเซ็นถูกต้อง)
  4. ผ่านตัวกรอง !isRemote || ps.secure() → ✅ ผ่าน
  5. ดาวน์โหลดและแสดงเคปของ Jeb_

ผลลัพธ์ในเกม : เคปบนสกินของคุณ

นี่คือสิ่งที่เห็นในเกม เริ่มจากมุมมองด้านหน้าพร้อมเคปของ Jeb_ ที่แสดงบนโฮสต์ :

Cape Mod -- เคป Jeb_ แสดงบนโฮสต์

เราจะเห็นลวดลายสีแดง/ขาวของเคปทางการ Mojang Studios อย่างชัดเจน ไม่มีความแตกต่างจาก Jeb_ ตัวจริงที่สวมเคปของตัวเอง -- ไคลเอ็นต์ดาวน์โหลด texture เดียวกันจาก textures.minecraft.net

และในมุมมองแบบดื่มด่ำ ในเกมจริง :

Cape Mod -- มุมมองในเกมพร้อมเคปที่มองเห็น

เคปปลิวไปด้านหลังผู้เล่น ขยับตามการเคลื่อนไหว แยกไม่ออกจากสกินจริงที่มีเคปทางการ

อีกมุม ในโลกที่มีลาวาและภูมิประเทศ :

Cape Mod -- เคปในสภาพแวดล้อมธรรมชาติ

และมุมใกล้ชิดสุดท้ายของการเล่นจริง ที่เห็นเคปในขณะเล่น :

Cape Mod -- เคปในการเล่น Minecraft ทั่วไป

สำหรับคนที่เข้าร่วม LAN โดยไม่รู้ว่าโฮสต์ใช้ม็อดอยู่ ไม่มีทางแยกแยะได้เลยว่านี่คือเคป Mojang ปลอม นี่คือประเด็น : ลายเซ็นถูกต้อง ไคลเอ็นต์ไม่มีเหตุผลที่จะสงสัย

ทำไมนี่ถึงเป็นช่องโหว่ (และทำไมถึงไม่ใช่)

เป็นเรื่องน่าขัน : เอ็กซ์พลอยต์ทำงานได้เพราะลายเซ็นถูกต้อง ไม่มีการบายพาสการเข้ารหัสลับใด ๆ ที่นี่ -- ที่แย่กว่านั้นคือมันเป็นช่องโหว่เชิงตรรกะ ในโมเดลความเชื่อถือ

การตรวจสอบ ผลลัพธ์
ความถูกต้องของลายเซ็น RSA ✅ ถูกต้อง (เซ็นโดย Mojang สำหรับ Jeb_)
profileId ใน payload ตรงกับ UUID ของโฮสต์หรือไม่? ❌ ไม่ (UUID ของ Jeb_ ≠ UUID ของโฮสต์)
ไคลเอ็นต์ตรวจสอบความสอดคล้องนี้หรือไม่? ❌ ไม่ เฉพาะลายเซ็น RSA เท่านั้นที่ถูกตรวจสอบ

Minecraft เชื่อถือลายเซ็น ไม่ใช่ตัวตนของผู้ที่ถือลายเซ็นนั้น ตราบใดที่ลายเซ็นมาจาก Mojang ไคลเอ็นต์ก็ยอมรับ มันเหมือนกับการแสดงพาสปอร์ตปลอมที่เซ็นโดยรัฐบาล -- ตราประทับถูกต้อง แม้พาสปอร์ตจะไม่ใช่ของคุณ

ผลกระทบด้านความปลอดภัย

ขอบเขตจำกัดที่ LAN

ม็อดทำงานได้เฉพาะบนเซิร์ฟเวอร์แบบบูรณาการ (LAN) ผู้โจมตีต้อง :

  • มีม็อด Fabric ติดตั้งอยู่
  • เป็นโฮสต์ของโลก LAN
  • เพื่อน ๆ เชื่อมต่อโดยไม่มีม็อด (vanilla)

แต่ความเป็นไปได้ขยายวงกว้างขึ้น

ในทางเทคนิคแล้ว ด้วยเทคนิคเดียวกัน สามารถ :

  • นำข้อมูลที่เซ็นแล้วกลับมาฉีดใหม่ : หัว, เอ็นแชนต์ที่ผิดกฎหมาย, ส่วนประกอบแชทที่เป็นอันตราย
  • รวมกับอุโมงค์ LAN (NGROK, playit.gg, Radmin VPN) เพื่อส่งผลต่อผู้เล่นบนอินเทอร์เน็ต
  • ขยายไปยังพร็อพเพอร์ตี้อื่น ๆ ของโปรไฟล์ที่ขึ้นอยู่กับลายเซ็น

ทำไม Mojang อาจจะไม่แพตช์

ไม่มี "ช่องโหว่" ในความหมายที่เคร่งครัด -- ลายเซ็นถูกต้อง การแพตช์สิ่งนี้จะต้องการให้ Mojang เปลี่ยนโมเดลการยืนยันตัวตนทั้งหมด ซึ่งเป็นเรื่องซับซ้อน ในตอนนี้มันเป็นกรณีขอบ : ผู้เล่น LAN ถูกสันนิษฐานว่าไว้ใจซึ่งกันและกัน

กับดักเชิงปรัชญา

Cape Mod เป็นการพิสูจน์แนวคิดที่ยอดเยี่ยมของความจริงที่กว้างกว่า : คุณไม่ควรเชื่อถือลายเซ็นโดยไม่ตรวจสอบว่าใครเป็นคนเซ็นและเกี่ยวกับอะไร

นี่คือบทเรียนพื้นฐานด้านการเข้ารหัส RSA เซ็นข้อความ ไม่ใช่ตัวตน ถ้าฉันให้ลายเซ็น RSA ที่ถูกต้องของ Mojang แก่คุณ คุณก็รู้ว่า Mojang เซ็นบางอย่าง คุณไม่รู้ว่าสำหรับใคร และคุณไม่สามารถสันนิษฐานได้เพียงแค่มองข้อความ

นี่คือสิ่งที่เกิดขึ้นกับใบรับรอง SSL/TLS ในช่วงปี 2000 เมื่อ CA รับรองอะไรก็ได้ -- ลายเซ็นถูกต้อง แต่มันใช้กับโดเมนผิด

บทสรุป

Cape Mod ไม่ใช่แฮ็กในความหมายคลาสสิก -- มันเป็นการใช้ประโยชน์อย่างสวยงามจากการขาดการตรวจสอบเชิงตรรกะใน Minecraft มันแสดงให้เห็นว่า :

  1. ลายเซ็นที่ถูกต้องไม่ได้รับประกันตัวตนของผู้ถือมัน
  2. ใน LAN ความเชื่อถือนั้นอ่อนแอกว่าที่เราคิด
  3. พร็อพเพอร์ตี้ textures ของ Minecraft โดยเนื้อแท้แล้วคือเนื้อหาที่ถูกฉีด -- จำเป็นต้องตรวจสอบว่ามันตรงกับผู้เล่นที่สวมใส่หรือไม่

ถ้าคุณเข้าร่วมโลก LAN บนเซิร์ฟเวอร์ "ที่ไม่รู้จัก" (หรือโฮสต์ที่มีม็อดน่าสงสัย) คุณมีปัญหาด้านความปลอดภัยตั้งแต่ก่อนเคปอยู่แล้ว แต่มันเป็นอาการ : Minecraft สมมติว่าทุกคนบน LAN ไว้ใจซึ่งกันและกัน ซึ่งเป็นจริง... จนกว่าจะไม่เป็น


แหล่งข้อมูล

3 ประเด็นสำคัญ

  1. ลายเซ็น RSA รับรองข้อความ ไม่ใช่ตัวตน -- รายละเอียดที่ทำให้หลายระบบต้องเสียหาย
  2. Minecraft ไม่ตรวจสอบว่าโปรไฟล์ผู้เล่นตรงกับลายเซ็นที่ได้รับ -- ช่องโหว่เชิงตรรกะ ไม่ใช่เชิงการเข้ารหัส
  3. ใน LAN หรือในอุโมงค์ ทุกอย่างเปิดกว้างสำหรับม็อดที่ควบคุมเซิร์ฟเวอร์แบบบูรณาการ

Related Articles