Repository: pwndbg/pwndbg

Issue:
pwndbg might fail to show the state of the GOT of i386 libc
```console
$ cat a.c
#include <stdio.h>
int main(){puts("hello world");return 0;}
$ gcc -m32 a.c
$ gdb -q a.out -ex 'break main' -ex 'run' -ex 'got -p libc'
```

<img width="1514" alt="image" src="https://github.com/pwndbg/pwndbg/assets/61896187/e0492360-8c33-495a-aad1-99e0a91ad4c8">

The above error was triggered with i386 libc with version: `2.35-0ubuntu3.6`.


Discussion:

Resolving patch:
diff --git a/pwndbg/commands/got.py b/pwndbg/commands/got.py
--- a/pwndbg/commands/got.py
+++ b/pwndbg/commands/got.py
@@ -136,15 +136,23 @@ def _got(path: str, accept_readonly: bool, symbol_filter: str) -> None:
     # Parse the output of readelf line by line
     for category, lines in got_entry.items():
         for line in lines:
-            # line might be something like:
-            # 00000000001ec018  0000000000000025 R_X86_64_IRELATIVE                        a0480
-            # or something like:
-            # 00000000001ec030  0000020a00000007 R_X86_64_JUMP_SLOT     000000000009ae80 realloc@@GLIBC_2.2.5 + 0
-            offset, _, rtype, *rest = line.split()[:5]
-            if len(rest) == 1:
-                value = rest[0]
+            # There are 5 fields in the output of readelf:
+            # "Offset", "Info", "Type", "Sym. Value", and "Symbol's Name"
+            # We only care about "Offset", "Sym. Value" and "Symbol's Name" here
+            offset, _, _, *rest = line.split()[:5]
+            if len(rest) < 2:
+                # "Sym. Value" or "Symbol's Name" are not present in this case
+                # The output of readelf might look like this (missing both value and name):
+                # 00004e88  00000008 R_386_RELATIVE
+                # or something like this (only missing name):
+                # 00000000001ec018  0000000000000025 R_X86_64_IRELATIVE                        a0480
+                # TODO: Is it possible that we are missing the value but not the name?
+                value = rest[0] if rest else ""
                 name = ""
             else:
+                # Every fields are present in this case
+                # The output of readelf might look like this:
+                # 00000000001ec030  0000020a00000007 R_X86_64_JUMP_SLOT     000000000009ae80 realloc@@GLIBC_2.2.5 + 0
                 value, name = rest
             address = int(offset, 16) + bin_base_offset
             # TODO/FIXME: This check might not work correctly if we failed to get the correct vmmap result


Test patch:
diff --git a/tests/gdb-tests/tests/binaries/makefile b/tests/gdb-tests/tests/binaries/makefile
--- a/tests/gdb-tests/tests/binaries/makefile
+++ b/tests/gdb-tests/tests/binaries/makefile
@@ -38,7 +38,7 @@ GLIBC_2_33=$(PWD)/glibcs/2.33
 
 .PHONY : all clean
 
-CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out symbol_1600_and_752.out initialized_heap_x64.out initialized_heap_i386_big.out linked_lists.out
+CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out reference_bin_nopie.i386.out symbol_1600_and_752.out initialized_heap_x64.out initialized_heap_i386_big.out linked_lists.out
 
 all: $(LINKED) $(LINKED_ASM) $(COMPILED_GO) $(CUSTOM_TARGETS)
 
@@ -146,5 +146,9 @@ reference_bin_nopie.out: reference-binary.c
 	@echo "[+] Building reference_bin_nopie.out"
 	${ZIGCC} -fno-pie -o reference_bin_nopie.out reference-binary.c
 
+reference_bin_nopie.i386.out: reference-binary.c
+	@echo "[+] Building reference_bin_nopie.i386.out"
+	${ZIGCC} -fpie -target i386-linux-gnu -o reference_bin_nopie.i386.out reference-binary.c
+
 symbol_1600_and_752.out: symbol_1600_and_752.cpp
 	${CXX} -O0 -ggdb -Wno-pmf-conversions symbol_1600_and_752.cpp -o symbol_1600_and_752.out
diff --git a/tests/gdb-tests/tests/test_commands_elf.py b/tests/gdb-tests/tests/test_commands_elf.py
--- a/tests/gdb-tests/tests/test_commands_elf.py
+++ b/tests/gdb-tests/tests/test_commands_elf.py
@@ -11,6 +11,7 @@
 NO_SECTS_BINARY = tests.binaries.get("gosample.x86")
 PIE_BINARY_WITH_PLT = "reference_bin_pie.out"
 NOPIE_BINARY_WITH_PLT = "reference_bin_nopie.out"
+NOPIE_I386_BINARY_WITH_PLT = "reference_bin_nopie.i386.out"
 
 
 def test_commands_plt_gotplt_got_when_no_sections(start_binary):
@@ -105,12 +106,18 @@ def test_command_got_for_target_binary(binary_name, is_pie):
     assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out[4])
 
 
-def test_command_got_for_target_binary_and_loaded_library():
-    binary = tests.binaries.get(NOPIE_BINARY_WITH_PLT)
+@pytest.mark.parametrize(
+    "binary_name", (NOPIE_BINARY_WITH_PLT, NOPIE_I386_BINARY_WITH_PLT), ids=["x86-64", "i386"]
+)
+def test_command_got_for_target_binary_and_loaded_library(binary_name):
+    binary = tests.binaries.get(binary_name)
     gdb.execute(f"file {binary}")
 
     gdb.execute("break main")
-    gdb.execute("starti")
+    try:
+        gdb.execute("starti")
+    except gdb.error:
+        pytest.skip("Test not supported on this platform.")
 
     # Before loading libc, we can't find .got.plt of libc
     out = gdb.execute("got -p libc", to_string=True).splitlines()
@@ -120,7 +127,7 @@ def test_command_got_for_target_binary_and_loaded_library():
     assert out[2] == ""
     assert out[3] == "No shared library matching the path filter found."
     assert out[4] == "Available shared libraries:"
-    assert out[5].endswith("/ld-linux-x86-64.so.2")
+    assert out[5].endswith(("/ld-linux-x86-64.so.2", "/ld-linux.so.2"))
 
     gdb.execute("continue")
 
@@ -175,12 +182,12 @@ def test_command_got_for_target_binary_and_loaded_library():
         assert re.match(r"\[0x[0-9a-f]+\] .*ABS.* -> .*", out[6 + i])
 
     # Try filtering out path with "l", which should match every library
-    # First should be ld-linux-x86-64.so.2
+    # First should be ld-linux(-x86-64)?.so.2
     out = gdb.execute("got -p l", to_string=True).splitlines()
     assert out[0] == "Filtering by lib/objfile path: l"
     assert out[1] == "Filtering out read-only entries (display them with -r or --show-readonly)"
     assert out[2] == ""
-    assert re.match(r"State of the GOT of .*/ld-linux-x86-64.so.2:", out[3])
+    assert re.match(r"State of the GOT of .*/ld-linux(-x86-64)?.so.2:", out[3])
     m = re.match(
         r"GOT protection: (?:Partial|Full) RELRO \| Found (\d+) GOT entries passing the filter",
         out[4],
@@ -212,8 +219,8 @@ def test_command_got_for_target_binary_and_loaded_library():
     assert out[4] == ""
     out = out[5:]
 
-    # Second should be ld-linux-x86-64.so.2
-    assert re.match(r"State of the GOT of .*/ld-linux-x86-64.so.2:", out[0])
+    # Second should be ld-linux(-x86-64)?.so.2
+    assert re.match(r"State of the GOT of .*/ld-linux(-x86-64)?.so.2:", out[0])
     m = re.match(
         r"GOT protection: (?:Partial|Full) RELRO \| Found (\d+) GOT entries passing the filter",
         out[1],
@@ -243,7 +250,7 @@ def test_command_got_for_target_binary_and_loaded_library():
     assert out[3] == "GOT protection: Full RELRO | Found 1 GOT entries passing the filter"
     assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out[4])
     assert out[5] == ""
-    assert re.match(r"State of the GOT of .*/ld-linux-x86-64.so.2:", out[6])
+    assert re.match(r"State of the GOT of .*/ld-linux(-x86-64)?.so.2:", out[6])
     assert re.match(
         r"GOT protection: (?:Partial|Full) RELRO \| Found 0 GOT entries passing the filter", out[7]
     )