Vanguard Logs

Background

First off, I like Valorant and don’t like cheaters. So if you are looking for new ways to cheat, this is really going to disappoint you. Now lets start with Valorant has had a lot of really smart people (smarter than me) reversing it and taking it apart for how the anti cheat (Vanguard) works. They then use this knowledge to write some advanced attack mechanisms. This blog is going to cover an aspect of Vanguard that is often over looked, logging.

Teardown Time

If you are following along at home, you can find your own Vanguard logs over at C:\Program Files\Riot Vanguard\Logs. It should look something like below.

Logs

Vanguard is a multipart beast, with one part being a kernel driver (called vgk) which you can actually check if it is loaded on your own machine by running a command like below.

driverquery.exe /V | findstr vgk Driver

Another part of Vanguard is the userland process which is controlled like a Windows service. Services

When trying to track down which process has a handle on which log file I noticed something interesting but not suprising considering its a kernel driver. VGK File handle

The vgk log file has its handle held by System , which makes sense condering this file is most likely being created and written by the kernel land process.

Looking at the logs

Logs

Right away we can tell that date is playing a factor in the filename generation

vgc = vanguard client (user-land) vgk = vanguard kernel (kernel-land)

vgk_YEAR-DAY-MONTH_HOUR_-MINUTE-SECOND.log
vgc_?RANDOM?_YEAR-DAY-MONTH_HOUR_-MINUTE-SECOND.log

In addition to just date, we can see that time is also involved, but in this case its using the 0-24, format. This can be seen by performing a carfully timed service restart and comparing the file modification timestamp to the log file name. Timed Log

Now that we know who makes the files and how the filename is made, lets look at the actual contents. Log Format

Now right away we aren’t seeing much of a pattern, except for those first 4 bytes VGL0. This is a magic number and is a common occurence for a lot of file formats. After looking for a bit longer another pattern starts to emerge, and this one is also another common technique used in serialization/deserializaiton, sizes. When serializing, a program knows how many bytes a piece of data might occupy, but when writing to a file they need a way to tell the corresponding program on the other side how big the data is. The solution to this problem is to not only write out the data, but to also write the number of bytes the data occupies, that way the unpacking program has all the information it needs to safely unpack.

Log Format Red = Magic Header [4 bytes]
Uncolored = Unknown Seed/IV/Time? [32 bytes]
Blue = Record Size [4 bytes]
Green = Record [RecordSize bytes]

After the Green, the pattern of [RecordSize,Record] repeats until the end of the file. Now knowing most of the file format we can write up a simple parser to try and gain some more insight.

import struct
from pathlib import Path
from argparse import ArgumentParser
import os.path


def is_valid_file(parser, arg):
    if not os.path.exists(arg):
        parser.error("The file %s does not exist!" % arg)
    else:
        return arg 

def unpack_logs(file):
  with open(file, 'rb') as fh:
    magic = fh.read(4)
    print("--- hdr ---")
    print(f"magic: {magic}")
    unknown = fh.read(32)
    print(f"unknown: {unknown.hex()}")
    print(f"unknown bytes: {len(unknown)}")
    print("--- logs ---")

    log_count = 0
    while True:
      log_name = f"logs/{log_count}.log"
      data = fh.read(4)
      if not data:
        break
      entry_size = struct.unpack('<i', data)[0]
      print(f"entry_size: {int(entry_size)}")
      data = fh.read(entry_size)
      with open(log_name,'wb') as of:
        of.write(data)
      log_count += 1
    print(f"extracted {log_count} log entries...")


if __name__ == '__main__':
  parser = ArgumentParser(description="unpack_vanguard")
  parser.add_argument("-i", dest="filename", required=True,
                      help="input file", metavar="FILE",
                      type=lambda x: is_valid_file(parser, x))
  args = parser.parse_args()

  Path("logs").mkdir(parents=True, exist_ok=True)
  unpack_logs(args.filename)
python unpack_vanguard.py -h
usage: unpack_vanguard.py [-h] -i FILE

unpack_vanguard

optional arguments:
  -h, --help  show this help message and exit
  -i FILE     input file

Now with this script we can begin examining individual records inside the log file format and see if we can see any other patterns.

Log 1
python unpack_vanguard.py -i vgc_10384_2021-04-14_19-57-44.log

--- hdr ---
magic: b'VGL0'
unknown: 3a5c7e7fa0c2e4e507294b6d6e8fb1d3d4f5183a3b5c7ea0a1c2e40708294b6d
unknown bytes: 32
--- logs ---
entry_size: 142
entry_size: 82
entry_size: 90
entry_size: 76
extracted 4 log entries...

The script will parse each log record and write the log data to its corresponding index (ex. first entry -> 0.log, second -> 1.log…). Below is a comparision check between the first and second entry in a log file. Right away we can see there is a lot of similarities. All records in the log seem to be using the same patterns of bytes 0xAC 0x78 0x37 ….. Log Format

Log 2
Next we have another log file that I was able to generate with a very similar 32 byte uknown value.

python unpack_vanguard.py -i vgc_12828_2021-04-14_19-57-48.log

--- hdr ---
magic: b'VGL0'
unknown: 3a5c7ea0a1c2e40708294b6d6e8fb1d3d4f5183a5c5d7ea0c2c3e407292a4b6d
unknown bytes: 32
--- logs ---
entry_size: 142
entry_size: 84
entry_size: 92
entry_size: 76
extracted 4 log entries...

When we compare the entries in this file we notice something strange, the bytes that are changing are very similar. In addition to that we see that all records in the log use the same patterns of bytes 0x74 0x5b 0x6a …. Log Format

So we have 2 log files, using a similar format, but different values and we know what everything in the file means except those 32 bytes. Given that some program at Riot HQ needs to be able to decode these files once recieved from anyone, it must mean that inside our mystery 32 bytes is the IV/seed/key to decode these files. Now I tried for a little bit to try to gain some more insights about the mysterious 32 bytes without having access to the dumped binary code (not risking my Riot accounts). In the end I was able to generate 2 log files that had a very similar 32 bytes by rapidly starting and stopping the vgc service which is creating the vgc log files. Sadly, I wasn’t able to determine the pattern, but given that the unknowns appear to be very similar when rapidly restarting its probably a good guess that timing definitely plays a part in the 32 bytes. The fact that this pattern exists shows that there is definitely more to be uncovered here if some more time is put in or if the dumped binary can be loaded into a dissassembler.

Log Format

What’s Left?

I kind of left this on a cliffhanger, and that’s because I didn’t feel comfortable proceeding without putting my own Riot accounts in jeopardy. Maybe if people are interested I’ll keep going, but for now this is going to be left where it is and maybe some enterprising people can solve the puzzle!