Skip to content

Commit

Permalink
We are now releasing pre-release 5 for Minecraft 1.19.1. This pre-rel…
Browse files Browse the repository at this point in the history
…ease includes the remaining fixes for a known exploit regarding player report context
  • Loading branch information
0xAda committed Jul 27, 2022
1 parent e977a20 commit 2055c0a
Show file tree
Hide file tree
Showing 14 changed files with 82 additions and 317 deletions.
18 changes: 16 additions & 2 deletions README.MD
@@ -1,4 +1,18 @@
# Gaslight
### (working as of 1.19.1)

1.19.1-pre4/5 was a patch to the initial version of Gaslight. Not long before their announcement banning all blockchain technologies from the game, Mojang themselves added a blockchain to the chat system, this clusterfuck of a system had us stumped, not because it fixed Gaslight, but because it doesn't make any sense.

Mojang's fix was to add a chat chain system, this is beyond the scope of a youtube/github description, but you can read in depth about it [here](https://gist.github.com/kennytv/ed783dd244ca0321bbd882c347892874), but the important part is that it stops you adding messages after the fact. You can't remove messages anymore, because the last seen field would show messages that no longer exist, and you can't add messages because it would invalidate the signatures of messages surrounding it as it would not be included in the last seen field.

The workaround Gaslight uses is private messages. In order to keep the chain consistent for every player, Mojang added a new packet that sends only the signature of a message for any message a player cannot see. Mojang, however, forgot the one thing that got them in this mess in the first place, you can't trust the client. With just the chat chain, how do they know if you actually received a message, or if it was a private message that you couldn't see? Every message you can see on your client will appear to Mojang as if it was sent in public chat.

Utilising private messages also gives us the ability to remove messages. With some exceptions, Mojang has no way of knowing if you received the full message, or just the header, therefore we can remove the body of messages, leaving it as just a header and making it identical to an equivalent private message.

By sending a private message to yourself with the desired context, then sending a message in public chat expecting a particular response, you can have context that is only visible to you and Mojang, and you can "remove" the message you sent in public chat, because Mojang has no way of knowing if you received the full message, or just the header. This is slightly harder to use than the previous iteration of Gaslight, but overall, the functionality is the same.

# Original Exploit

[https://www.youtube.com/watch?v=uyqi-CzB8Dg](https://www.youtube.com/watch?v=uyqi-CzB8Dg)

1.19.1-rc1 changed how abuse reports were sent to Mojang, context of other messages is now included. Mojang does not verify that these messages were actually sent to the server, or even that the players were on the server at the time. You can modify the request sent to Mojang to fake context around the message, messages can be signed as if they were sent in the past to make innocent messages look malicious, especially if you have keys for multiple accounts.
Expand All @@ -11,6 +25,6 @@

## Fake Conversation, Reported to Mojang

[12:59] player 3: are you breaking mojang's terms of service?
[12:59] player 1: are you breaking mojang's terms of service?

[13:00] player 2: of course!
[13:00] player 2: of course!
6 changes: 3 additions & 3 deletions gradle.properties
Expand Up @@ -2,13 +2,13 @@
org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=1.19.1-pre3
yarn_mappings=1.19.1-pre3+build.1
minecraft_version=1.19.1
yarn_mappings=1.19.1+build.1
loader_version=0.14.8
# Mod Properties
mod_version=1.0-SNAPSHOT
maven_group=gg.nodus
archives_base_name=gaslight
# Dependencies
# check this on https://modmuss50.me/fabric.html
fabric_version=0.57.0+1.19
fabric_version=0.58.4+1.19.1
2 changes: 1 addition & 1 deletion src/main/java/gg/nodus/gaslight/Gaslight.java
Expand Up @@ -6,7 +6,7 @@
import java.util.Set;

public class Gaslight implements ModInitializer {
public static final Set<Integer> removedIndexes = new HashSet<>();
public static final Set<String> removedMessages = new HashSet<>();

@Override
public void onInitialize() {
Expand Down
6 changes: 0 additions & 6 deletions src/main/java/gg/nodus/gaslight/TimestampCalculationHack.java

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Expand Up @@ -3,87 +3,26 @@
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.report.ChatSelectionScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.PressableTextWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.client.network.message.MessageTrustStatus;
import net.minecraft.client.report.AbuseReportContext;
import net.minecraft.client.report.ReceivedMessage;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.network.encryption.NetworkEncryptionUtils;
import net.minecraft.network.message.ChatMessageSigner;
import net.minecraft.network.message.MessageSignature;
import net.minecraft.network.message.SignedMessage;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

@Mixin(ChatSelectionScreen.class)
public abstract class MixinChatSelectionScreen extends Screen {

@Shadow
@Final
private AbuseReportContext reporter;

@Shadow
@Nullable
private ChatSelectionScreen.SelectionListWidget selectionList;
private TextFieldWidget newMessageField;

protected MixinChatSelectionScreen(Text title) {
super(title);
}

@Inject(method = "init", at = @At("HEAD"))
public void init(CallbackInfo ci) {
var width = (MinecraftClient.getInstance().getWindow().getScaledWidth() - MinecraftClient.getInstance().textRenderer.getWidth(Text.translatable("gui.chatSelection.title"))) / 2 - 20;
newMessageField = new TextFieldWidget(this.textRenderer, 4, 10, width - 32, 20, Text.of(""));
newMessageField.setMaxLength(256);
newMessageField.setTextFieldFocused(true);
this.addSelectableChild(newMessageField);
this.addDrawableChild(new ButtonWidget(width - 25, 10, 30, 20, Text.of("Add"), (button) -> {
if (newMessageField.getText().isBlank()) {
return;
}
var signer = new ChatMessageSigner(MinecraftClient.getInstance().player.getUuid(), Instant.now().minus(ChronoUnit.MINUTES.getDuration()), NetworkEncryptionUtils.SecureRandomUtil.nextLong());
var otherSigner = MinecraftClient.getInstance().getProfileKeys().getSigner();
try {
var signature = signer.sign(otherSigner, Text.of(newMessageField.getText()));
var message = new ReceivedMessage.ChatMessage(
MinecraftClient.getInstance().player.getGameProfile(),
MinecraftClient.getInstance().player.getName(),
new SignedMessage(Text.of(newMessageField.getText()),
new MessageSignature(
MinecraftClient.getInstance().player.getUuid(),
Instant.now(),
new NetworkEncryptionUtils.SignatureData(signature.saltSignature().salt(), signature.saltSignature().signature())
),
Optional.empty()
),
MessageTrustStatus.SECURE
);
this.selectionList.addMessage(0, message);
this.reporter.chatLog().add(message);
var parent = ((AccessorChatReportScreen) ((AccessorMixinChatSelectionScreen) MinecraftClient.getInstance().currentScreen).getParent());
MinecraftClient.getInstance().setScreen(new ChatSelectionScreen((Screen) parent, parent.getAbuseReporter(), parent.getReport(), (report) -> {
parent.setReport(report);
parent.onChange();
}));
} catch (Exception e) {
e.printStackTrace();
}
}));
this.addDrawableChild(
new PressableTextWidget(2, this.height - 10, MinecraftClient.getInstance().textRenderer.getWidth("Gaslight - nodus.gg"), 10, Text.of("§aGaslight - nodus.gg"), (button) -> {
try {
Expand All @@ -94,9 +33,4 @@ public void init(CallbackInfo ci) {
}, this.textRenderer));
}

@Inject(method = "render", at = @At("TAIL"))
public void render(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo ci) {
newMessageField.render(matrices, mouseX, mouseY, delta);
}

}
90 changes: 16 additions & 74 deletions src/main/java/gg/nodus/gaslight/mixin/MixinMessageEntry.java
Expand Up @@ -4,11 +4,10 @@
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.report.ChatSelectionScreen;
import net.minecraft.client.report.ChatLogImpl;
import net.minecraft.client.report.ReceivedMessage;
import net.minecraft.client.sound.PositionedSoundInstance;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.sound.SoundEvents;
import net.minecraft.text.StringVisitable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
Expand All @@ -25,10 +24,10 @@ public abstract class MixinMessageEntry {
@Shadow
private boolean fromReportedPlayer;

private int buttonHovered = -1;
@Shadow
@Final
private int index;
private int buttonHovered = -1;
private StringVisitable truncatedContent;

@Shadow
public abstract boolean isSelected();
Expand All @@ -38,7 +37,7 @@ public abstract class MixinMessageEntry {

@ModifyArg(method = "render", index = 5, at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawableHelper;drawWithShadow(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/OrderedText;III)V"))
public int redirect(int x) {
if (Gaslight.removedIndexes.contains(index)) {
if (Gaslight.removedMessages.contains(this.truncatedContent.getString())) {
return 0xFFFF0000;
}
if (fromReportedPlayer) {
Expand All @@ -50,91 +49,34 @@ public int redirect(int x) {
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
public void mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
if (button == 0) {
switch (buttonHovered) {
case 0 -> {
if (index == 0) {
return;
}
MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));

swapMessages(index, index - 1);
refreshScreen();
cir.setReturnValue(true);
if (buttonHovered == 2) {
if (!Gaslight.removedMessages.remove(this.truncatedContent.getString())) {
System.out.println(this.truncatedContent.getString());
Gaslight.removedMessages.add(this.truncatedContent.getString());
}
case 1 -> {
var chatLog = (AccessorChatLogImplMixin) ((AccessorMixinChatSelectionScreen) MinecraftClient.getInstance().currentScreen).getReporter().chatLog();
if (index == ((ChatLogImpl) chatLog).getMaxIndex()) {
return;
}
MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));

swapMessages(index, index + 1);
refreshScreen();
cir.setReturnValue(true);
if (this.isSelected()) {
this.toggle();
}
case 2 -> {
if (!Gaslight.removedIndexes.remove(index)) {
Gaslight.removedIndexes.add(index);
}
if (this.isSelected()) {
this.toggle();
}
MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
cir.setReturnValue(true);
} else {
if (Gaslight.removedMessages.contains(this.truncatedContent.getString())) {
cir.setReturnValue(true);
}
default -> {
if (Gaslight.removedIndexes.contains(index)) {
cir.setReturnValue(true);
}
}
}
}
}

private void swapMessages(int index, int index2) {
var chatLog = (AccessorChatLogImplMixin) ((AccessorMixinChatSelectionScreen) MinecraftClient.getInstance().currentScreen).getReporter().chatLog();
var message = chatLog.getMessages()[index];
chatLog.getMessages()[index] = chatLog.getMessages()[index2];
chatLog.getMessages()[index2] = message;

if (Gaslight.removedIndexes.contains(index) && !Gaslight.removedIndexes.contains(index2)) {
Gaslight.removedIndexes.remove(index);
Gaslight.removedIndexes.add(index2);
} else if (Gaslight.removedIndexes.contains(index2)) {
Gaslight.removedIndexes.remove(index2);
Gaslight.removedIndexes.add(index);
}
}

private void refreshScreen() {
var parent = ((AccessorChatReportScreen) ((AccessorMixinChatSelectionScreen) MinecraftClient.getInstance().currentScreen).getParent());
MinecraftClient.getInstance().setScreen(new ChatSelectionScreen((Screen) parent, parent.getAbuseReporter(), parent.getReport(), (report) -> {
parent.setReport(report);
parent.onChange();
}));
}

@Inject(method = "render", at = @At("TAIL"))
public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta, CallbackInfo ci) {
if (mouseY > y + entryHeight || y > mouseY) {
return;
}

boolean canReorder = ((AccessorMixinChatSelectionScreen) MinecraftClient.getInstance().currentScreen).getReporter().chatLog().get(this.index).isSentFrom(MinecraftClient.getInstance().player.getUuid());
int moveUpX = x + entryWidth;
drawButton(matrices, moveUpX, y, entryHeight, "↑", canReorder);

int moveDownX = x + entryWidth + entryHeight + 3;
drawButton(matrices, moveDownX, y, entryHeight, "↓", canReorder);

int deleteX = x + entryWidth + entryHeight * 2 + 6;
int deleteX = x + entryWidth + 2;
drawButton(matrices, deleteX, y, entryHeight, "X", true);

if (isMouseOver(moveUpX, y, entryHeight, mouseX, mouseY) && canReorder) {
buttonHovered = 0;
} else if (isMouseOver(moveDownX, y, entryHeight, mouseX, mouseY) && canReorder) {
buttonHovered = 1;
} else if (isMouseOver(deleteX, y, entryHeight, mouseX, mouseY)) {
if (isMouseOver(deleteX, y, entryHeight, mouseX, mouseY)) {
buttonHovered = 2;
} else {
buttonHovered = -1;
Expand Down
@@ -1,7 +1,8 @@
package gg.nodus.gaslight.mixin;

import net.minecraft.client.report.MessagesListAdder;
import net.minecraft.client.report.ReceivedMessage;
import net.minecraft.client.report.log.ChatLog;
import net.minecraft.client.report.log.ReceivedMessage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
Expand All @@ -10,12 +11,12 @@
import java.util.List;

@Mixin(MessagesListAdder.class)
public class MixinMessagesListAdder {
public class MixinMessagesListAdder<T extends ReceivedMessage> {

@Inject(method = "addContextMessages", at = @At("HEAD"), cancellable = true)
private static void addContextMessages(List<ReceivedMessage.IndexedMessage> messages, MessagesListAdder.MessagesList messagesList, CallbackInfoReturnable<Integer> cir) {
messagesList.addMessages(messages);
cir.setReturnValue(messages.size());
private void addContextMessages(List<ChatLog.IndexedEntry<T>> list, MessagesListAdder.MessagesList<T> messagesList, CallbackInfoReturnable<Integer> cir) {
messagesList.addMessages(list);
cir.setReturnValue(list.size());
}

}
Expand Up @@ -41,7 +41,6 @@ public void getEntryAtPosition(double x, double y, CallbackInfoReturnable<E> cir
int n = m / this.itemHeight;
cir.setReturnValue(n >= 0 && m >= 0 && n < this.getEntryCount() ? this.children().get(n) : null);
}

}

}

0 comments on commit 2055c0a

Please sign in to comment.