Skip to content

Latest commit

 

History

History
361 lines (290 loc) · 15 KB

notes.md

File metadata and controls

361 lines (290 loc) · 15 KB

Technical Notes

This file contains technical notes that may be useful to anyone exploring the internals of Lightroom 6.14 Classic.

Wishlist

  • Log or display of Javascript errors, especially those triggered when JS is invoked from LUA
  • Find better decompiler for LUA 5.1, source code generated by unluac is hard to follow
  • Run arbitrary LUA code via config.lua or other LUA files
  • Enable the LUA debugger

Tools

  • Resource Hacker (Windows only) to extract the LUA files
  • unluac to decompile the LUA binaries to somewhat human-readable source code.
  • patchluastr to replace strings in LUA binaries
  • Ghidra to decompile and analyze native code
  • Frida to intercept and modify DLL calls

Table of contents

LUA

Large parts of Lightroom, including all the modules (Library, Develop, Map, etc.), are written in LUA 5.1. Except for AgKernel, the LUA code is only available as byte code.

Lightroom contains ~2400 LUA files (libraries?), organized into 50 "modules", which (on Windows) are stored as resources in .dll, .exe and .lrmodule files. When stored in a .dll or .exe, that file may also contain native code associated with the functionality of the LUA code.

Each LUA module contains the file INFO.LUA, which specifies the name and version of the module, and makes specific libraries within that module available globally.

LUA files

During startup, Lightroom also tries to open the following LUA files:

[program path]config.lua
[program path]config.lua.txt
[appdata roaming path]config.lua
[appdata roaming path]config.lua.txt
[appdata roaming path]config-kernel.lua
[program path]AgNamespace.lua
[program path]/lua/AgNamespace.lua
[program path]/lua/AgNamespace.lua/init.lua
[program path]AgNamespace.lua/init.lua

config.lua can be used to enable a debug log. It may be possible to leverage these files to run arbitrary LUA code.

Map module

The LUA libraries for Lightroom's Map module are stored in the file Location.lrmodule.

The Map module uses Google Maps Javascript API. Specifically, Lightroom Classic 6.14 was developed using Google Maps JS API version v3.12.

The Map module first broke with API version v3.52, likely due to Javascript exceptions caused by deprecated attributes. This was fixed by requesting API version v3.51, however this stopped working in November 2023 when Google removed access to that API version.

The LUA code generating HTML for the map view is found in LOCATONMAPVIEW.LUA. In addition, the resources of Location.lrmodule include images and a few .JS files that are referenced in the Javascript code generated by LOCATIONMAPVIEW.LUA.

The HTML code generated for the map view can be exfiltrated by patching LOCATIONMAPVIEW.LUA:

< <html>
> <html><textarea><![CDATA[
< </html>
> ]]></textarea></html>

With this patch, the HTML code is displayed in a text box and can be selected with the mouse and copied into a new file.

Javascript running in the map calls back to LUA through hosteval(), see callCallback = function() in the generated HTML page. Javascript code is also directly invoked from LUA across several libraries.

When the Map module stops working, the most likely reason are uncaught exceptions in Javascript invoked from LUA.

Debug log

Lightroom has low-level logging, which is turned off by default.

To enable logging:

  1. In Lightroom, go to Help > System Info and find Settings Folder
  2. In that folder, create a new file named config.lua with the following text content:
loggers["Location"] = {
  logLevel = 'trace',
  action = 'logfile',
}
  1. Restart Lightroom and you will see log files in your user's Documents folder (Windows, might be somewhere else on Mac)
  2. To disable logging, delete or rename config.lua and restart Lightroom.

Log level trace can be replaced with debug for different output.

The logging functionality is described in following comment found in AgKernel.dll/LUA/KRCONFIGURATION.LUA:

--[[
--- KrConfiguration provides a shared mechanism for retrieving application-wide configuration
--  details. Configurations are represented as a tree of configuration properties.

--  When KrConfiguration is first loaded, it searches both the Lightroom application directory
--  and the Lightroom support files directory for a config.lua file. It merges the results of
--  these files if found and exports them as the KrConfiguration namespace.

--  Example Configuration File:

--  loggers.AgImageMetadata = {
--    logLevel = "trace",
--    action = "print"
--  }

--  This will result in an entry in KrConfiguration called 'logger', that contains the tree
--  described in the configuration file.
--]]

Chromium Embedded Framework

Under the hood, the map view uses the Chromium Embedded Framework (CEF). Native code and related LUA libraries are stored in the file cef_toolkit.dll. devtools_resources.pak is also part of CEF.

Getting CEF to log any Javascript errors would be very helpful for fixing the Map module.

The following LUA module and libraries are stored in cef_toolkit.dll:

AgToolkitIdentifier = "com.adobe.ag.cef_toolkit"

AgExports:
  AgCefHost = "AgCefHost.lua"
  AgHtml5View = "AgHtml5View.lua"
  AgCEFView = "AgCEFView.lua"
  AgViewWinHtml5View = "C:registerAgViewWinHtml5View_L"

Unfortunately, no log output is generated for any of these names.

Based on the decompiled native code, cef_toolkit.dll also implements the LUA-to-Javascript interface:

AgViewWinHtml5View::RunJavaScript()
AgViewWinHtml5View::RunJsCallback()

libcef.dll contains the low-level CEF C API. Usage example of the low-level API: https://github.com/cztomczak/cefcapi/blob/master/examples/main_win.c

Hash 770131916655f914b4659d82ef08993bf9cfdc22 referenced in code in cef_toolkit.dll points to API version 3.2704.1431, available here.

CEF has remote debugging functionality. Can be enabled by setting debug_port in cef_settings_t structure passed to cef_initialize function.

We can use Frida to inspect and modify the settings structure.

Frida script:

import frida
import sys

def on_message(message, data):
    print("[%s] => %s" % (message, data))

def main(target_process):
    session = frida.attach(target_process)
    script = session.create_script("""
        // Find base address of current imported libcef.dll by target process
        const baseAddr = Module.findBaseAddress('libcef.dll');
        if (!baseAddr) {
            console.log('Error: failed to locate DLL');
        } else {
            console.log('libcef.dll baseAddr: ' + baseAddr);

            try {
                // Find function we want to intercept
                const cef_initialize = Module.getExportByName('libcef.dll', 'cef_initialize');

                // Intercept calls to cef_initalize
                Interceptor.attach(cef_initialize, {
                    // When function is called, print out its parameters
                    onEnter(args) {
                        console.log('');
                        console.log('[+] Called cef_initialize ' + cef_initialize);
                        console.log('[+] *args ' + args[0]);
                        console.log('[+] *settings ' + args[1]);
                        console.log('[+] *application ' + args[2]);
                        console.log('[+] *windows_sandbox_info ' + args[3]);
                        dumpAddr('Settings', args[1], 0x150);
                        console.log('browser_subprocess_path='  + args[1].add(0x10).readPointer().readUtf16String())
                        console.log('log_file=' + args[1].add(0xb8).readPointer().readUtf16String());
                        console.log('log_severity=' + args[1].add(0xd0).readU32());
                        console.log('remote_debugging_port=' + args[1].add(0x124).readU32());
                    },

                    // When function is finished
                    onLeave(retval) {
                        console.log('[+] Returned from cef_initialize');
                    }
                });
            } catch (error) {
                console.error(error);
            }
        }

        function dumpAddr(info, addr, size) {
            if (addr.isNull())
                return;

            console.log('Data dump ' + info + ':');
            const buf = addr.readByteArray(size);

            // If you want color magic, set ansi to true
            console.log(hexdump(buf, { offset: 0, length: size, header: true, ansi: false }));
        }
        """)

    script.on('message', on_message)
    script.load()
    print("[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n")
    sys.stdin.read()
    session.detach()

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: %s <process name or PID>" % __file__)
        sys.exit(1)

    try:
        target_process = int(sys.argv[1])
    except ValueError:
        target_process = sys.argv[1]
    main(target_process)

Launching Lightroom, starting above script, and then entering the Map module results in this output:

[+] Called cef_initialize 0x7ffb121af274
[+] *args 0x14d758
[+] *settings 0x14d7a8
[+] *application 0x24109330
[+] *windows_sandbox_info 0x0
Data dump Settings:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  50 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00  P...............
00000010  30 d0 e5 3b 00 00 00 00 20 00 00 00 00 00 00 00  0..;.... .......
00000020  60 2e e3 10 fb 7f 00 00 01 00 00 00 00 00 00 00  `...............
00000030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000040  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000070  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000080  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000090  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000a0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000b0  00 00 00 00 00 00 00 00 a0 dc 29 05 00 00 00 00  ..........).....
000000c0  31 00 00 00 00 00 00 00 60 2e e3 10 fb 7f 00 00  1.......`.......
000000d0  03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000e0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000f0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000100  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000110  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000120  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000130  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000140  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
browser_subprocess_path=.\Adobe Lightroom CEF Helper.exe
log_file=C:\Users\[user]\AppData\Local\Temp\\cef_debug.log
log_severity=3
remote_debugging_port=0
[+] Returned from cef_initialize

Current debug log level 3 is WARNING. There is no log file in the temp directory.

Changed log level to VERBOSE (1) by adding this line at the end of onEnter():

args[1].add(0xd0).writeU32(1); // set log level VERBOSE

With this, cef_debug.log is created at the location specified in log_file. Contains detailed log of HTTP headers, no information related to Javascript.

Enabled remote debugging on port 8080 by adding this line at the end of onEnter():

args[1].add(0x124).writeU32(8080); // enable remote debugging on port 8080

There is now a a web page accessible at http://127.0.0.1:8080, but the debug functionality is broken with the following Javascript error:

Uncaught TypeError: document.registerElement is not a function
    at registerCustomElement (inspector.js:3938:18)
    at inspector.js:3950:492
    at inspector.js:3965:113
registerCustomElement @ inspector.js:3938
(anonymous) @ inspector.js:3950
(anonymous) @ inspector.js:3965

However, using Chrome DevTools works. Steps as described here.

Phoning home

Lightroom sends HTTP requests to www.photoshop.com, presumably to verify whether certain features are available for a given version of Lightroom.

For the Map module, the request is:

https://www.photoshop.com/api/service_status/lightroom/6.14:v1.0.0.0/win/en_us/google-map.json

The response is:

{
  "locale": "en_us",
  "status": "ok",
  "timestamp": "2023-11-22T08:24:20+00:00"
}

Replacing google-map with an arbitrary string returns the same result, indicating that the service status functionality is no longer active. Though still worth documenting in case of issues when the URL goes offline, or Adobe changes its mind.

The LUA code responsible is in AGSERVICESTATUS.LUA, found in LightroomSDK.dll.

Related: Service status was used to disable maps in Lightroom 5 (see issue #8):

https://www.photoshop.com/api/service_status/lightroom/5.0:v1.0.0.0/win/en_us/google-map.json
{
  "locale": "en_us",
  "status": "kill",
  "message_title": "Map is not available",
  "message_body": "Map view is no longer supported on this version of Lightroom.  For more information go to www.adobe.com/go/lightroom-map",
  "timestamp": "2018-07-16T06:33:49Z"
}

Misc notes

Miscellaneous observations.

Lightroom Command line

Lightroom.exe may have these command line options:

  • -runLua
  • -logToDebugStr

LUA debugger

Native code found in wichitafoundation.dll and AgKernel.dll indicate remote LUA debugging functionality, possibly accessible on port 11111.

if (bIsDistribution == 0) {
  if (_DAT_18016e24c != 0) {
    KrDebugger_setCodeRunner(0,FUN_180032c80);
  }
  if (DAT_18016e250 == 0) {
    AgDebugger_setBreakOnThrow(0);
    pcVar2 = "11111";
    if (PTR_s_11111_18016e278 != (undefined *)0x0) {
      pcVar2 = PTR_s_11111_18016e278;
    }
    PTR_s_11111_18016e278 = pcVar2;
    DAT_18017d880 = KrDebugger_open();
  }
  if (DAT_18017d880 != 0) {
    AgLua_setCodeCapturing(DAT_18017d880,1);
  }
}

Random

  • natives_blob.bin contains JavaScript
  • snapshot_blob.bin contains some kind of bytecode, probably not Lua