InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

Follow publication

Reverse Engineering a Native Desktop Application (Tauri App)

You might extract some secrets from this!

Felix Alexander
InfoSec Write-ups
Published in
23 min readDec 5, 2022

--

Tauri Natives App

Notes:
Greetings fellow readers! Before you read this content, I’d like to remind you that this is based on my research and findings, so if you encounter or happen to see a misleading information, I’d love to hear your opinion and I’m very open to any information that should be stated correctly.

This writing is based on what I encountered from a challenge that was designed in one of the biggest national CTF, Cyber Jawara 2022 in Indonesia which was created by an amazing problem setter, Rendi Yuda. The challenge involves a native desktop application called Tauri App. I’d also like to give a shoutout to my team, Happy Three Friends and all CJ participants and stakeholders for organizing this amazing event. Kudos to you all!

The Background

Have you ever heard about Tauri before? You might’ve not known it before because most of us know more about Electron.js, a framework wrapper for building independent application. Both of them are the same thing, yet there are some significant differences between those two.

In general, Tauri GitHub Page has provided them in a table view.

Tauri vs Electron.js Comparison

If we’re talking about the resource consumption, Tauri wins the game since it doesn’t rely on Chromium. There are numerous articles which explain them more detail and we are not going to discuss further about it here.

What we want to achieve is that to understand how secure is this Tauri app can be, either we explore it from the intended behavior of Tauri, or just a misconfiguration that happens by the user/developer who decides to migrate their app with Tauri in a single desktop app. This can be done in such manner because sometimes a user can “accidentally” downgraded the built-in app’s security features due to not placing its aspect at the first place.

The scenario that is involved here and to get our hands dirty is how Tauri App protects their User/Developer’s Assets inside a Binary that was targeted in a specific OS, especially in Linux.

You can imagine it like this, we know that with only HTML, CSS and JS, we can create an awesome website so we can also try to elevate our website into a single desktop apps and we choose Tauri for dealing with it. Yet as a ‘rookie’ developer, you accidentally put your hardcoded API key or even another confidential data such as PII-based ones to be embedded inside the Javascript since there’s a dependant functionality with it. Do you expect Tauri App to protect those assets? Let’s dive into that!

The writer used the following tools for assisting the reverse engineering process:

“From Dev to Rev” Perspectives

The backend binding of Tauri is using a Rust language. The bundled application itself always has a core process and it serves an OEP (Original Entry Point) or a _start of the user/developer’s app. Interesting fact to be stated is that this native app doesn’t require Chromium so that it has to render the designated app, yet it takes an advantage from a WebView Libraries which derived from WRY. This means that all the HTML,CSS and JS later will be loaded in a WebView, just like how Android uses WebView to load a web-based content since it contains a browser engine.

In order to build the basic app for the first time, Tauri needs to know the user/developer’s JSON-like configuration files and that’s why there’s a thing called tauri.conf.json. The general structure looks probably like this, taken from the Tauri Github examples and what we’ll be focusing at is in the build objects since it holds the source code location. What we’re interpreting later is that not only a HTML file that will be loaded to the Tauri context but also some of the files, and suppose there’s also a JS. Thus, the distDir or the devPath might points out to the specific src-tauri directory.

{
"$schema": "../../core/config-schema/schema.json",
"build": {
"distDir": ["index.html"],
"devPath": ["index.html"],
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
"package": {
"productName": "The Basic Rookie Dev App",
"version": "0.1.0"
},
"tauri": {
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"icon": [
"../.icons/32x32.png",
"../.icons/128x128.png",
"../.icons/128x128@2x.png",
"../.icons/icon.icns",
"../.icons/icon.ico"
],
"resources": [],
"externalBin": [],
"copyright": "",
"category": "DeveloperTool",
"shortDescription": "",
"longDescription": "",
"deb": {
"depends": []
},
"macOS": {
"frameworks": [],
"exceptionDomain": ""
}
},
"allowlist": {
"all": false
},
"windows": [
{
"title": "Aseng was here~!",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": "default-src 'self'"
}
}

Finally, this JSON configuration file will be loaded and built since from the main.rs of the application will handle and passed it to the context generator and Codegen later as a structure. Below is the sample of main Rust code from the same link here.

#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]


fn main() {
tauri::Builder::default()
.run(tauri::generate_context!(
"../../examples/helloworld/tauri.conf.json"
))
.expect("error while running tauri application");
}

If we take a look at the snippet from https://github.com/tauri-apps/tauri/blob/dev/core/tauri-codegen/src/context.rs, our assets will also be handled from this code. Each of our defined asset is carefully parsed from its extension and later on through the array of configuration objects.

use tauri_utils::assets::AssetKey;
use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
use tauri_utils::html::{
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
};

#[cfg(feature = "shell-scope")]
use tauri_utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistScope};

use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};

/// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
pub struct ContextData {
pub dev: bool,
pub config: Config,
pub config_parent: PathBuf,
pub root: TokenStream,
}

fn map_core_assets(
options: &AssetOptions,
target: Target,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
#[cfg(feature = "isolation")]
let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
let csp = options.csp;
let dangerous_disable_asset_csp_modification =
options.dangerous_disable_asset_csp_modification.clone();
move |key, path, input, csp_hashes| {
if path.extension() == Some(OsStr::new("html")) {
#[allow(clippy::collapsible_if)]
if csp {
let mut document = parse_html(String::from_utf8_lossy(input).into_owned());

if target == Target::Linux {
::tauri_utils::html::inject_csp_token(&mut document);
}

inject_nonce_token(&mut document, &dangerous_disable_asset_csp_modification);

if dangerous_disable_asset_csp_modification.can_modify("script-src") {
if let Ok(inline_script_elements) = document.select("script:not(empty)") {
let mut scripts = Vec::new();
for inline_script_el in inline_script_elements {
let script = inline_script_el.as_node().text_contents();
let mut hasher = Sha256::new();
hasher.update(&script);
let hash = hasher.finalize();
scripts.push(format!("'sha256-{}'", base64::encode(hash)));
}
csp_hashes
.inline_scripts
.entry(key.clone().into())
.or_default()
.append(&mut scripts);
}
}

#[cfg(feature = "isolation")]
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
// create the csp for the isolation iframe styling now, to make the runtime less complex
let mut hasher = Sha256::new();
hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
let hash = hasher.finalize();
csp_hashes
.styles
.push(format!("'sha256-{}'", base64::encode(hash)));
}
}

*input = serialize_html_node(&document);
}
}
Ok(())
}
}

//.... [SNIP] .......

let app_url = if dev {
&config.build.dev_path
} else {
&config.build.dist_dir
};

let assets = match app_url {
AppUrl::Url(url) => match url {
WindowUrl::External(_) => Default::default(),
WindowUrl::App(path) => {
if path.components().count() == 0 {
panic!(
"The `{}` configuration cannot be empty",
if dev { "devPath" } else { "distDir" }
)
}
let assets_path = config_parent.join(path);
if !assets_path.exists() {
panic!(
"The `{}` configuration is set to `{:?}` but this path doesn't exist",
if dev { "devPath" } else { "distDir" },
path
)
}
EmbeddedAssets::new(assets_path, &options, map_core_assets(&options, target))?
}
_ => unimplemented!(),
},
AppUrl::Files(files) => EmbeddedAssets::new(
files
.iter()
.map(|p| config_parent.join(p))
.collect::<Vec<_>>(),
&options,
map_core_assets(&options, target),
)?,
_ => unimplemented!(),
};

// ..... [SNIP] ......

Later on, here comes the questions.

We are interested in how Tauri App protects our assets. Are they encrypted? Are they stored in a different format from a transformation process? Are they stored in a Tauri special Cloud Storage? Or are they stored in a plain text thus left unprotected?

After looking at those snippets, we know that there’s a reference to tauri_utils that handle our main assets and this is where things are getting interesting. Let’s take a look at the snippet below which was taken from the official Tauri Github at https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/assets.rs

/// Represents a container of file assets that are retrievable during runtime.
pub trait Assets: Send + Sync + 'static {
/// Get the content of the passed [`AssetKey`].
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>>;

/// Gets the hashes for the CSP tag of the HTML on the given path.
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_>;
}

/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
#[derive(Debug)]
pub struct EmbeddedAssets {
assets: phf::Map<&'static str, &'static [u8]>,
// Hashes that must be injected to the CSP of every HTML file.
global_hashes: &'static [CspHash<'static>],
// Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
}

impl EmbeddedAssets {
/// Creates a new instance from the given asset map and script hash list.
pub const fn new(
map: phf::Map<&'static str, &'static [u8]>,
global_hashes: &'static [CspHash<'static>],
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
) -> Self {
Self {
assets: map,
global_hashes,
html_hashes,
}
}
}

impl Assets for EmbeddedAssets {
#[cfg(feature = "compression")]
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
self
.assets
.get(key.as_ref())
.map(|&(mut asdf)| {
// with the exception of extremely small files, output should usually be
// at least as large as the compressed version.
let mut buf = Vec::with_capacity(asdf.len());
brotli::BrotliDecompress(&mut asdf, &mut buf).map(|()| buf)
})
.and_then(Result::ok)
.map(Cow::Owned)
}

#[cfg(not(feature = "compression"))]
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
self
.assets
.get(key.as_ref())
.copied()
.map(|a| Cow::Owned(a.to_vec()))
}

What’s your first thoughts when reading those snippet? Although specific assets are “marked” with some hashes validation, randomized ID and stuff, our assets are not encrypted, yet our assets are only compressed using Brotli, a lossless data compressor system. According to the reference from this official Taori docs -> https://tauri.app/v1/guides/building/app-size/, we need to pay attention on this part:

Tauri Asset Compression Features

It turns out Brotli Compression is actually enabled by default, but somehow user also have a capability to disable this feature as they like since this is considered as a preferential options. Another things that we need to take note is stated from the security docs itself (https://tauri.app/v1/references/architecture/security/).

There are some CSP Injection as a initial setup and handling from the context generator and Codegen, and the last point makes the challenge to become harder since we can’t decompile the native desktop apps binary easily.

Some quick notes about the ASAR file reference that is stated from the docs actually just a simple tar-archive file format and the unpacker is already existed, refers to this answer. ASAR file itself can also be retrieved from static analysis, even using a famous file carver tool which used for a forensic purpose such as binwalk, yet in Tauri App case, it doesn’t work that easily since all of the assets are entirely loaded in runtime. This means the compression and the decompression mechanism also will be done at runtime. What could be our finale solution for this problem? Since the assets handling is done at runtime, we’ll debug the application at runtime as well so we’ll be using a combination of static and dynamic analysis.

Diving into the Challenge

We already knew the background story of how Tauri App works in general so we’ll be focusing more on the challenge now. Given a Linux Binary which was derived from the actual project from a simple website validator and the scenario stays the same from what the writer has told earlier, to be wrapped in a single desktop application.

Tauri App Binary

The goals and objective of this challenge is pretty straightforward, since there’s a special page that prompts a user input that will be taken for a validation as a password and thus upgrading the application to pro version. In order to understand the logic behind the app, we need to know how the input is passed and validated which means we’ll have to leak the source code of the Tauri App, or the original assets from the creator itself.

Let’s take a look at how static analysis might help, we’ll be using IDA to decompile the Rust binary. Rust itself sometimes become a nightmare for the decompiler as this language seems to have a similarity of the mangled function names just like how C++ also interpreted.

// ... [SNIP] ...
do
{
v21 = v20 + 1;
if ( v20 == -1 )
std::thread::ThreadId::new::exhausted::h8b15b09161259129();
v22 = v20;
v20 = _InterlockedCompareExchange64(&std::thread::ThreadId::new::COUNTER::h3a05da468aebc0a6, v21, v20);
}
while ( v22 != v20 );
v19[4] = (void *)v21;
*((_DWORD *)v19 + 10) = 0;
fds.sa_handler = v12;
fds.sa_mask.__val[0] = v3;
fds.sa_mask.__val[1] = v4;
std::sys_common::thread_info::set::hd2260a9241afaa5b();
std::sys_common::backtrace::__rust_begin_short_backtrace::h9567a838cd73883d(&helloworld::main::hf56bc84460f29d43);
if ( std::rt::cleanup::CLEANUP::h49cafe1e845be7b2 != 3 )
{
LOBYTE(v25) = 1;
fds.sa_handler = (__sighandler_t)&v25;
std::sync::once::Once::call_inner::he7c95df8c763a30d();
}

// ... [SNIP] ...

This is how the pre-handler for the application entry point is going to be started and it uses a signal handler as an IPC or Inter-Process Communication mechanism to call the helloworld::main::<somehash> . If we are following its CF/Control Flow to the function, we’ll notice something that was discussed earlier.

qmemcpy(v7, "flag-viewer", 11);
v10 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(5uLL);
v12 = v11;
*(_DWORD *)v10 = 774975024;
*(_BYTE *)(v10 + 4) = 48;
v13 = _rust_alloc(0x138uLL, 8uLL);
if ( !v13 )
goto LABEL_25;
v14 = v13;
v131 = v2;
v132 = v12;
v133 = v10;
v134 = v7;
*(_QWORD *)v250 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(4uLL);
*(_QWORD *)&v250[8] = v15;
**(_DWORD **)v250 = 1852399981;
*(_QWORD *)&v250[16] = 4LL;
std::sys::unix::os_str::Slice::to_owned::hafd5ea8305043fe1();
v16 = (void *)alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0xBuLL);
qmemcpy(v16, "Flag Viewer", 11);
qmemcpy((void *)v14, v250, 0x40uLL);
*(_DWORD *)(v14 + 64) = *(_DWORD *)&v250[64];
*(_DWORD *)(v14 + 68) = 2;
*(_QWORD *)(v14 + 112) = 0LL;
*(_QWORD *)(v14 + 136) = 0LL;
*(_QWORD *)(v14 + 152) = 0LL;
*(_QWORD *)(v14 + 168) = 0x4089000000000000LL;
*(_QWORD *)(v14 + 176) = 0x4082C00000000000LL;
*(_QWORD *)(v14 + 184) = 0LL;
*(_QWORD *)(v14 + 200) = 0LL;
*(_QWORD *)(v14 + 216) = 0LL;
*(_QWORD *)(v14 + 232) = 0LL;
*(_QWORD *)(v14 + 248) = v16;
*(_QWORD *)(v14 + 256) = v17;
*(_QWORD *)(v14 + 264) = 11LL;
*(_QWORD *)(v14 + 272) = 0LL;
*(_QWORD *)(v14 + 296) = 0x100000100010001LL;
*(_DWORD *)(v14 + 304) = 33554433;
*(_WORD *)(v14 + 308) = 0;
v135 = v14;
*(_BYTE *)(v14 + 310) = 0;
v18 = (void *)alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0xDuLL);
v129 = v19;
qmemcpy(v18, "com.tauri.dev", 13);
v130 = v18;
v20 = _rust_alloc(0x78uLL, 8uLL);
if ( !v20 )
goto LABEL_25;
v21 = (_QWORD *)v20;
v22 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0x13uLL);
v84 = v23;
*(_OWORD *)v22 = *(_OWORD *)"../.icons/32x32.png../.icons/128x128.png../.icons/128x128@2x.png../.icons/icon.icns../.icons/icon.icodefault-src 'self'public";
*(_DWORD *)(v22 + 15) = 1735290926;
v24 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0x15uLL);
v82 = v25;
*(_QWORD *)(v24 + 13) = 0x676E702E38323178LL;
*(_OWORD *)v24 = *(_OWORD *)"../.icons/128x128.png../.icons/128x128@2x.png../.icons/icon.icns../.icons/icon.icodefault-src 'self'public";
v26 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0x18uLL);
v80 = v27;
*(_QWORD *)(v26 + 16) = 0x676E702E78324038LL;
*(_OWORD *)v26 = *(_OWORD *)"../.icons/128x128@2x.png../.icons/icon.icns../.icons/icon.icodefault-src 'self'public";
v28 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0x13uLL);
v79 = v29;
*(_OWORD *)v28 = *(_OWORD *)"../.icons/icon.icns../.icons/icon.icodefault-src 'self'public";
*(_DWORD *)(v28 + 15) = 1936614249;
v30 = alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h0ef84f43d0508836(0x12uLL);
v21[12] = v30;
v21[13] = v31;
*(_OWORD *)v30 = *(_OWORD *)"../.icons/icon.icodefault-src 'self'public";
*(_WORD *)(v30 + 16) = 28515;

What can we interpret from this function? You might want to scroll up if you forget about these strings indication, which were derived from the tauri.conf.json earlier. It looks like there’s a process to load these array objects since there are numerous of function that takes a capability to copy from a single source to a destination (in the context of struct, vector and array).

v431 = "Tauri Programme within The Commons ConservancyMake tiny, secure apps for all desktop platforms with Taurierror "
"while running tauri application";
v432 = 46LL;
v433 = "Make tiny, secure apps for all desktop platforms with Taurierror while running tauri application";
v434 = 59LL;
((void (__fastcall *)(_BYTE *, _BYTE *, _BYTE *))tauri::app::Builder$LT$R$GT$::build::h7c64c955f54959e3)(
v246,
v238,
v250);
memcpy(v225, &v246[1], 0x5FuLL);
if ( v247 != 3 )
{
memcpy(&v250[96], &v246[96], 0x90uLL);
qmemcpy(&v250[248], v248, 0x68uLL);
v250[0] = v246[0];
memcpy(&v250[1], v225, 0x5FuLL);
*(_QWORD *)&v250[240] = v247;
((void (__fastcall *)(__m256i *, _BYTE *))_$LT$tauri..app..AppHandle$LT$R$GT$$u20$as$u20$core..clone..Clone$GT$::clone::h22f9c9fdb5774633)(
v236,
&v250[224]);
v59 = *(_QWORD *)&v250[216];
v60 = _InterlockedIncrement64(*(volatile signed __int64 **)&v250[216]);
if ( !((v60 < 0) ^ v61 | (v60 == 0)) )
{
if ( *(_QWORD *)&v250[16] == 3LL )
core::panicking::panic::hf0565452d0d0936c();
*(_OWORD *)v238 = *(_OWORD *)v250;
*(_QWORD *)&v238[16] = *(_QWORD *)&v250[16];
memcpy(&v238[24], &v250[24], 0xC0uLL);
v62 = *(_QWORD *)&v238[88];
v63 = _InterlockedIncrement64(*(volatile signed __int64 **)&v238[88]);
if ( !((v63 < 0) ^ v61 | (v63 == 0)) )
{
v64 = *(_QWORD *)v238;
v65 = _InterlockedIncrement64(*(volatile signed __int64 **)v238);
if ( !((v65 < 0) ^ v61 | (v65 == 0)) )
{
v66 = *(_QWORD *)&v238[80];
v67 = ((__int64 (__fastcall *)(_BYTE *))tao::event_loop::EventLoop$LT$T$GT$::create_proxy::h6f86f7baa55289ee)(&v238[120]);
qmemcpy(v226, &v238[120], sizeof(v226));
*(_QWORD *)&v246[16] = *(_QWORD *)&v238[112];
*(_OWORD *)v246 = *(_OWORD *)&v238[96];
*(_QWORD *)&v246[24] = v67;
*(_QWORD *)&v246[32] = v68;
qmemcpy(&v246[40], v236, 0x80uLL);
*(_QWORD *)&v246[168] = v59;
*(_QWORD *)&v246[176] = v64;
*(_QWORD *)&v246[184] = v62;
*(_QWORD *)&v246[192] = v66;
((void (__fastcall __noreturn *)(__m256i *, _BYTE *))tao::event_loop::EventLoop$LT$T$GT$::run::h9b4fce1a6961520f)(
v226,
v246);
}
}
}
BUG();
}
memcpy(v238, v225, 0x5FuLL);
if ( v246[0] != 28 )
{
v250[0] = v246[0];
memcpy(&v250[1], v238, 0x5FuLL);
LABEL_31:
core::result::unwrap_failed::hfaddf24b248137d3();
}
if ( v120 )
free(v119);
((void (__fastcall *)(__int64 *))core::ptr::drop_in_place$LT$tauri_utils..config..FsAllowlistScope$GT$::h59f25386f9484a44)(&v174);
v54 = (void *)v180;
((void (__fastcall *)(__int64, __int64))_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..ops..drop..Drop$GT$::drop::h7a5477a2103cf4a6)(
v180,
v181.m256i_i64[1]);
if ( v181.m256i_i64[0] )
free(v54);
if ( v181.m256i_i64[2] && v181.m256i_i64[3] )
free((void *)v181.m256i_i64[2]);
v55 = (char *)v184;
if ( *((_QWORD *)&v185 + 1) )
{
v56 = 88LL * *((_QWORD *)&v185 + 1);
v57 = 0LL;
do
{
if ( *(_QWORD *)&v55[v57 + 8] )
free(*(void **)&v55[v57]);
v57 += 88LL;
}
while ( v56 != v57 );
}
if ( (_QWORD)v185 )
free(v55);
((void (__fastcall *)(__int64 *))core::ptr::drop_in_place$LT$tauri_utils..config..FsAllowlistScope$GT$::h59f25386f9484a44)(&v187);
((void (__fastcall *)(__int64, _QWORD))_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..ops..drop..Drop$GT$::drop::h7a5477a2103cf4a6)(
8LL,
0LL);
((void (__fastcall *)(__int64 *))core::ptr::drop_in_place$LT$tauri_utils..config..FsAllowlistScope$GT$::h59f25386f9484a44)(&v232);
return ((__int64 (__fastcall *)(__int64 *))core::ptr::drop_in_place$LT$tauri_utils..config..FsAllowlistScope$GT$::h59f25386f9484a44)(&v227);
}

Finally, the Tauri Builder is called and this is the root or the fondation of the binary native apps. There are a lot of branches that will be called during the control flow execution.

Tauri App Builder Control Flow XREFS

This might be confusing and takes a lot of time to figure out what’s the behavior of each functions do. The writer notice something that might be advantageous to check where’s the assets location without having to go traversing through all those functions. We’ll go for two alternative methodologies for extracting and leaking the source code.

The Art of Deconstructors

Have you ever heard about how does a constructors (.ctor) or a deconstructors (.dtor) work? If you’re more into Rust, you probably want to take a look at this crates, but as the writer is more capable on explaining it in C, we’ll be taking an example of this code below.

#include <stdio.h>
void pre_main() __attribute__((constructor));
void post_main() __attribute__((destructor));

void pre_main() {
puts("Yup I'm called before the main() function");
}

void post_main() {
puts("Eh? I'm called after all the main() is completed! No matter how many iterations ~");
}

int main() {
puts("Hello seng.");
return 0;
}

It’s pretty straightforward to explain the concept, the constructor responsibles for executing something before the main function is called but on the other hand, the destructor responsibles for executing certain command after the main function is completed or called.

How do these two functions work in low level? There’s a great explanation from https://www.exploit-db.com/papers/13234, since I also love to hide a syscall such as abusing these two for hiding a certain function call or syscall which has a capability for anti-reverse engineering methodology. Pay attention to these lines:

Studying the glibc initialization code shows that ‘__libc_csu_fini’ and ‘__libc_csu_init’ functions have a special purpose in the startup flow.
The ‘__libc_csu_init’ acts as the constructor function. Is job is to be invoked prior to the real program in our case the main() function. That
is an ideal place to put program related initialization code. The ‘__libc_csu_fini’ function acts as the destructor function. Its job is to
be invoked as the last function, thus to make sure the program hasn’t left anything behind and made a nice clean exit.

In a shared libraries programming, the programmers have two functions ‘_init’ and ‘_fini’ which are the shared library constructer and
destructor functions. Our executable has these two functions as well. Only in our case they are been part of an integration mechanism.

To conclude, what are the relationships between a destructor (.dtor) with the Tauri Binary App? We know that there are several libraries which are linked and imported to the binary itself. It might be possible that among those libraries, there are one which might contain those attributes and thus we noticed these exported functions at the end.

Tauri App Binary Exports

Although there’s only one pointer that was located in the _init constructor but there’s none in the _fini destructor, the flow stays the same. The destructor will always be called after all the functionalities in the main called along with its XREFS branches.

Since Tauri App loads the user/developer’s assets at runtime, there’s a similar concept just like how Android prepares its MainActivity to be completed and this is where the idea comes from. Why don’t we put a breakpoint on _fini destructor so that after all those assets are loaded, there’s a probability that the asset remains available in the heap memory region, known as carving the memory.

We’ll use the strings indication based on what the intended behavior of the challenge should be. We know at the release build that the author’s name “vidner” is popped-up as a HTML static format, so we could use the GEF GDB feature of search-pattern which also known as the grep alias to do it. We should also rebase the address since it’s PIE enabled.

GDB Debugging

Let’s try to grep the “vidner” strings and see if it successfully finds the HTML indication. We’ll be using “search-pattern” in GEF.

Vidner Strings Indication in Heap Memory

We successfully find the indication, next one we’ll be using the GDB capability to print out the certain value in the address offset as a string x/s

gef➤  x/10s 0x5555559c7289-200
0x5555559c71c1: "a\322UUU"
0x5555559c71c7: ""
0x5555559c71c8: "\360a\322UUU"
0x5555559c71cf: ""
0x5555559c71d0: "<head>\n <meta charset=\"UTF-8\">\n <meta content=\"ie=edge\" http-equiv=\"X-UA-Compatible\">\n <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\">\n <meta content=\"vidner\" name=\"author\">\n <title>Flag Viewer</title>\n <link href=\"https://fonts.gstatic.com\" rel=\"preconnect\">\n <link href=\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&amp;display=swap\" rel=\"stylesheet\">\n <link href=\"./css/styles.css\" rel=\"stylesheet\">\n <meta content=\"script-src 'self' 'sha256-LiAOJW+SxhRQ1D3FA8oo/AjCouFSGU4xYS5EfRdWEx4=' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; default-src 'self'\" http-equiv=\"Content-Security-Policy\"></head>\n <body>\n <header>\n <div class=\"logo\">\n <img alt=\"logo\" src=\"./images/logo.svg\">\n </div>\n <button class=\"btn\">\n <img alt=\"About\" src=\"./images/icon-hamburger.svg\">\n </button>\n </header>\n <main>\n <button id=\"prev\">&lt;</button>\n <div class=\"card-container\"></div>\n <button id=\"next\">&gt;</button>\n <div class=\"about\">\n <h1>About</h1>\n <p id=\"changeme\">This is a release build, have fun with it.</p>\n <h2>- vidner</h2>\n <div>\n <h3 id=\"message\">Upgrade</h3>\n <br>\n <input id=\"key\" name=\"key\" placeholder=\"password-that-you-guys-need-to-have\" type=\"text\">\n <button id=\"upgrade\" name=\"upgrade\">⬆</button>\n </div>\n </div>\n </main>\n <script src=\"./js/scripts.js\"></script>\n \n\n</body></html>U"
<head>
<meta charset=\ "UTF-8\">
<meta content=\ "ie=edge\" http-equiv=\ "X-UA-Compatible\">
<meta content=\ "width=device-width, initial-scale=1.0\" name=\ "viewport\">
<meta content=\ "vidner\" name=\ "author\">
<title>Flag Viewer</title>
<link href=\ "https://fonts.gstatic.com\" rel=\ "preconnect\">
<link href=\ "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&amp;display=swap\" rel=\ "stylesheet\">
<link href=\ "./css/styles.css\" rel=\ "stylesheet\">
<meta content=\ "script-src 'self' 'sha256-LiAOJW+SxhRQ1D3FA8oo/AjCouFSGU4xYS5EfRdWEx4=' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; default-src 'self'\" http-equiv=\ "Content-Security-Policy\">
</head>

<body>
<header>
<div class=\ "logo\"> <img alt=\ "logo\" src=\ "./images/logo.svg\"> </div>
<button class=\ "btn\"> <img alt=\ "About\" src=\ "./images/icon-hamburger.svg\"> </button>
</header>
<main>
<button id=\ "prev\">&lt;</button>
<div class=\ "card-container\"></div>
<button id=\ "next\">&gt;</button>
<div class=\ "about\">
<h1>About</h1>
<p id=\ "changeme\">This is a release build, have fun with it.</p>
<h2>- vidner</h2>
<div>
<h3 id=\ "message\">Upgrade</h3>
<br>
<input id=\ "key\" name=\ "key\" placeholder=\ "password-that-you-guys-need-to-have\" type=\ "text\">
<button id=\ "upgrade\" name=\ "upgrade\"></button>
</div>
</div>
</main>
<script src=\ "./js/scripts.js\"></script>
</body>

</html>

Finally the HTML content is recovered from carving through the memory heaps after putting on the breakpoint in the _fini deconstructor. Yet we haven’t discovered the algorithm validation of the password since it’s located in the JS folder of the original source app. But it doesn’t actually matter since all the assets still remain in the memory region afterall, we’ll be using the string indication based on the input id, key.

Key Pattern Indication

You’ll notice something at the address from 0x555555ce3a67, 0x555555ce3a7a, 0x555555ce40e3, 0x555555ce40f5 and 0x555555ce4175. There’s a reference from the Javascript that it takes the value of our input and it does some algorithm validation in there. Let’s try to dump them as well.

gef➤  x/s 0x555555ce39db-443
0x555555ce3820: "lags/ukraine.png',\n title: 'ukraine',\n },\n {\n image: './flags/china.png',\n title: 'china',\n },\n {\n image: './flags/japan.png',\n title: 'japan',\n },\n {\n image: './flags/indonesia.png',\n title: 'indonesia',\n },\n {\n image: './flags/usa.png',\n title: 'usa',\n },\n {\n image: './flags/malaysia.png',\n title: 'malaysia',\n },\n {\n image: './flags/not-flag.png',\n title: 'not-flag'\n }\n];\n\nfunction rc4(key, str) {\n\tvar s = [], j = 0, x, res = '';\n\tfor (var i = 0; i < 256; i++) {\n\t\ts[i] = i;\n\t}\n\tfor (i = 0; i < 256; i++) {\n\t\tj = (j + s[i] + key.charCodeAt(i % key.length)) % 256;\n\t\tx = s[i];\n\t\ts[i] = s[j];\n\t\ts[j] = x;\n\t}\n\ti = 0;\n\tj = 0;\n\tfor (var y = 0; y < str.length; y++) {\n\t\ti = (i + 1) % 256;\n\t\tj = (j + s[i]) % 256;\n\t\tx = s[i];\n\t\ts[i] = s[j];\n\t\ts[j] = x;\n\t\tres += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);\n\t}\n\treturn res;\n}\n\nHTMLElement.prototype.empty = function() {\n while (this.firstChild) {\n this.removeChild(this.firstChild);\n }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n fillCards();\n const next = document.getElementById(\"next\");\n const prev = document.getElementById(\"prev\");\n next.addEventListener(\"click\", function () {\n const currCard = document.querySelector(\".card.view\");\n const nextCard = currCard.nextElementSibling\n ? currCard.nextElementSibling\n : document.querySelector(\".card-container\").firstElementChild;\n currCard.classList.remove(\"view\");\n nextCard.classList.add(\"view\");\n });\n\n prev.addEventListener(\"click\", function () {\n const currCard = document.querySelector(\".card.view\");\n const prevCard = currCard.previousElementSibling\n ? currCard.previousElementSibling\n : document.querySelector(\".card-container\").lastElementChild;\n currCard.classList.remove(\"view\");\n prevCard.classList.add(\"view\");\n });\n\n document.addEventListener(\"keydown\", function (e) {\n if (e.key === \"ArrowLeft\") prev.click();\n else if (e.key === \"ArrowRight\") next.click();\n else return false;\n });\n\n document.querySelector(\"#upgrade\").addEventListener(\"click\", function () {\n const key = document.getElementById(\"key\");\n const msg = document.querySelector(\"#message\");\n if (key.value === rc4(key.getAttribute(\"placeholder\"), atob(\"+KXDg64ffbAnFhbDbfvFqoK8jEOsod1qhvEXXPWzXnc2I5u/tkcf+eQ=\"))) {\n data[6].image = rc4(key.value, atob(\"sSWO4RshtePKZSKVyaGTLfZsFXGREULgLLgFgKtLweWHQWGz+oVm6qocDzecEGOA2aR5pg95NkibE2H0aA==\"));\n data[6].title = \"flag\";\n msg.textContent = \"Correct!!, now not-flag should evolve to a flag\",\n fillCards();\n } else {\n msg.textContent = \"Wrong password!!\";\n }\n });\n\n document.querySelector(\".btn\").addEventListener(\"click\", function () {\n const img = this.children[0];\n document.querySelector(\".about\").classList.toggle(\"view\");\n setTimeout(function () {\n img.setAttribute(\n \"src\",\n img.getAttribute(\"src\") === \"./images/icon-cross.svg\"\n ? \"./images/icon-hamburger.svg\"\n : \"./images/icon-cross.svg\"\n );\n }, 800);\n });\n});\n\nfunction fillCards() {\n const container = document.querySelector(\".card-container\");\n container.empty();\n data.forEach((data) => {\n const card = document.createElement(\"div\"),\n cardImage = document.createElement(\"div\"),\n img = document.createElement(\"img\"),\n url = document.createElement(\"a\");\n img.setAttribute(\"src\", data.image);\n img.setAttribute(\"alt\", data.title);\n url.textContent = data.title;\n card.classList.add(\"card\");\n cardImage.classList.add(\"card-image\");\n if (data.title === \"ukraine\") {\n card.classList.add(\"view\");\n }\n cardImage.appendChild(img);\n card.appendChild(cardImage);\n card.appendChild(url);\n container.appendChild(card);\n });\n}\n\005"

Although the dump is not that perfect, but we get the idea. It compares our input with a transformed data that is passed to the RC4 stream cipher with a hardcoded keys and from the input placeholder itself. We can write it into Javascript and pass the output to the Tauri App again and get the correct flag.

function rc4(key, str) {
var s = [],
j = 0,
x, res = '';
for (var i = 0; i < 256; i++) {
s[i] = i;
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
}
i = 0;
j = 0;
for (var y = 0; y < str.length; y++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
}
return res;
}


console.log(rc4("password-that-you-guys-need-to-have",atob("+KXDg64ffbAnFhbDbfvFqoK8jEOsod1qhvEXXPWzXnc2I5u/tkcf+eQ=")))

Brotli Decompression by Binary Instrumentation

The second method approach is more precise for this Tauri App as we’ll be combining the details of our background knowledge on how it stores our assets with Brotli compression by default.

Back to the static analysis with IDA, we know that there are several function calls to BrotliDecompressor.

Brotli Decompressor

Those calls are sorted and if we take a look at the final function call at the 0xBAF20, we may consider as it takes the final product of the decompressed content since it decompresses the stream that may considered as the assets while the upper address holds the functionality of the specific decompression subroutines.

So how do we check the arguments passed in the certain native function from the binary? Here comes the role of Frida as one of the Binary Instrumentation tools that we’ll be using. Frida uses a Javascript API called Interceptor to intercept the calls in the native function and we can play around either with the return value, patch some address inside the certain interval range, prints out register or arguments and et cetera, just like what it is told by the official Frida documentation below.

To begin with, the writer tries to debug what kind of information like arguments that’s passed inside this function. We also need to check how many arguments are passed from IDA.

__int64 __fastcall brotli_decompressor::decode::BrotliDecompressStream::h6fd4311faa5fd5ae(
_QWORD *a1,
unsigned __int64 *a2,
_BYTE *a3,
unsigned __int64 a4,
_QWORD *a5,
_QWORD *a6,
__int64 a7,
unsigned __int64 a8,
__int64 a9,
__int64 a10)

There are 10 arguments which are passed and we don’t know whether it’s a size, pointer of address or a bare value so let’s try to check all of them. We’ll use the Interceptor and also the address of brotli_decompressor::decode::BrotliDecompressStream::h6fd4311faa5fd5ae which will be rebased as well using another Javascript API Module.

Frida JS API Docs about Rebasing with Offset address
Interceptor.attach(Module.findBaseAddress("flag-viewer").add(0xbaf20), {
onEnter: function(args) {
console.log("[+] Hooked brotlidecompress!");
this.arg0 = args[0];
this.arg1 = args[1];
this.arg2 = args[2];
this.arg3 = args[3];
this.arg4 = args[4];
this.arg5 = args[5];
this.arg6 = args[6];
this.arg7 = args[7];
this.arg8 = args[8];
this.arg9 = args[9];

},
onLeave: function(retval) {

send(ptr(this.arg0));
send(ptr(this.arg1));
send(ptr(this.arg2));
send(ptr(this.arg3));
send(ptr(this.arg4));
send(ptr(this.arg5));
send(ptr(this.arg6));
send(ptr(this.arg7));
send(ptr(this.arg8));
send(ptr(this.arg9));
}
});

After preparing the script, we may execute it with Frida using the following command.

frida -f ./flag-viewer -l your_script_name.js
Hooked Address View and its returned data

If we check carefully, most of the arguments contain the pointer address but the fourth and the eighth arguments are not, instead it returns some kind of a size data. We shouldn’t read those since it’ll trigger an access violation. So what we’ll be reading is the pointed addresses that exclude the fourth and the eighth arguments. Later on, we can modify our Frida script into something like this.

Interceptor.attach(Module.findBaseAddress("flag-viewer").add(0xbaf20), {
onEnter: function(args) {
console.log("[+] Hooked brotlidecompress!");
this.arg0 = args[0];
this.arg1 = args[1];
this.arg2 = args[2];
this.arg3 = args[3];
this.arg4 = args[4];
this.arg5 = args[5];
this.arg6 = args[6];
this.arg7 = args[7];
this.arg8 = args[8];
this.arg9 = args[9];

},
onLeave: function(retval) {

try{
console.log(ptr(this.arg0).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg1).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg2).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg4).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg5).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg6).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg8).readCString());
}catch(err){
console.log("Exception!")
}
try{
console.log(ptr(this.arg9).readCString());
}catch(err){
console.log("Exception!")
}
}
});

Run the script once again and we’ll see all of the decompressed assets successfully, including the CSS, HTML and Javascript. We manage to leak the source code of the original application.

The writer would also like to thanks Maulvi Alfansuri, as the help of assisting the Frida JS script for this one after 2 days of the competition was over, he also had several tricks on how to deal with real world Android challenges by instrumenting them. Feel free to reach him out as well!

Key Takeaways

We know how lightweight is the app can be while using Tauri App as a choice, but as the matter of fact that a user or a developer who wants to migrate their infrastructure that consists of a web-components shall not store a hardcoded data that might be very sensitive. This demo concludes that we have the capability to leak your source code of the application since only a compression that was used to store the assets of the desired infrastructure application. This vulnerability by “human” is mentioned in CWE-798: Use of Hard-coded Credentials.

The writer gives the audience a choice to choose whether Electron.js or Tauri is the one that might be suitable for your application to migrate in a single Desktop Application. I hope you learn something new by reading this content and by the time this writing is done, Tauri App version is in 1.2. Perhaps their developer would also want to consider on strengthening their core infrastructure to gain much resiliency on reverse engineering such that they could implement more cryptographic implementation in order to protect the assets, or even try implementing an anti-reverse engineering approach as well.

Thankyou for reading this content! Subscribe & Follow me for more cybersecurity topics 😁.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Responses (2)

Write a response