Modifying Process State (Registers)
Implementation Goal: Modifying Register Data
Before continuing execution after hitting a breakpoint, we need to restore the instruction data at PC-1 and modify the register PC=PC-1. We have already demonstrated how to read and modify register data, but its modification action is built into the continue
debugging command. Here, we need a general debugging command set <register> <value>
. OK, we indeed need such a debugging command, especially for instruction-level debuggers, where the operands of instructions are either immediate values, memory addresses, or registers. We will implement this debugging command to modify any register in godbg
. However, this section will focus on explaining the necessity of mastering this operation and how to implement it through specific examples.
Code Implementation
We will first implement a test program that prints the process pid every 1 second. The loop condition of the for-loop is a function loop()
that always returns true. We want to modify the return value of the function call loop()
by changing the register value.
package main
import (
"fmt"
"os"
"runtime"
"time"
)
func main() {
runtime.LockOSThread()
for loop() {
fmt.Println("pid:", os.Getpid())
time.Sleep(time.Second)
}
}
//go:noinline
func loop() bool {
return true
}
Below is the debugging program we wrote. It first attaches to the debugged process, then prompts us to obtain and input the return address of the loop()
function call. It then adds a breakpoint, runs to that breakpoint location, adjusts the value of the RAX register (the return value of loop()
is stored in RAX), and then resumes execution. We will see the program exit the loop.
package main
import (
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"syscall"
"time"
)
var usage = `Usage:
go run main.go <pid>
args:
- pid: specify the pid of process to attach
`
func main() {
runtime.LockOSThread()
if len(os.Args) != 2 {
fmt.Println(usage)
os.Exit(1)
}
// pid
pid, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
if !checkPid(int(pid)) {
fmt.Fprintf(os.Stderr, "process %d not existed\n\n", pid)
os.Exit(1)
}
// step1: supposing running dlv attach here
fmt.Fprintf(os.Stdout, "===step1===: supposing running `dlv attach pid` here\n")
// attach
err = syscall.PtraceAttach(int(pid))
if err != nil {
fmt.Fprintf(os.Stderr, "process %d attach error: %v\n\n", pid, err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "process %d attach succ\n\n", pid)
// check target process stopped or not
var status syscall.WaitStatus
var options int
var rusage syscall.Rusage
_, err = syscall.Wait4(int(pid), &status, options, &rusage)
if err != nil {
fmt.Fprintf(os.Stderr, "process %d wait error: %v\n\n", pid, err)
os.Exit(1)
}
if !status.Stopped() {
fmt.Fprintf(os.Stderr, "process %d not stopped\n\n", pid)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "process %d stopped\n\n", pid)
regs := syscall.PtraceRegs{}
if err := syscall.PtraceGetRegs(int(pid), ®s); err != nil {
fmt.Fprintf(os.Stderr, "get regs fail: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "tracee stopped at %0x\n", regs.PC())
// step2: supposing running `dlv> b <addr>` and `dlv> continue` here
time.Sleep(time.Second * 2)
fmt.Fprintf(os.Stdout, "===step2===: supposing running `dlv> b <addr>` and `dlv> continue` here\n")
// read the address
var input string
fmt.Fprintf(os.Stdout, "enter return address of loop()\n")
_, err = fmt.Fscanf(os.Stdin, "%s", &input)
if err != nil {
fmt.Fprintf(os.Stderr, "read address fail\n")
os.Exit(1)
}
addr, err := strconv.ParseUint(input, 0, 64)
if err != nil {
panic(err)
}
fmt.Fprintf(os.Stdout, "you entered %0x\n", addr)
// add breakpoint and run there
var orig [1]byte
if n, err := syscall.PtracePeekText(int(pid), uintptr(addr), orig[:]); err != nil || n != 1 {
fmt.Fprintf(os.Stderr, "peek text fail, n: %d, err: %v\n", n, err)
os.Exit(1)
}
if n, err := syscall.PtracePokeText(int(pid), uintptr(addr), []byte{0xCC}); err != nil || n != 1 {
fmt.Fprintf(os.Stderr, "poke text fail, n: %d, err: %v\n", n, err)
os.Exit(1)
}
if err := syscall.PtraceCont(int(pid), 0); err != nil {
fmt.Fprintf(os.Stderr, "ptrace cont fail, err: %v\n", err)
os.Exit(1)
}
_, err = syscall.Wait4(int(pid), &status, options, &rusage)
if err != nil {
fmt.Fprintf(os.Stderr, "process %d wait error: %v\n\n", pid, err)
os.Exit(1)
}
if !status.Stopped() {
fmt.Fprintf(os.Stderr, "process %d not stopped\n\n", pid)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "process %d stopped\n\n", pid)
// step3: supposing change register RAX value from true to false
time.Sleep(time.Second * 2)
fmt.Fprintf(os.Stdout, "===step3===: supposing change register RAX value from true to false\n")
if err := syscall.PtraceGetRegs(int(pid), ®s); err != nil {
fmt.Fprintf(os.Stderr, "ptrace get regs fail, err: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "before RAX=%x\n", regs.Rax)
regs.Rax &= 0xffffffff00000000
if err := syscall.PtraceSetRegs(int(pid), ®s); err != nil {
fmt.Fprintf(os.Stderr, "ptrace set regs fail, err: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "after RAX=%x\n", regs.Rax)
// step4: let tracee continue and check it behavior (loop3.go should exit the for-loop)
if n, err := syscall.PtracePokeText(int(pid), uintptr(addr), orig[:]); err != nil || n != 1 {
fmt.Fprintf(os.Stderr, "restore instruction data fail: %v\n", err)
os.Exit(1)
}
if err := syscall.PtraceCont(int(pid), 0); err != nil {
fmt.Fprintf(os.Stderr, "ptrace cont fail, err: %v\n", err)
os.Exit(1)
}
}
// checkPid check whether pid is valid process's id
//
// On Unix systems, os.FindProcess always succeeds and returns a Process for
// the given pid, regardless of whether the process exists.
func checkPid(pid int) bool {
out, err := exec.Command("kill", "-s", "0", strconv.Itoa(pid)).CombinedOutput()
if err != nil {
panic(err)
}
// output error message, means pid is invalid
if string(out) != "" {
return false
}
return true
}
Code Testing
Testing method:
First, we prepare a test program,
loop3.go
, which outputs the pid every 1 second, with the loop controlled by theloop()
function that always returns true. Seetestdata/loop3.go
for details.According to the ABI calling convention, the return value of the function call
loop()
will be returned through the RAX register. Therefore, we want to modify the return value to false by changing the value of the RAX register after theloop()
function call returns.We first determine the return address of the
loop()
function. This can be done by adding a breakpoint atloop3.go:13
using thedlv
debugger, then disassembling, and we can determine the return address as0x4af15e
.After determining the return address, we can detach the tracee and resume its execution.
(dlv) disass
Sending output to pager...
TEXT main.main(SB) /home/zhangjie/debugger101/golang-debugger-lessons/testdata/loop3.go
loop3.go:10 0x4af140 493b6610 cmp rsp, qword ptr [r14+0x10]
loop3.go:10 0x4af144 0f8601010000 jbe 0x4af24b
loop3.go:10 0x4af14a 55 push rbp
loop3.go:10 0x4af14b 4889e5 mov rbp, rsp
loop3.go:10 0x4af14e 4883ec70 sub rsp, 0x70
loop3.go:11 0x4af152 e8e95ef9ff call $runtime.LockOSThread
loop3.go:13 0x4af157 eb00 jmp 0x4af159
=> loop3.go:13 0x4af159* e802010000 call $main.loop
loop3.go:13 0x4af15e 8844241f mov byte ptr [rsp+0x1f], al
...
(dlv) quit
Would you like to kill the process? [Y/n] n
- If we do not interfere,
loop3
will continuously output the pid information every 1 second.
$ ./loop3
pid: 4946
pid: 4946
pid: 4946
pid: 4946
pid: 4946
...
zhangjie🦀 testdata(master) $
- Now run our debugging tool
./15_set_regs 4946
.
$ ./15_set_regs 4946
===step1===: supposing running `dlv attach pid` here
process 4946 attach succ
process 4946 stopped
tracee stopped at 476263
===step2===: supposing running `dlv> b <addr>` and `dlv> continue` here
enter return address of loop()
0x4af15e
you entered 4af15e
process 4946 stopped
===step3===: supposing change register RAX value from true to false
before RAX=1
after RAX=0 <= we changed retvalue to zero
...
pid: 4946
pid: 4946
pid: 4946 <= we changed retvalue, so loop stop
zhangjie🦀 testdata(master) $
(dlv) disass
TEXT main.loop(SB) /home/zhangjie/debugger101/golang-debugger-lessons/testdata/loop3.go
loop3.go:20 0x4af260 55 push rbp
loop3.go:20 0x4af261 4889e5 mov rbp, rsp
=> loop3.go:20 0x4af264* 4883ec08 sub rsp, 0x8
loop3.go:20 0x4af268 c644240700 mov byte ptr [rsp+0x7], 0x0
loop3.go:21 0x4af26d c644240701 mov byte ptr [rsp+0x7], 0x1
loop3.go:21 0x4af272 b801000000 mov eax, 0x1 <== retvalue save in eax
loop3.go:21 0x4af277 4883c408 add rsp, 0x8
loop3.go:21 0x4af27b 5d pop rbp
loop3.go:21 0x4af27c c3 ret
Through this example, we have demonstrated how to set register values. We will implement the godbg> set reg value
command in hitzhangjie/godbg to modify register values.
Summary
In this section, we introduced how to modify register values and demonstrated a case of tampering with function return values by modifying registers. Of course, if you have a thorough understanding of stack frame composition, combined with reading and writing registers and memory operations, you can also modify function call parameters and return addresses.