ret2plt | ret2plt Research Blog

Local Vulnerabilities in VTube Studio

Introduction

VTube Studio is a freeware program that allows people to VTube. Essentially if you have an RTX card or if you have an iPhone, you hook it up to your PC and you can then control a VTuber Avatar. An example of a VTuber Avatar is seen below.

Raki Kazuki VTuber Model Reference VTube Studio has a large attack surface, both local and remote. Today we’ll be exploring the local vulnerabilities I found in VTS.

Note that all vulnerabilities have been discussed and disclosed to denchi, one requires a malicious collab participant, two require an executable already running on your computer, in which case you can be considered “already compromised”.

Collab Model Ripping

Scenario: a malicious participant in a VTuber collab can rip the models of all other participants, unencrypted.

Access to model files is not possible unless you have been invited to the collab and have thnen session password.

The process, in a simple way, would be as such:

It’s a bit more technically complicated than that. There’s an indicator that can be used akin to egg-finder shellcode to find the SHA256sums of the models in memory. From there, an experienced reverse engineer can find the offsets to other function pointers which will assist in finding the model files. From there, they can be carved out.

It’s also worth noting that VTube Studio’s Assembly-CSharp.dll can be hooked at runtime to bypass all of this, which will also give you the decryption keys. No minidump needs to be taken, nothing. Eventually, I plan to write a PoC for this.

Given the worth of VTuber models this is relatively severe but only affects you if you have a malicioius actor in your collab.

A “vaccine” will be released on my Patreon at https://patreon.com/c/impost0r. Note that this, until enough people buy it and I can afford a code signing key, will break compatibility with some anticheat software, namely Riot Game’s Vanguard (you will have to set the test-signing paramater to on in bcdedit).

VTube Studio Plugin Permissions Bypass

Scenario: You have installed a new VTube Studio plugin and it is able to bypass permission checks for risky API calls, potentially doing something malicious within VTube Studio.

Note that if you have installed a malicious VTube Studio plugin, they are not limited to interfering with the API. A malicious actor may drop a trojan, stealer, miner, and/or escalate to NT AUTHORITY/SYSTEM alongside this. Please vet your plugins carefully!

From reverse engineering how .vtsauth files are created (VTube Studio’s interal authenticator that permission has been granted for risky API access) I’ve discovered it’s possible to reimplement their code to forge a .vtsauth file for your plugin before accessing the risky endpoint. .vtsauth files contain metadata about the plugin and are encrypted with AES-256 with a static 16-byte key and salt. These can either be changed at runtime, to change what it is comparing to, or it can be forged outright and placed in the correct directory

The reference code for how VTube Studio handles .vtsauth files is seen here:

	private void CreateOrDeletePluginAuthFile(PluginEntry plugin, bool createNewAuth)
	{
		VTubeStudioAPI.APIDebug("Scanning for existing tokens for plugin \"" + plugin.Name + "\".", false);
		EncryptionHelper.SimpleAes simpleAes = new EncryptionHelper.SimpleAes();
		string pluginsFolderPath = IOHelper.GetPluginsFolderPath();
		foreach (PluginEntry pluginEntry in Executor_AuthenticationTokenRequest.GetAllPluginEntriesAndDeleteInvalidOnes(null))
		{
			if (Executor_AuthenticationTokenRequest.checkIfSameNameAndDeveloper(pluginEntry, plugin))
			{
				VTubeStudioAPI.APIDebug("Removing old authentication data for plugin \"" + plugin.Name + "\". Old token was invalidated.", false);
				IOHelper.DeleteIfExists(Path.Combine(pluginsFolderPath, pluginEntry.ID + ".vtsauth"));
			}
		}
		if (createNewAuth)
		{
			string unencrypted = JsonUtility.ToJson(plugin);
			string text = simpleAes.Encrypt(unencrypted);
			IOHelper.WriteTextFile(Path.Combine(pluginsFolderPath, plugin.ID + ".vtsauth"), text);
			VTubeStudioAPI.APIDebug(string.Concat(new string[]
			{
				"Authenticated plugin \"",
				plugin.Name,
				"\" by developer \"",
				plugin.Developer,
				"\". Returning token."
			}), false);
		}
		simpleAes.Dispose();
		APIPermissionData.ReloadPermissionsFromDisk();
		SingletonMonoBehaviour<CustomDataItemWhitelistManager>.Instance().ClearWhitelistForPlugin(plugin.Name, plugin.Developer);
	}

And the C# implementation for simpleAes is as follows:

using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

// Token: 0x02000195 RID: 405
public class EncryptionHelper : MonoBehaviour
{
	// Token: 0x02000196 RID: 406
	public class SimpleAes : IDisposable
	{
		// Token: 0x060006B0 RID: 1712 RVA: 0x000277AC File Offset: 0x000259AC
		public SimpleAes()
		{
			this._rijndael = new RijndaelManaged
			{
				Key = EncryptionHelper.SimpleAes.Key
			};
			this._rijndael.GenerateIV();
			this._encryptor = this._rijndael.CreateEncryptor();
			this._encoder = new UTF8Encoding();
		}

		// Token: 0x060006B1 RID: 1713 RVA: 0x000277FC File Offset: 0x000259FC
		public string Decrypt(string encrypted)
		{
			if (this.skipEncrypt)
			{
				return encrypted;
			}
			return this._encoder.GetString(this.Decrypt(Convert.FromBase64String(encrypted)));
		}

		// Token: 0x060006B2 RID: 1714 RVA: 0x0002781F File Offset: 0x00025A1F
		public string Encrypt(string unencrypted)
		{
			if (this.skipEncrypt)
			{
				return unencrypted;
			}
			return Convert.ToBase64String(this.Encrypt(this._encoder.GetBytes(unencrypted)));
		}

		// Token: 0x060006B3 RID: 1715 RVA: 0x00027842 File Offset: 0x00025A42
		public void Dispose()
		{
			this._rijndael.Dispose();
			this._encryptor.Dispose();
		}

		// Token: 0x060006B4 RID: 1716 RVA: 0x0002785C File Offset: 0x00025A5C
		private byte[] Decrypt(byte[] buffer)
		{
			byte[] rgbIV = buffer.Take(16).ToArray<byte>();
			byte[] result;
			using (ICryptoTransform cryptoTransform = this._rijndael.CreateDecryptor(this._rijndael.Key, rgbIV))
			{
				result = cryptoTransform.TransformFinalBlock(buffer, 16, buffer.Length - 16);
			}
			return result;
		}

		// Token: 0x060006B5 RID: 1717 RVA: 0x000278BC File Offset: 0x00025ABC
		private byte[] Encrypt(byte[] buffer)
		{
			byte[] second = this._encryptor.TransformFinalBlock(buffer, 0, buffer.Length);
			return this._rijndael.IV.Concat(second).ToArray<byte>();
		}

		// Token: 0x04000936 RID: 2358
		private const int IvBytes = 16;

		// Token: 0x04000938 RID: 2360
		private readonly UTF8Encoding _encoder;

		// Token: 0x04000939 RID: 2361
		private readonly ICryptoTransform _encryptor;

		// Token: 0x0400093A RID: 2362
		private readonly RijndaelManaged _rijndael;

		// Token: 0x0400093B RID: 2363
		private bool skipEncrypt;
	}
}

Some bits have been omitted. This enough, plus the AES key and salt, is enough to forge your own .vtsauth file.

Subshell Powershell Code Execution from Signed App

Again, this requires a malicious plugin to already be installed on your PC. This is also not a vulnerability in the plugins itself, but rather a TOCTOU in a .ps1 file created for when VTS checks your antivirus. The code for checking your antivirus is also never called, so

Scenario: you have installed a malicious plugin and it comes with a bit extra! A UAC bypass Powershell payload and access to the venerable-old WriteProcessMemory() syscall.

Under some scenarios VTube Studio will check your antivirus.

This is the code:

	private void antivirusInstalled()
	{
		if (Application.isEditor)
		{
			Debug.Log("Skipping antivirus check in editor.");
			return;
		}
		try
		{
			string text = Path.Combine(Application.dataPath, "Check_AV", "query_av_name_windows.ps1");
			text = new DirectoryInfo(text).FullName;
			Debug.Log("Running antivirus app query. Script path: \"" + text + "\"");
			string arguments = "-NoProfile -ExecutionPolicy unrestricted -File \"" + text + "\"";
			Process process = new Process();
			process.StartInfo.UseShellExecute = false;
			process.StartInfo.CreateNoWindow = true;
			process.StartInfo.FileName = "powershell.exe";
			process.StartInfo.Arguments = arguments;
			process.StartInfo.RedirectStandardOutput = true;
			process.StartInfo.RedirectStandardError = true;
			string text2 = null;
			string errorString = null;
			process.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs e)
			{
				errorString += e.Data;
			};
			try
			{
				process.Start();
				process.BeginErrorReadLine();
				text2 = process.StandardOutput.ReadToEnd();
				process.WaitForExit();
			}
			catch (Exception ex)
			{
				Debug.LogError("Antivirus check run error: " + ex.ToString());
				throw ex;
			}
			finally
			{
				text2 = ((text2 == null || text2.Equals("")) ? "-" : text2);
				errorString = ((errorString == null || errorString.Equals("")) ? "-" : errorString);
			}
			Debug.Log("Result from antivirus check:\n" + text2 + " \nErrors:\n" + errorString);
		}
		catch (Exception ex2)
		{
			Debug.Log("Failed to query antivirus apps: " + ex2.Message);
		}
	}
 

An enterprising malicious plugin author can easily replace the contents of the .ps1 file, install a detour in the function in the Assembly-CSharp.dll, and then you have a UAC bypass coupled with Mimikatz or whatever red-team script kiddies like to use being dropped.

Note: denchi will be removing this functionality in a future update, as it provides additional attack surface to the enterprising malicious plugin author. (Executing malicious Powershell from a subshell from a trusted, signed source.)

Conclusions

Run a VirusTotal scan on plugins that are binary only or have the source code audited, and only collab with people that you know aren’t going to steal your files. A PoC in C++ for ripping models will be available soon on GitHub, but not before the “vaccine” is released on Patreon

It’s been a fun detour from my work, doing static analysis on VTS. Dynamic analysis to come soon! (Man, do I love the Steam API…)

A PoC for the TOCTOU will also be released as soon as possible.

Alternatively, you can get Marathon, a BepInEx-based patcher for all vulnerabilities except the model dumping vulnerability here.

Slainte, impost0r