Android Kernel Privilege Escalation
Escalate Android shell to root by patching kernel credential structures live.
Introduction
30 Second Summary
Every app on your phone trusts the kernel to decide who gets access to what. When an attacker can reach the memory structure that tracks a process's identity, they can rewrite who they are.
In this project, you will attach a debugger to a live Android kernel running in a virtual machine. You will walk its internal process list to find your own shell process, then rewrite the credential structure that controls your shell's identity to escalate from an unprivileged user to full root.
What You'll Build
You run id and see uid=2000(shell), patch eight fields in kernel memory, then run id again to see uid=0(root) gid=0(root).
By the end of this project, you'll have:
- A working kernel debugging lab with GDB on Kali Linux attached to a live Android kernel through VMware's debug stub, letting you inspect any kernel data structure in real time.
- The ability to walk the kernel's process linked list from init_task, locating any running process by PID or name.
- A full privilege escalation from uid=2000(shell) to uid=0(root) achieved by patching all eight credential fields in kernel memory.
- Secret Mission: Automate the entire kill chain with a GDB Python script that finds a process, reads its credentials, patches them, and reports success.
Are there any prerequisites?
This project assumes familiarity with Linux kernel concepts, C programming, and basic GDB usage. You should already have a Kali Linux VM and an Android-x86 9.0 VM running on VMware Workstation.
Before We Start
You are about to reach into a live operating system's kernel memory and rewrite the structures that control who has access to what. Before you touch anything, let's lock in exactly what you are building and why it matters.
Wire Up the Kernel Debugging Lab
GDB cannot attach to a virtual machine's kernel out of the box. VMware Workstation includes a hidden debug stub that exposes the guest CPU and memory over a GDB-compatible protocol. It requires explicit configuration in the VM's .vmx file before it will accept connections.
In this step, you will enable VMware's debug stub, boot Android with stable kernel addresses, and connect GDB from your Kali VM to the running Android kernel. By the end, you will have a live debugging bridge directly into kernel memory.
In this step, get ready to:
- Enable the VMware debug stub on your Android VM.
- Boot Android-x86 with KASLR disabled for stable kernel addresses.
- Connect GDB from Kali to the live Android kernel and verify the connection.
Enable the VMware debug stub on the Android VM
VMware's debug stub is disabled by default. Adding three lines to the VM's configuration file opens a GDB-compatible port on your host machine, letting another VM connect and inspect the guest's CPU state and memory.
- Shut down the Android-x86 VM completely. Use Power Off, not Suspend. The .vmx file cannot be safely edited while the VM is running or suspended.
- On your Windows host, navigate to the Android VM's folder. This is typically located at C:\Users\<your-username>\Documents\Virtual Machines\<VM name>\.
- Right-click the file ending in .vmx and open it with a text editor such as Notepad.
- Scroll to the bottom of the file and add these three lines:
debugStub.listen.guest64 = "TRUE"
debugStub.listen.guest64.remote = "TRUE"
debugStub.hideBreakpoints = "TRUE"
What do these lines do?
debugStub.listen.guest64 = "TRUE" enables the GDB debug stub for 64-bit guests. It opens port 8864 on the host by default.
debugStub.listen.guest64.remote = "TRUE" allows connections from machines other than localhost. This is what lets your Kali VM connect across the VMware network.
debugStub.hideBreakpoints = "TRUE" uses hardware breakpoints instead of software INT3 instructions, preventing the guest kernel from detecting debugger-inserted breakpoints.
- Save the file and close the text editor.
- Power the Android-x86 VM back on in VMware Workstation.
- While the VM boots, open vmware.log in the same folder as the .vmx file. Search for the text debugStub to confirm the stub is active.
You should see a log line containing debugStub that confirms the stub is listening. Note the port number listed. It defaults to 8864 for 64-bit guests.
Don't see debugStub in the log?
- Make sure you fully powered off the VM before editing. Suspended VMs lock the .vmx file and ignore new settings.
- Double-check the three lines for typos. Every character matters, including the quotes around TRUE.
- Confirm you edited the correct .vmx file. If you have multiple VMs, each has its own .vmx in a separate folder.
Help me troubleshoot the VMware debug stub configuration.
Configure Android-x86 for kernel debugging
The Android kernel randomizes its address layout on every boot by default. This feature is called KASLR (Kernel Address Space Layout Randomization). With KASLR active, the symbol addresses you extract from the kernel image will not match the runtime addresses, making debugging impossible.
- When the GRUB boot menu appears on the Android VM, press e to edit the default boot entry.
- Find the line that starts with kernel. If you are using GRUB legacy, select the kernel line and press e again to edit it. If you are using GRUB2, you can edit it directly.
- Go to the end of the kernel line and append a space followed by nokaslr.
- Press Enter to accept the edit. Then press b to boot (GRUB legacy) or Ctrl+X to boot (GRUB2).
Why nokaslr?
KASLR shifts the entire kernel's virtual address space by a random offset each boot. Disabling it with nokaslr keeps kernel addresses at their compile-time values, so the symbol addresses you extract later will match exactly what GDB sees in memory.
Wait for Android to finish booting. You will see the Android home screen or lock screen.
- On the Android VM, press Alt+F1 to open a console terminal. If that does not work, use the Terminal app from the app drawer.
- Get the Android VM's IP address by running:
ip addr
- Look for an inet address on the eth0 or wlan0 interface. Record this IP address:
your Android VM's IP address
- Switch to your Kali VM. Connect to the Android VM over ADB by running:
adb connect [[ANDROID_IP="your Android VM's IP address"]]:5555
- Verify the connection by running:
adb devices
You should see your Android VM listed as a connected device with the IP address and port 5555.
ADB connection refused or device not listed?
- Make sure both VMs are on the same VMware network (NAT or Bridged). Check your VMware virtual network settings.
- Verify the IP address. Run ip addr again on the Android VM and make sure you copied the correct inet address (not the loopback 127.0.0.1).
- Android-x86 runs the ADB daemon on the network by default. If the connection still fails, try restarting the ADB server on Kali with adb kill-server and then reconnecting.
Help me get ADB connected to my Android-x86 VM.
Verify GDB connectivity from Kali
The debug stub is listening on the Windows host, and ADB confirms the Android VM is reachable. The final piece is connecting GDB from Kali directly to the Android kernel through VMware's debug stub.
- First, determine your Windows host's IP address as seen from Kali. Run this on Kali:
ip route | grep default
- The gateway IP shown is typically your VMware host's address on the NAT network. Record it:
your Windows host IP as seen from Kali
Finding the right host IP
If both VMs use NAT mode, the default gateway is the host. If you use Bridged networking, find the Windows host's IP from a command prompt on Windows with ipconfig. Use the IP on the same subnet as your Kali VM.
Before you run the next command, think about what you expect to happen. GDB will try to connect over the network to port 8864 on the host, where the debug stub is listening. What do you think GDB will show if the connection succeeds?
- Start GDB and connect to the Android kernel by running:
gdb -q -ex "set architecture i386:x86-64" -ex "target remote [[HOST_IP="your Windows host IP as seen from Kali"]]:8864"
What does this command do?
gdb -q starts GDB in quiet mode, suppressing the startup banner.
set architecture i386:x86-64 tells GDB the target is a 64-bit x86 CPU, so it decodes instructions correctly.
target remote <host-ip>:8864 connects to VMware's debug stub. The moment GDB attaches, it pauses the entire Android VM.
GDB should connect and display the current instruction pointer or a register dump. The Android VM will freeze because GDB has halted the guest CPU.
- Confirm GDB is reading the kernel by running:
info registers
You should see a full register dump including rax, rbx, rip, and other x86-64 registers with kernel-space addresses (typically starting with 0xffff).
- Resume the Android VM so it unfreezes by running:
c
The Android VM resumes normal operation. GDB stays connected in the background, ready for you to pause it again with Ctrl+C whenever you need to inspect kernel memory.
GDB connection timed out or refused?
- Verify the host IP. Run ip route | grep default on Kali again and confirm the gateway address matches what you used.
- Check that the debug stub port matches. Open vmware.log on the Windows host and search for the actual port number the stub is using. It defaults to 8864 but can vary.
- Make sure Windows Firewall is not blocking port 8864. Add an inbound rule to allow TCP traffic on that port if needed.
- Confirm the Android VM is powered on and running (not suspended or shut down). The debug stub only accepts connections while the VM is active.
Help me connect GDB to the VMware debug stub.
Your kernel debugging lab is live. GDB on Kali is attached to a running Android kernel through VMware's debug stub, and you can pause, inspect, and resume the kernel at will. Next up, you will use this bridge to read live kernel memory and discover the data structures that track every process's identity.
Breach the Wall - Read Live Kernel Memory
You are trapped. Your ADB shell runs as uid 2000, a low-privilege user locked out of system files and kernel addresses. Before you can break out, you need to see exactly what the kernel is using to keep you caged.
Your GDB connection gives you unrestricted visibility into the running Android kernel's memory. In this step, you'll use that access to locate init_task, the kernel's root process, and map out the credential structure that defines who gets root privileges.
In this step, get ready to:
- Confirm your unprivileged state and see exactly what uid 2000 cannot access.
- Extract kernel symbols from the Android VM's kernel image.
- Map the init_task structure in live memory to discover the credential offsets you'll use for escalation.
Confirm your unprivileged state
Before you can appreciate what kernel-level access gives you, you need to see the walls around your current shell. Three quick commands will show exactly what uid 2000 can and cannot do.
- Open a new terminal window on your Kali VM.
- Start an ADB shell session by running:
adb shell
- Check your current identity by running:
id
You see uid=2000(shell) gid=2000(shell). This is the default ADB shell user. You are not root.
- Try to access a protected directory by running:
ls /data/data/
You see "Permission denied". The /data/data/ directory holds application data that only root or specific app UIDs can read.
- Try to read kernel symbol addresses by running:
cat /proc/kallsyms | head
Every address shows as 0000000000000000. The kernel's kptr_restrict mechanism replaces real addresses with zeros for unprivileged users.
What is kptr_restrict?
The Linux kernel uses kptr_restrict to control whether kernel addresses are visible in /proc/kallsyms and similar interfaces. When set to a non-zero value, unprivileged users see all addresses replaced with zeros. This prevents userspace programs from learning the kernel's memory layout.
You can still see the symbol names, though. Scroll through the output and note the names init_task and init_cred. You know what exists in the kernel, but you can't see where it lives. Your GDB connection can.
Extract kernel symbols from the Android VM
The kernel image stored on the Android VM contains all the symbol names and their compile-time addresses. You need to pull this image to Kali and extract it so nm can read the symbol table.
- In your ADB shell, find the kernel image path by running:
ls /android-*/kernel
Note the path that appears. If no result is found, try ls /boot/kernel instead.
- Exit the ADB shell by running:
exit
- Pull the kernel image to your Kali working directory by running:
adb pull [[KERNEL_PATH="path from the ls output, e.g. /android-9.0-r2/kernel"]] ./android-kernel
- Confirm the file arrived by running:
ls -lh android-kernel
You should see the android-kernel file listed with its size.
adb pull failed?
- Double-check the path you found in the ADB shell matches what you pasted into the pull command.
- If ADB reports the device is offline, reconnect with adb connect using the Android VM's IP address.
- Help me troubleshoot the adb pull command.
The pulled file is a compressed kernel image. You need to extract the uncompressed ELF binary from it so nm can read its symbol table.
- Extract the vmlinux ELF binary by running:
extract-vmlinux android-kernel > vmlinux
- Confirm the extraction worked by running:
file vmlinux
You should see output identifying vmlinux as an ELF 64-bit executable. This is the uncompressed kernel binary with its full symbol table.
extract-vmlinux: command not found?
The extract-vmlinux script ships with the Linux kernel source tree. If it is not available on your Kali VM, you can download it from the kernel source repository under scripts/extract-vmlinux. Download it, make it executable with chmod +x, and run it with ./extract-vmlinux android-kernel > vmlinux.
Help me get the extract-vmlinux script working.
Now extract the two addresses you need for the rest of this project.
- Get the init_task address by running:
nm vmlinux | grep -w init_task
- Record the hex address printed in the first column: your init_task address
- Get the init_cred address by running:
nm vmlinux | grep -w init_cred
- Record the hex address printed in the first column: your init_cred address
What are init_task and init_cred?
Every process in the Linux kernel is represented by a task_struct. The init_task is the very first one. It is the idle process (PID 0, named "swapper") and the head of the kernel's linked list of all processes.
The init_cred is the credential structure used by init_task. Because init_task runs as root, every identity field in init_cred is zero. You'll use these known-zero values as a reference point to map out the credential layout.
Examine init_task and discover struct offsets
You now have the addresses. It is time to look at the raw bytes of init_task in live kernel memory and figure out where the key fields live inside the structure.
- Switch to the terminal running your GDB session from Step 1.
- Pause the Android VM by pressing Ctrl+C in GDB.
- Dump the first 800 bytes of init_task by running this GDB command:
x/100gx [[INIT_TASK_ADDR="your init_task address"]]
GDB prints 100 eight-byte values starting at your init_task address. This is the raw memory layout of the kernel's first task_struct.
What am I looking at?
Each line shows a memory address and its contents as 64-bit hex values. You are reading the kernel's in-memory representation of init_task. Somewhere in this dump are the pointers and fields you need to find: the process linked list pointer (tasks.next), the credential pointer (cred), and the process name (comm).
Now scan the dump for three landmarks. Work through these one at a time.
- Find the credential pointers. Search the dump for your init_cred address. It appears twice consecutively because task_struct has two adjacent pointer fields: real_cred and cred. The byte offset from the start of init_task to the second pointer is your cred offset.
- Find the process name. Look for the ASCII hex encoding of "swapper" (0x72657070617773 in little-endian). The byte offset from init_task to this string is your comm offset.
- Find the linked list pointer. Near the beginning of the structure, look for a kernel-range address that is not init_cred. This is tasks.next. Its byte offset from init_task is your tasks offset.
- Record the offsets you discovered:
- Tasks offset: byte offset of tasks.next in task_struct
- Cred offset: byte offset of cred pointer in task_struct
- Comm offset: byte offset of comm field in task_struct
How do I calculate byte offsets from the hex dump?
Subtract your init_task address from the address shown on the left of the line where you found the value. Then add 8 bytes for each column position to the right. For example, if init_task is at 0xffffffff81c13480 and you find init_cred at address 0xffffffff81c13780 in the second column, the offset is 0x780 - 0x480 + 8 = 0x308.
The pid field sits between the tasks and cred fields. Once you know the tasks and cred offsets, examine the area between them for a 4-byte integer. For init_task (PID 0), this value is zero, so look for a small gap of zeros near the tasks pointer.
Before you examine init_cred, what do you expect to see in the credential fields for the kernel's root process?
- Examine the init_cred structure by running this GDB command:
x/16wx [[INIT_CRED_ADDR="your init_cred address"]]
The first 4 bytes are the usage counter (a nonzero reference count). The next 32 bytes are eight consecutive 32-bit zeros. These are uid, gid, suid, sgid, euid, egid, fsuid, and fsgid. All zero means all root.
What is struct cred?
The struct cred is the kernel's record of a process's identity. It starts with a 4-byte usage counter, followed by eight consecutive 4-byte fields: uid, gid, suid, sgid, euid, egid, fsuid, fsgid. Each field is a different facet of the process's identity.
For init_cred (root), every identity field is zero. For your ADB shell (uid 2000), you'll see 0x000007d0 (decimal 2000) repeated across those fields. That is the exact set of bytes you'll patch to zero in a later step.
- Record the pid offset you identified between the tasks and cred fields: byte offset of pid in task_struct
- Resume the Android VM by running c in GDB and pressing Enter.
Can't find the init_cred address in the dump?
- Make sure you are comparing the full 64-bit hex value from nm output against the values in the GDB dump. GDB's x/gx format shows full 64-bit values, so the address should appear as-is.
- If the dump is too short, extend it: x/200gx will show 1600 bytes instead of 800.
- Confirm you booted with nokaslr. If KASLR is active, the runtime addresses won't match the nm output.
- Help me find the cred pointer offset in the hex dump.
You have a complete map of the kernel's credential system. You know where processes live, how they link together, and exactly which bytes define root. In the next step, you'll use these offsets to hunt down your own shell process and make your first attempt at privilege escalation.
Hunt Your Process and Attempt Escalation
You proved in the last step that you can see everything inside the kernel. But init_task is the idle process with PID 0. Its credentials are already root. That does not help you.
To escalate YOUR privileges, you need to find the task_struct belonging to your ADB shell process. The kernel keeps every running process in a circular linked list rooted at init_task. You will walk this list the same way the kernel's scheduler does, jumping from one process to the next until you find your shell.
In this step, get ready to:
- Identify your shell process by walking the kernel's task_struct linked list from init_task.
- Read your process's credential structure to see your current identity in kernel memory.
- Attempt a privilege escalation by patching a single credential field.
Identify your shell process
Every process on the Android system has a task_struct in the kernel. To find yours, you first need to know what to look for.
- In your ADB shell on Kali, get your shell's PID by running:
echo $$
- Record the PID number that appears: your shell PID
Now you need to find that PID inside the kernel. The kernel stores all processes in a circular linked list threaded through the tasks field of each task_struct.
What is the container_of pattern?
The tasks.next pointer does NOT point to the beginning of the next task_struct. It points to the tasks member inside the next struct.
To get the base address of the next task_struct, you subtract the tasks offset from the pointer value. This is exactly what the kernel's container_of() macro does thousands of times per second.
- Switch to your GDB terminal and press Ctrl+C to pause the Android VM.
- Store your offsets and starting address in GDB convenience variables by running these commands:
set $tasks_off = [[TASKS_OFFSET="your tasks offset, e.g. 0x2e8"]]
set $pid_off = [[PID_OFFSET="your pid offset, e.g. 0x448"]]
set $comm_off = [[COMM_OFFSET="your comm offset, e.g. 0x5e0"]]
set $cred_off = [[CRED_OFFSET="your cred offset, e.g. 0x5f8"]]
set $task = [[INIT_TASK_ADDR="your init_task address"]]
What does this do?
GDB convenience variables (prefixed with $) persist across commands. Storing your offsets once means you can reference $tasks_off instead of retyping a hex number every time you take a step in the linked list.
- Walk to the first process in the list by running these GDB commands:
set $next = *(unsigned long *)($task + $tasks_off) - $tasks_off
x/s $next + $comm_off
x/wx $next + $pid_off
What does this do?
- The first line reads the tasks.next pointer from the current task, then subtracts the tasks offset to get the struct's base address. That is the container_of pattern in action.
- x/s displays the process name (the comm field) as a string.
- x/wx displays the PID as a 32-bit hex value.
You should see a process name (like init or kthreadd) and its PID. This is not your shell yet. You need to keep walking.
- Advance to the next process and check its identity by running:
set $task = $next
set $next = *(unsigned long *)($task + $tasks_off) - $tasks_off
x/s $next + $comm_off
x/wx $next + $pid_off
- Keep repeating this same block of four commands. Each time, GDB prints the next process's name and PID.
- Stop when you see your PID (the number you recorded from echo $$) or see sh in the comm field.
- Once found, set $task to the matching process by running:
set $task = $next
Can't find your process?
- If you see garbage characters in the comm field or impossibly large PID numbers, double-check the offsets you recorded in Step 2. Even one wrong byte in an offset shifts all reads.
- If you loop back to swapper (init_task) without finding your PID, your ADB shell may have exited. Switch to the ADB terminal, run echo $$ again to confirm the PID, then restart the walk.
- Multiple processes may show sh in the comm field. Match by PID, not just name.
Need help troubleshooting your linked list walk?
Read your process's credentials
You found your shell's task_struct in kernel memory. Now you can read the credential structure the kernel uses to decide what your process is allowed to do.
- Read the cred pointer from your shell's task_struct by running:
x/gx $task + $cred_off
- GDB prints the memory address stored in the cred pointer. This is the address of your process's credential structure.
- Store it in a GDB convenience variable and examine the credential structure by running:
set $cred = *(unsigned long *)($task + $cred_off)
x/12wx $cred
What do these values mean?
The first 32-bit word is the usage counter (how many references point to this credential structure). After that, you see 0x000007d0 repeated. That is decimal 2000. Your process's uid, gid, suid, sgid, euid, egid, fsuid, and fsgid are all set to 2000.
Compare this to init_cred from Step 2, where all eight fields were zero. That is the difference between your shell user and root. It comes down to these 32 bytes.
Seeing unexpected values?
- If you see all zeros, you may have accidentally read init_cred instead of your shell's cred. Confirm $task is your shell's task_struct (check the comm field again with x/s $task + $comm_off).
- If the values look random, double-check your cred offset from Step 2.
Help me verify I'm reading the correct credential structure for my shell process.
Attempt a naive privilege escalation
You can see your identity in kernel memory. The uid field is 2000. What happens if you just overwrite it with 0?
- Patch the uid field (4 bytes into the credential structure) to zero by running:
set *(unsigned int *)($cred + 4) = 0
What does this command do?
This casts the address $cred + 4 as a pointer to an unsigned 32-bit integer and writes the value 0. The + 4 skips past the 4-byte usage counter to reach the first credential field: uid.
- Resume the Android VM so your shell can execute commands again by running:
c
Before you check the result, think about what you expect. You changed the uid to 0. Do you think you now have full root access? What about the other seven credential fields?
- Switch to your ADB shell terminal and check your identity by running:
id
You see uid=0(root) but gid=2000(shell) and euid=2000(shell). Your real UID changed, but your effective UID did not.
- Test whether this partial change is enough by running:
ls /data/data/
Permission denied. Your single-field patch was not enough.
Why didn't the single patch work?
Linux maintains four pairs of credentials for every process: real, effective, saved, and filesystem. Each pair has a user ID and a group ID. That is eight fields total.
The kernel uses the effective UID (euid) for access control decisions, not the real UID. File operations check the filesystem UID (fsuid). Saved credentials allow privilege restoration. You changed only one field out of eight.
Real kernel exploits like CVE-2023-20938 call commit_creds(prepare_kernel_cred(NULL)), which replaces ALL credential fields at once. You need to do the same thing manually.
Your uid says root, but the kernel knows better. The effective UID is still 2000, and that is what the kernel actually checks. Next, you will patch all eight credential fields to achieve full, unrestricted root.
Complete the Kill - Patch All Credential Fields
You found your shell process in the kernel's task list and patched its uid to zero. But id revealed the gap: your real UID changed to root while your effective UID stayed at 2000, and file access to /data/data/ was still denied.
The kernel checks the effective UID for access control, not the real UID. To achieve full root, you need to zero out all eight credential fields in one sweep.
In this step, get ready to:
- Map the full credential layout inside struct cred.
- Patch all eight identity fields to zero through GDB.
- Verify full root privileges with file access and kernel symbol visibility.
Understand the full credential layout
Linux maintains four pairs of credentials for every process, covering both user and group identity. Each pair serves a different purpose in the kernel's security checks.
Starting at offset 4 from the base of struct cred (right after the 4-byte usage counter), the eight fields are:
- uid (offset 4) and gid (offset 8): the real user and group IDs. These record who launched the process.
- suid (offset 12) and sgid (offset 16): the saved IDs. These let a process drop privileges temporarily and restore them later.
- euid (offset 20) and egid (offset 24): the effective IDs. The kernel checks these for permission on every syscall.
- fsuid (offset 28) and fsgid (offset 32): the filesystem IDs. The kernel checks these specifically for file operations.
Eight consecutive 32-bit fields, 32 bytes total. All must be zero for full root.
Why do real exploits replace all eight fields?
Real Android kernel exploits like CVE-2023-20938 call commit_creds(prepare_kernel_cred(NULL)). This kernel function creates a brand-new credential structure copied from init_cred (where every field is zero for root) and replaces the current task's credentials atomically.
Your naive patch in the previous step only changed uid, leaving euid at 2000. The kernel used euid for the access check on /data/data/ and denied you. Now you will do manually what commit_creds does in code: zero every field.
Patch all eight credential fields to zero
Your cred pointer address from Step 3 points to the exact memory location holding your shell's identity. You will store it in a GDB convenience variable, then overwrite all eight fields in one sequence.
- Switch to your GDB terminal and press Ctrl+C to pause the Android VM.
- Set a convenience variable for your cred pointer address and zero all eight fields by running these commands:
set $cred = [[CRED_PTR="your cred pointer address from Step 3"]]
set *(unsigned int *)($cred + 4) = 0
set *(unsigned int *)($cred + 8) = 0
set *(unsigned int *)($cred + 12) = 0
set *(unsigned int *)($cred + 16) = 0
set *(unsigned int *)($cred + 20) = 0
set *(unsigned int *)($cred + 24) = 0
set *(unsigned int *)($cred + 28) = 0
set *(unsigned int *)($cred + 32) = 0
What do these commands do?
The $cred convenience variable stores your cred pointer address so you do not have to retype the full hex address eight times.
Each set command writes a 32-bit zero to one credential field. The fields run from offset 4 (uid) through offset 32 (fsgid), covering all 32 bytes. The uid at offset 4 is already zero from your Step 3 patch. Writing zero again is harmless.
- Resume the Android VM by running:
c
Seeing "Cannot access memory" in GDB?
Double-check that you pressed Ctrl+C before running the set commands. GDB can only write to kernel memory while the VM is paused.
Verify your cred pointer address is correct by re-reading it: x/gx <your_task_struct + cred_offset>. If the address changed, your shell process may have been replaced. Run echo $$ in ADB to confirm the PID, then re-walk the task list to find the new cred pointer.
Still stuck? Help me debug a GDB memory write error when patching kernel credentials on an Android-x86 VM through VMware's debug stub.
Verify full privilege escalation
In Step 3, patching only uid gave you uid=0(root) but euid=2000(shell). You just zeroed all eight fields. Before you check, what do you expect id to show this time?
- Switch to your ADB shell terminal and check your identity by running:
id
You should see uid=0(root) gid=0(root) followed by your groups list. Every credential field is now zero. The kernel sees your shell as root across every security check.
- Test file access that was denied before by running:
ls /data/data/
You should see a list of installed application data directories. These directories were off-limits moments ago when your euid was 2000.
- Confirm that the kernel now trusts you with its own secrets by running:
cat /proc/kallsyms | head
You should see real kernel addresses next to symbol names. In Step 2, these were all zeros because kptr_restrict hides them from unprivileged users. Now that you are root, the kernel reveals its full symbol map.
What just happened?
You performed the same fundamental operation that every real Android kernel exploit achieves. Exploits like CVE-2023-20938 (a use-after-free in the Binder driver) and CVE-2019-2215 corrupt kernel memory to call commit_creds(prepare_kernel_cred(NULL)). That function does exactly what you did by hand: it creates a new struct cred copied from init_cred (all zeros for root) and replaces the task's credentials.
The difference is that those exploits achieve their memory write through a vulnerability. You achieved yours through a debugger. The target data structure and the result are identical.
Still seeing gid=2000 or euid=2000?
One or more of your eight set commands may have been skipped or mistyped. Pause the VM again with Ctrl+C in GDB, then verify all eight fields by running x/10wx $cred. After the first 4-byte usage counter, you should see eight consecutive zeros. Re-run any set command for fields that still show 0x000007d0 (decimal 2000).
If your ADB shell session disconnected while the VM was paused, reconnect with adb shell. The new shell will have a new PID and a fresh struct cred with uid 2000. You will need to re-walk the task list in GDB to find the new process and patch it.
Need help? Help me verify my credential patch on an Android-x86 kernel.
Secret mission
Automate the Kill Chain with a GDB Python Script
You just escalated privileges by typing GDB commands one at a time. Professional exploit developers automate their primitives into repeatable tools. Your mission: write a GDB Python script that walks the kernel task list, finds any process by name or PID, and patches its credentials to root in a single command.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on local VMs, so there are no ongoing cloud costs or subscriptions to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- VMware debug stub configuration (3 lines added to the Android VM's .vmx file on the Windows host).
- Extracted kernel files on Kali (android-kernel and vmlinux).
- GDB Python script (escalate.py) on Kali.
What about the kernel patches and GRUB edit?
The credential patches you wrote into kernel memory are volatile. They disappear the moment the Android VM reboots. The nokaslr parameter you added in the GRUB editor was also a one-time, per-boot edit. It resets to the default entry on the next boot.
Neither of these changes persist across reboots, so there is nothing to undo for them.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with kernel debugging or want to refine your escalate.py script.
- The debug stub lines stay in your .vmx file so you can reconnect GDB at any time.
- The extracted kernel files (android-kernel, vmlinux) remain on Kali for quick symbol lookups.
- Remember that your kernel credential patches reset every time the Android VM reboots. You will need to re-apply nokaslr in GRUB and re-run the escalation each session.
Pause - I'll come back to this later
Shut down both VMs to free up memory and CPU. Your files and configuration stay on disk for next time.
- In the GDB session on Kali, type quit to disconnect from the debug stub.
- In your ADB terminal on Kali, run adb disconnect to close the ADB connection.
- In VMware Workstation, shut down the Android-x86 VM and the Kali Linux VM.
- The debug stub lines in your .vmx file, the extracted kernel files, and escalate.py all remain on disk. When you return, boot both VMs, add nokaslr in GRUB again, and reconnect GDB.
Delete - I don't want to use this again
Remove all project-specific configuration and files. This does not delete the VMs themselves, only the changes and files this project created.
- Quit GDB on Kali by typing quit and disconnect ADB by running adb disconnect.
- Shut down the Android-x86 VM in VMware Workstation.
- On the Windows host, open the Android VM's .vmx file in a text editor. Remove the three debug stub lines you added:
debugStub.listen.guest64 = "TRUE"
debugStub.listen.guest64.remote = "TRUE"
debugStub.hideBreakpoints = "TRUE"
- Save and close the .vmx file.
- On Kali, delete the extracted kernel files and the escalation script by running:
rm android-kernel vmlinux escalate.py
- Confirm the files are gone by running ls in the same directory. You should no longer see android-kernel, vmlinux, or escalate.py listed.
Nice Work!
Nice Work!
You just escalated an Android system from an unprivileged shell user to full root by directly rewriting kernel memory. That is the exact endgame technique behind real-world Android kernel exploits.
You learned how to:
- Built a kernel debugging lab by connecting GDB on Kali to a live Android kernel through VMware's debug stub, and inspected raw kernel data structures in real time.
- Walked the kernel's task_struct linked list from init_task, located a running process by PID and name, and read its live credential structure to see how the kernel tracks process identity.
- Performed a full privilege escalation from uid 2000 to uid 0 by patching all eight struct cred fields, discovering firsthand why patching only the real UID is insufficient and how effective, saved, and filesystem UIDs control access.
- Secret Mission: Automated the entire escalation chain into a reusable GDB Python script that walks the process list, finds a target, patches all credential fields, and confirms root.
Ready to quiz yourself?