修改进程状态:修改寄存器
实现目标:修改寄存器数据
我们已经展示过如何读取并且修改寄存器数据了,比如continue命令执行时,如果当前PC-1处是软件断点0xCC,我们需要重置断点并且设置寄存器PC=PC-1。
和当时设置PC=PC-1相同,我们这里用到的寄存器修改方法仍然是通过ptrace(PTRACE_SET_REGS,...)
。所不同的是本小节要实现一个通用的寄存器修改命令 setreg <registerName> <value>
。
当高级语言代码被构建完成后就变成了一系列的机器指令,机器指令的操作数可以是立即数、内存地址、寄存器编号。我们在使用符号级调试器的时候,有时候会改变变量值(迭代变量、函数参数、函数返回值等等)来控制程序执行逻辑。其实在指令级调试时,也是有这样的需求去修改内存中的数据、寄存器中的数据,所以我们需要有修改内存命令setmem、修改寄存器命令setreg命令。
ps: 当然从易用性角度来说,可以使用一个set命令来实现setmem、setreg、setvar,但是我们是为了教学目的,所以每个操作最好相对独立,这样逻辑清晰简单、篇幅也更简短。
代码实现
godbg中的实现也非常简单,接收用户输入的寄存器名args[0]、要设置的值args[1],然后通过 syscall.PtraceGetRegs(...)
操作拿到所有寄存器的值regs,并通过反射找到代表对应寄存器名的字段(如regs.rax),并修改字段值,最后将修改后的regs再通过 syscall.PtraceSetRegs(...)
设置回寄存器。
package debug
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/hitzhangjie/godbg/pkg/target"
"github.com/spf13/cobra"
)
var setRegCmd = &cobra.Command{
Use: "setreg <reg> <value>",
Short: "设置寄存器值",
Annotations: map[string]string{
cmdGroupAnnotation: cmdGroupInfo,
},
RunE: func(cmd *cobra.Command, args []string) error {
// 检查参数数量
if len(args) != 2 {
return errors.New("usage: setreg <reg> <value>")
}
// 检查是否有调试进程
if target.DBPProcess == nil {
return errors.New("please attach to a process first")
}
regName := strings.ToLower(args[0])
valueStr := args[1]
// 解析值参数
value, err := strconv.ParseUint(valueStr, 0, 64)
if err != nil {
return fmt.Errorf("invalid value format: %s", valueStr)
}
// 读取当前寄存器状态
regs, err := target.DBPProcess.ReadRegister()
if err != nil {
return fmt.Errorf("failed to read registers: %v", err)
}
// 使用反射设置寄存器值
rv := reflect.ValueOf(regs).Elem()
rt := reflect.TypeOf(*regs)
var fieldFound bool
for i := 0; i < rv.NumField(); i++ {
fieldName := strings.ToLower(rt.Field(i).Name)
if fieldName == regName {
// 设置新值
rv.Field(i).SetUint(value)
fieldFound = true
// 写回寄存器
err = target.DBPProcess.WriteRegister(regs)
if err != nil {
return fmt.Errorf("failed to write register %s: %v", regName, err)
}
break
}
}
if !fieldFound {
return fmt.Errorf("invalid register name: %s", regName)
}
return nil
},
}
func init() {
debugRootCmd.AddCommand(setRegCmd)
}
代码测试
首先我们先执行一个简单的测试:
$ while [ 1 -eq 1 ]; do echo $$; sleep 1; done
1521639
1521639
1521639
1521639
1521639
1521639
1521639 <= godbg attach 1521639
然后我们执行调试跟踪:
root🦀 ~ $ godbg attach 1521639
process 1521639 attached succ
process 1521639 stopped: true
godbg>
godbg> pregs <= pregs显示当前寄存器信息,其中R12=0x1
Register R15 0x7ffd8a1e55e0
Register R14 0x0
Register R13 0x7ffd8a1e56b0
Register R12 0x1
Register Rbp 0x0
Register Rbx 0xa
Register R11 0x246
Register R10 0x0
...
godbg> setreg r12 0x2 <= 执行setreg命令修改R12=0x2
godbg> pregs <= 再次查看当前寄存器信息,R12=0x2,修改成功
Register R15 0x7ffd8a1e55e0
Register R14 0x0
Register R13 0x7ffd8a1e56b0
Register R12 0x2
Register Rbp 0x0
Register Rbx 0xa
Register R11 0x246
Register R10 0x0
...
godbg>
OK,这个测试演示了调试精灵setreg基本的用法和执行效果。
有的读者可能会想,什么情况下我需要显示修改寄存器,真有这种情景吗?下面咱们就来看一个相对更实际的案例。
代码测试2: 篡改返回值跳出循环
:) 无法修改返回变量值来跳出循环
我们先实现一个测试程序,该测试程序每隔1s打印一下进程pid,for-loop的循环条件是一个固定返回true的函数loop(),我们想通过修改寄存器的方式来篡改函数调用loop()
的返回值来实现。
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
}
这里的挑战点在于,for loop() {}
而不是 for v := true; v ; v = loop() {}
,在loop函数体内部是 return true
而不是 v := true; return v
。我们既不能通过 set <var> <value>
来修改loop返回值的值,也不能修改函数体内部return的变量值。
此时我们只能在返回前修改ret指令的操作数的值,或者loop函数调用返回后修改返回值寄存器的值。修改ret指令的操作数寄存器也可以,我们这里演示修改返回值寄存器RAX。
写个程序模拟下篡改返回值的操作
TODO: 改成使用godbg进行调试,代替这里冗长的单文件测试。
下面是我们写的调试程序,它首先attach被调试进程,然后提示我们获取并输入loop()函数调用的返回地址,然后它就会通过添加断点、运行到该断点位置,然后调整寄存器RAX的值(loop()返回值就存在RAX),再然后恢复执行,我们将看到程序跳出了循环。
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
}
代码测试
测试方法:
1、首先我们准备一个测试程序,loop3.go,该程序每隔1s输出一下pid,循环由固定返回true的loop()函数控制
详见 testdata/loop3.go
。
2、按照ABI调用惯例,这里的函数调用loop()的返回值会通过RAX寄存器返回,所以我们想在loop()函数调用返回后,通过修改RAX寄存器的值来篡改返回值为false。
那我们先确定下loop()函数的返回地址,这个只要我们通过dlv调试器在loop3.go:13添加断点,然后disass,就可以确定返回地址为 0x4af15e。
确定完返回地址后我们即可detach tracee,恢复其执行。
(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
3、如果我们不加干扰,loop3会每隔1s不停地输出pid信息。
$ ./loop3
pid: 4946
pid: 4946
pid: 4946
pid: 4946
pid: 4946
...
zhangjie🦀 testdata(master) $
4、现在运行我们编写的调试工具 ./16_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 <= 我们篡改了返回值为0
...
pid: 4946
pid: 4946
pid: 4946 <= 因为篡改了loop()的返回值为false,循环跳出,程序结束
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 <== 返回值是用eax来存的
loop3.go:21 0x4af277 4883c408 add rsp, 0x8
loop3.go:21 0x4af27b 5d pop rbp
loop3.go:21 0x4af27c c3 ret
至此,通过这个实例演示了如何设置寄存器值,我们将在 hitzhangjie/godbg 中实现godbg> set reg value
命令来修改寄存器值。
本节小结
本节我们也介绍了如何修改寄存器的值,也通过具体实例演示了通过修改寄存器来篡改函数返回值的案例,当然你如果对栈帧构成了解的够细致,结合读写寄存器、内存操作,也可以修改函数调用参数、返回地址。