Repository: pwndbg/pwndbg
Pull request: add TLS canary address display
Reopen #2751 with fixes, as original author (@kgand) appears inactive

Fix:
- Corrected usage of `@pwndbg.commands.Command(...)` instead of `@pwndbg.commands.ArgparsedCommand(...)`
- Linting

Closes #2511 

Problem statement:
canary command should show the TLS canary address
```
pwndbg> canary
AT_RANDOM = 0x7fff05b46609 # points to (not masked) global canary value
Canary    = 0x989538cda22ba700 (may be incorrect on != glibc)
Thread 1: Found valid canaries.
```

The "points to (not masked) global canary value" is somewhat a lie, because this is pointer to `AT_RANDOM` in AUXV from where the global canary value is derived and stored in TLS (its first byte is zeroed). So yeah, we should clarify this and try to display the TLS address too.

Without this ppl have to do `search -8 0x989538cda22ba700` to find global canary address in TLS...

Hints from issue discussion:
Note the TLS address can probably be fetched via one of fs/fs_base/gs/gs_base register (I don't remember which one :shrug:, I think its FS register)

Commit message: fix: linting

Patch to review:
diff --git a/pwndbg/commands/canary.py b/pwndbg/commands/canary.py
index 4f24421fc46..adf78debfe7 100644
--- a/pwndbg/commands/canary.py
+++ b/pwndbg/commands/canary.py
@@ -5,6 +5,7 @@
 import pwndbg.aglib.memory
 import pwndbg.aglib.regs
 import pwndbg.aglib.stack
+import pwndbg.aglib.tls
 import pwndbg.auxv
 import pwndbg.commands
 import pwndbg.commands.telescope
@@ -14,8 +15,25 @@
 
 DEFAULT_NUM_CANARIES_TO_DISPLAY = 1
 
+# Architecture-specific TLS canary offsets
+# These offsets are from the TLS base to the canary
+# References:
+# - x86_64: fs:0x28 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/x86_64/nptl/tls.h)
+# - i386: gs:0x14 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/i386/nptl/tls.h)
+# - aarch64: tpidr_el0 + 0x28 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/aarch64/nptl/tls.h)
+TLS_CANARY_OFFSETS = {
+    "x86-64": 0x28,
+    "i386": 0x14,
+    "aarch64": 0x28,
+}
+
 
 def canary_value():
+    """Get the global canary value from AT_RANDOM.
+
+    Returns:
+        tuple: (canary_value, at_random_addr) or (None, None) if not found
+    """
     at_random = pwndbg.auxv.get().AT_RANDOM
     if at_random is None:
         return None, None
@@ -28,6 +46,36 @@ def canary_value():
     return global_canary, at_random
 
 
+def find_tls_canary_addr():
+    """Find the address of the canary in the Thread Local Storage (TLS).
+
+    The canary is stored at a fixed offset from the TLS base, which varies by architecture.
+    The TLS base can be accessed through architecture-specific registers:
+    - x86_64: fs register
+    - i386: gs register
+    - aarch64: tpidr_el0 register
+
+    Returns:
+        int: The virtual address of the canary in TLS, or None if not found/supported
+    """
+    arch = pwndbg.aglib.arch.name
+
+    # Get TLS base address
+    tls_base = (
+        pwndbg.aglib.tls.find_address_with_register()
+        or pwndbg.aglib.tls.find_address_with_pthread_self()
+    )
+    if not tls_base:
+        return None
+
+    # Get architecture-specific offset
+    offset = TLS_CANARY_OFFSETS.get(arch)
+    if offset is None:
+        return None
+
+    return tls_base + offset
+
+
 parser = argparse.ArgumentParser(description="Print out the current stack canary.")
 parser.add_argument(
     "-a",
@@ -40,15 +88,30 @@ def canary_value():
 @pwndbg.commands.Command(parser, command_name="canary", category=CommandCategory.STACK)
 @pwndbg.commands.OnlyWhenRunning
 def canary(all) -> None:
+    """Display information about the stack canary, including its location in TLS and any copies found on the stack."""
     global_canary, at_random = canary_value()
 
     if global_canary is None or at_random is None:
         print(message.error("Couldn't find AT_RANDOM - can't display canary."))
         return
 
-    print(
-        message.notice("AT_RANDOM = %#x # points to (not masked) global canary value" % at_random)
-    )
+    print(message.notice("AT_RANDOM = %#x # points to global canary seed value" % at_random))
+
+    # Get and display the TLS canary address
+    tls_addr = find_tls_canary_addr()
+    if tls_addr is not None:
+        print(message.notice("TLS Canary = %#x # address where canary is stored" % tls_addr))
+
+        # Verify the value at the TLS address matches our computed canary
+        try:
+            tls_canary = pwndbg.aglib.memory.pvoid(tls_addr) & (pwndbg.aglib.arch.ptrmask ^ 0xFF)
+            if tls_canary != global_canary:
+                print(message.warn("Warning: TLS canary value doesn't match global canary!"))
+        except Exception:
+            print(message.warn("Warning: Could not read TLS canary value"))
+    else:
+        print(message.warn("Note: Could not determine TLS canary address for current architecture"))
+
     print(message.notice("Canary    = 0x%x (may be incorrect on != glibc)" % global_canary))
 
     found_canaries = False


Code review comments:
diff_hunk: @@ -40,15 +88,30 @@ def canary_value():
 @pwndbg.commands.Command(parser, command_name="canary", category=CommandCategory.STACK)
 @pwndbg.commands.OnlyWhenRunning
 def canary(all) -> None:
+    """Display information about the stack canary, including its location in TLS and any copies found on the stack."""
     global_canary, at_random = canary_value()
 
     if global_canary is None or at_random is None:
         print(message.error("Couldn't find AT_RANDOM - can't display canary."))
         return
 
-    print(
-        message.notice("AT_RANDOM = %#x # points to (not masked) global canary value" % at_random)
-    )
+    print(message.notice("AT_RANDOM = %#x # points to global canary seed value" % at_random))
path: pwndbg/commands/canary.py
text: @user1:
Can we use f'string-formatting'?

diff_hunk: @@ -14,8 +15,25 @@
 
 DEFAULT_NUM_CANARIES_TO_DISPLAY = 1
 
+# Architecture-specific TLS canary offsets
+# These offsets are from the TLS base to the canary
+# References:
+# - x86_64: fs:0x28 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/x86_64/nptl/tls.h)
path: pwndbg/commands/canary.py
text: @user1:
Can we change the links to not be 'latest' but to latest current version? This way we know when it worked last time if it ever changes

diff_hunk: @@ -14,8 +15,25 @@
 
 DEFAULT_NUM_CANARIES_TO_DISPLAY = 1
 
+# Architecture-specific TLS canary offsets
+# These offsets are from the TLS base to the canary
+# References:
+# - x86_64: fs:0x28 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/x86_64/nptl/tls.h)
+# - i386: gs:0x14 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/i386/nptl/tls.h)
+# - aarch64: tpidr_el0 + 0x28 (https://elixir.bootlin.com/glibc/latest/source/sysdeps/aarch64/nptl/tls.h)
+TLS_CANARY_OFFSETS = {
+    "x86-64": 0x28,
+    "i386": 0x14,
+    "aarch64": 0x28,
+}
+
 
 def canary_value():
+    """Get the global canary value from AT_RANDOM.
path: pwndbg/commands/canary.py
text: @user1:
```suggestion
    """Get the global canary value from AT_RANDOM with its last byte masked (as glibc does)
```