Reverse-Engineering the Script Message API

Amongst many other debug settings (following the menu items Advanced→Debug settings…) there are two settings, namely:

  • ScriptMessageAPI, and
  • ScriptMessageAPIKey

for which there is no documentation to be found.

In brief, these settings can be traced down to some code within the viewer meant to emit messages on a chosen channel number whenever certain events occur.

The ScriptMessageAPI setting allows the user to define a channel number where the messages will be received and ScriptMessageAPIKey is an encryption key used to encrypt the message being sent. The "encryption" is really a character-by-character XOR between the message generated by the viewer and the key provided by the ScriptMessageAPIKey that is linearly shifted to the length of the message. In order words, the final message that will be sent to the channel defined by the ScriptMessageAPI could be expressed as follows.

Theory and Code

Given a key $k$ of any arbitrary length $\gt 0$, and a message $M$ containing characters $m_{i..n}$ then the encrypted message $E$ will consists of corresponding characters $e_{i..n}$ where each character $e_{i}$ can be computed as:

\begin{eqnarray*}
e_{i} &=& m_{i} \oplus k[i_{c} \% length(k)]
\end{eqnarray*}

The encrypted message $E$ is then converted to Base64 and sent on the channel specified by the ScriptMessageAPI debug setting.

The code responsible for encrypting the message can be found in the source inside the file llimprocessing.cpp in Snowglobe-based viewers defined within the script_msg_api function:

void script_msg_api(const std::string& msg)
{
        static const LLCachedControl<S32> channel("ScriptMessageAPI");
        if (!channel) return;
        static const LLCachedControl<std::string> key("ScriptMessageAPIKey");
        std::string str;
        for (size_t i = 0, keysize = key().size(); i != msg.size(); ++i)
                str += msg[i] ^ key()[i%keysize];
        send_chat_from_viewer(LLBase64::encode(reinterpret_cast<const U8*>(str.c_str()), str.size()), CHAT_TYPE_WHISPER, channel);
}

The actual messages being sent by the viewer have the following format:

UUID + ", " + n

where:

  • UUID is the key of the avatar generating the message,
  • n is a single digit number corresponding to the event type.

The single digit number n corresponds to certain events that are spread out through llimprocessing.cpp and some in llviewermessage.cpp.

n Event Example Message
0 instant message bcb678be-cddc-a470-8fd7-845fefb4a853, 0
1 inventory item offered f0e9f375-e2de-9853-4a0b-ebb8a0e9da54, 1
2 teleport lure received 67a469f5-0692-4524-92d9-f3c340af6ba9, 2
3 teleport lure sent 91b45476-d316-4755-b667-9819441e13e6, 3
4 start typing ea7f05b7-aaf1-4e7c-ae08-d96f9af131d5, 4
5 stop typing 62ef45a0-4642-4b9a-ac2b-46a2fb5e79ca, 5
6 message from object 9da01e7f-c82b-4ca1-b7dd-345afc473ff7, 6
7 group message 52bd6c1e-9b04-419c-be5a-f39ce898e9ee, 7

Decrypting Messages in LSL Using Well-Chosen Keys

Obtaining the plaintext message from the viewer can be done in LSL. Unfortunately, a well-chosen key is an easy choice to work around the limitations of llBase64ToString; namely, llBase64ToString will display a placeholder (?) for all non-printable characters meaning that performing a XOR between two values that would yield an unprintable character cannot be converted back using llBase64ToString. As a workaround, and knowing that all the messages sent by the "Script Message API" have a character set restricted to [0-9a-f], then the key to be chosen could consist of only upper-case characters ([A-Z]) in order to avoid that the XOR operation produces unprintable characters.

The following script is more or less a verbatim translation of the algorithm implemented in llimprocessing.cpp using Ord and Chr by Pedro Oval for elegance:

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
// Chr() function, written by Pedro Oval, 2010-05-28
// Auxiliary function UrlCode, which returns the hex code of the given integer
// (which must be in range 0-255) with a "%" prepended.
string UrlCode(integer b)
{
    string hexd = "0123456789ABCDEF";
    return "%" + llGetSubString(hexd, b>>4, b>>4)
               + llGetSubString(hexd, b&15, b&15);
}
 
// Chr function itself, which implements Unicode to UTF-8 conversion and uses
// llUnescapeURL to do its job.
string Chr(integer ord)
{
    if (ord <= 0)
        return "";
    if (ord < 0x80)
        return llUnescapeURL(UrlCode(ord));
    if (ord < 0x800)
        return llUnescapeURL(UrlCode((ord >> 6) | 0xC0)
                            +UrlCode(ord & 0x3F | 0x80));
    if (ord < 0x10000)
        return llUnescapeURL(UrlCode((ord >> 12) | 0xE0)
                            +UrlCode((ord >> 6) & 0x3F | 0x80)
                            +UrlCode(ord & 0x3F | 0x80));
    return llUnescapeURL(UrlCode((ord >> 18) | 0xF0)
                        +UrlCode((ord >> 12) & 0x3F | 0x80)
                        +UrlCode((ord >> 6) & 0x3F | 0x80)
                        +UrlCode(ord & 0x3F | 0x80));
}
 
// Ord() function, written by Pedro Oval, 2010-05-28
// This function works by using llEscapeURL to find the corresponding UTF-8
// string then converts it to the Unicode code. In cases where llEscapeURL
// doesn't help, a combination of llStringToBase64 and llBase64ToInteger
// does the job instead.
integer Ord(string chr)
{
    if (chr == "")
        return 0;
    chr = llGetSubString(chr, 0, 0);
    string hex = llEscapeURL(chr);
    if (llGetSubString(hex, 0, 0) != "%")
    {
        // Regular character - we can't take advantage of llEscapeURL in this case,
        // so we use llStringToBase64/llBase64ToInteger instead.
        return llBase64ToInteger("AAAA" + llStringToBase64(chr));
    }
    integer b = (integer)("0x" + llGetSubString(hex, 1, 2));
    if (b < 194 || b > 244)
        return b;
    if (b < 224)
        return ((b & 0x1F) << 6) | (integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F;
    integer cp;
    if (b < 240)
    {
        cp = (b & 0x0F) << 12;
        cp += ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 6;
        cp += (integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F;
        return cp;
    }
    cp = (b & 0x07) << 18;
    cp += ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 12;
    cp += ((integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F) << 6;
    cp += (integer)("0x" + llGetSubString(hex, 10, 11)) & 0x3F;
    return cp;
}
 
default
{
    state_entry() {
        // Listen on the channel number specified by the ScriptMessageAPI debug setting.
        llListen(1, "", "", "");
    }
 
    listen(integer channel, string name, key id, string message) {
        // Define the encryption key specified by the ScriptMessageAPIKey debug setting.
        string enc = "ABRACADABRA";
 
        // Dump all listen() parameters.
        llOwnerSay("CHANNEL: " + (string)channel);
        llOwnerSay("NAME: " + name);
        llOwnerSay("KEY: " + (string)id);
 
        // Convert the message from Base64 to its string equivalent.
        message = llBase64ToString(message);
 
        // Reproduce the algorith verbatim as defined by the viewer in the script_msg_api() 
        // function within the llimprocessing.cpp file.
        string output = "";
        integer i;
        integer size;
        for(i = 0, size = llStringLength(enc); i != llStringLength(message); ++i) {
            output += Chr(Ord(llGetSubString(message, i, i)) ^ Ord(llGetSubString(enc, i % size, i % size)));
        }
 
        // Print out the converted message.
        llOwnerSay("MESSAGE: " + output);
    }
}

Unfortunately, the "Script Message API" does not carry too much information about the event, just that it is possible to determine from the number following the UUID what event occurred.

Customized AFK Auto-Responder as Practical Application

One possible example application that would come to mind (which was more or less the incentive for demystifying this debug setting) is a customized AFK responder. The AFK responder that SecondLife viewers have will respond using the same message to any avatars that sends an instant message. Leveraging the "Script Message API" debug setting, it is possible to create a customized AFK responder that will make a primitive send a message back to an avatar that sends an instant message. An example script could be as follows:

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                          CONFIGURATION                                //
///////////////////////////////////////////////////////////////////////////
 
// Define the encryption key specified by the ScriptMessageAPIKey debug setting.
string ENCRYPTION_KEY = "ABRACADABRA";
 
// Define a list of automatic responses for various avatars by UUID.
list AUTOMATIC_RESPONSES = [
    "4d7d815d-ce6a-4ddf-94f6-ab2a2ed97f75",
    "Nuh brah, I am buzzy!",
    "ae3910d8-ad88-4987-a5fa-2f43e1811056",
    "Cool store, brah, get back to you soon!"
];
 
///////////////////////////////////////////////////////////////////////////
//                            INTERNALS                                  //
///////////////////////////////////////////////////////////////////////////
 
// Chr() function, written by Pedro Oval, 2010-05-28
// Auxiliary function UrlCode, which returns the hex code of the given integer
// (which must be in range 0-255) with a "%" prepended.
string UrlCode(integer b)
{
    string hexd = "0123456789ABCDEF";
    return "%" + llGetSubString(hexd, b>>4, b>>4)
               + llGetSubString(hexd, b&15, b&15);
}
 
// Chr function itself, which implements Unicode to UTF-8 conversion and uses
// llUnescapeURL to do its job.
string Chr(integer ord)
{
    if (ord <= 0)
        return "";
    if (ord < 0x80)
        return llUnescapeURL(UrlCode(ord));
    if (ord < 0x800)
        return llUnescapeURL(UrlCode((ord >> 6) | 0xC0)
                            +UrlCode(ord & 0x3F | 0x80));
    if (ord < 0x10000)
        return llUnescapeURL(UrlCode((ord >> 12) | 0xE0)
                            +UrlCode((ord >> 6) & 0x3F | 0x80)
                            +UrlCode(ord & 0x3F | 0x80));
    return llUnescapeURL(UrlCode((ord >> 18) | 0xF0)
                        +UrlCode((ord >> 12) & 0x3F | 0x80)
                        +UrlCode((ord >> 6) & 0x3F | 0x80)
                        +UrlCode(ord & 0x3F | 0x80));
}
 
// Ord() function, written by Pedro Oval, 2010-05-28
// This function works by using llEscapeURL to find the corresponding UTF-8
// string then converts it to the Unicode code. In cases where llEscapeURL
// doesn't help, a combination of llStringToBase64 and llBase64ToInteger
// does the job instead.
integer Ord(string chr)
{
    if (chr == "")
        return 0;
    chr = llGetSubString(chr, 0, 0);
    string hex = llEscapeURL(chr);
    if (llGetSubString(hex, 0, 0) != "%")
    {
        // Regular character - we can't take advantage of llEscapeURL in this case,
        // so we use llStringToBase64/llBase64ToInteger instead.
        return llBase64ToInteger("AAAA" + llStringToBase64(chr));
    }
    integer b = (integer)("0x" + llGetSubString(hex, 1, 2));
    if (b < 194 || b > 244)
        return b;
    if (b < 224)
        return ((b & 0x1F) << 6) | (integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F;
    integer cp;
    if (b < 240)
    {
        cp = (b & 0x0F) << 12;
        cp += ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 6;
        cp += (integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F;
        return cp;
    }
    cp = (b & 0x07) << 18;
    cp += ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 12;
    cp += ((integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F) << 6;
    cp += (integer)("0x" + llGetSubString(hex, 10, 11)) & 0x3F;
    return cp;
}
 
default
{
    state_entry() {
        llListen(1, "", "", "");
    }
 
    listen(integer channel, string name, key id, string message) {
        // Dump all listen() parameters.
        llOwnerSay("CHANNEL: " + (string)channel);
        llOwnerSay("NAME: " + name);
        llOwnerSay("KEY: " + (string)id);
 
        // Convert the message from Base64 to its string equivalent.
        message = llBase64ToString(message);
 
        // Reproduce the algorith verbatim as defined by the viewer in the script_msg_api()
        // function within the llimprocessing.cpp file.
        string output = "";
        integer i;
        integer size;
        for(i = 0, size = llStringLength(ENCRYPTION_KEY); i != llStringLength(message); ++i) {
            output +=
                Chr(Ord(llGetSubString(message, i, i)) ^
                Ord(llGetSubString(ENCRYPTION_KEY, i % size, i % size)));
        }
 
        // Print out the converted message.
        llOwnerSay("MESSAGE: " + output);
 
        // Split the message into a list where sm[0] -> UUID and sm[1] -> event.
        list sm = llCSV2List(output);
 
        // If the event is not an instant message (0) nor a teleport (2) then do nothing.
        //if(llList2Integer(sm, 1) != 0 && llList2Integer(sm, 1) != 2) {
        //    return;
        //}
 
        // Check if the UUID of the foreign avatar generating the event can be
        // found amongst the avatars in the auto-response list.
        i = llListFindList(AUTOMATIC_RESPONSES, [ llList2String(sm, 0) ]);
        // .. do nothing if there is no auto-response for the avatar.
        if(i == -1) {
            return;
        }
 
        // Retrieve the response and send it to the avatar.
        string response = llList2String(AUTOMATIC_RESPONSES, i + 1);
        llInstantMessage((key)llList2String(sm, 0), response);
    }
}

secondlife/reverse-engineering_the_script_message_api.txt · Last modified: 2022/11/24 07:46 by 127.0.0.1

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.