Golang汇编之通过map地址找到value的值

文章目录

    • 背景
    • gdb调试Go程序
      • 为什么不用dlv
      • gdb调试Go可执行程序
      • gdb打印地址内容
    • go汇编快速入门
      • 常用的寄存器和用法
        • AMD64
        • ARM64
        • loong64
        • riscv64
      • Go汇编常用命令及含义
      • Go汇编和x86的区别
      • 找到map的赋值指令
    • Go中map的内存布局
      • gdb中查看map结构
      • map的存储结构
      • map的内存布局
      • 计算bmap偏移量
    • 根据map的地址获取key和value
      • 测试代码
      • 获取key和value的值
      • 操作指令介绍
        • 根据map地址获取buckets地址
        • 根据buckets地址获取keys的值
        • 根据keys地址获取values的值
    • 总结

背景

在逆向一个无符号可执行Go程序的时候,有个需求是获取map里面存储的值。但是只能拿到map的地址,以及知道key是string类型,其他的就不知道了。
那要怎么实现我们的需求呢?上GDB,读汇编,取地址。
下面是介绍一些前置知识,以及实操。实操部分的代码是写的测试代码,方便观看。

gdb调试Go程序

参考:
11.2. 使用 GDB 调试 | 第十一章. 错误处理,调试和测试 |《Go Web 编程》| Go 技术论坛
查看Golang程序的汇编的话,参考:Golang调试之GDB高级功能_go 实现的高级功能-CSDN博客
gdb调试Go程序的详细文档:GDB
GBD的多窗口管理和切换窗口: GDB调试之多窗口管理 (十二) - TechNomad - 博客园
Go的内存机制:一文彻底理解Go语言栈内存/堆内存 - 掘金

为什么不用dlv

不得不说,dlv要调试汇编的话,相比gdb来说还差点。特别是可视化还有取地址打印地址这块。
比如我想直接获取某个地址的值,或者打印寄存器的值之类的,确实不太方便,因此还是选择gdb来作为调试工具了。

当然,gdb调试Go程序有一个致命缺陷,那就是无法控制goroutine。这个问题目前博主还没找到解决方案,有类似经验的同学可以指点下博主,不胜感谢。

关于dlv可以参考: https://zhuanlan.zhihu.com/p/655096453

gdb调试Go可执行程序

这块网上文章比较多,大概介绍下怎么用gdb,以及使用到的命令。

开始调试

gdb demo # 开始调试可执行程序b main.main # 给main加断点run # 执行到断点处

源码和汇编窗口

layout src # 查看源码
layout asm # 查看汇编指令info win # 查看当前打开的窗口
focus cmd # 焦点回到cmd窗口,同样也可以回到源码或者汇编窗口
layout split # 分割窗口,同时打开cmd,源码,汇编框# 关闭窗口
tui disable

效果如下:
image.png

gdb打印地址内容

需要使用gdb中的x指令,通过help x来查看怎么使用。
格式: x /nuf
x 是 examine 的缩写

n 表示要显示的内存单元的个数
u 表示一个地址单元的长度:

  1. b 表示单字节
  2. h 表示双字节
  3. w 表示四字节
  4. g 表示八字节

f 表示显示方式, 可取如下值:

  1. x 按十六进制格式显示变量w
  2. d 按十进制格式显示变量
  3. u 按十进制格式显示无符号整型
  4. o 按八进制格式显示变量
  5. t 按二进制格式显示变量
  6. a 按十六进制格式显示变量
  7. si 指令地址格式
  8. c 按字符格式显示变量
  9. f 按浮点数格式显示变量

举例
x/3uh buf:表示从内存地址buf读取内容,h 表示以双字节为一个单位,3 表示三个单位,u 表示按十六进制显示
x/3gx addr: 从内存addr中,输出3个,g代表8个字节(int指针是8字节), x是十六进制的
x/144bx addr中 : 从内存,输出144个单字节的16进制数据
x/3cb addr: 从内存中,输出3个单字节,并且转换成字符的数据。一般是ascii

go汇编快速入门

参考:
go汇编学习: 初识Golang汇编
go文档的汇编解释:https://tip.golang.org/doc/asm
go文档的ABI定义: https://tip.golang.org/src/cmd/compile/abi-internal
通过上面的截图,我们看到了Go的汇编代码。大家基本在学习计算机的时候或多或少都会接触到汇编,不过Go的汇编会稍微有点区别,语法是基于Plan9的,Go 汇编器所用的指令,一部分与目标机器的指令一一对应,而另外一部分则不是,略有有点差异。
下面简单介绍一下汇编相关的知识,以及Go汇编的一些特性,顺带帮大家复习一波汇编知识。

常用的寄存器和用法

AMD64
amd64 架构使用以下 9 个寄存器序列来存储整数参数和结果:
RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11它使用 X0 – X14 来表示浮点参数和结果。

ARM64
arm64 架构使用 R0 – R15 来表示整数参数和结果。它使用 F0 – F15 来表示浮点参数和结果。

loong64
loong64 架构使用 R4 – R19 来表示整数参数和整数结果。它使用 F0 – F15 来表示浮点参数和结果。

riscv64
riscv64 架构使用 X10 – X17、X8、X9、X18 – X23 作为整数参数和结果。它使用 F10 – F17、F8、F9、F18 – F23 来表示浮点参数和结果。

Go汇编常用命令及含义

助记符指令种类用途示例
MOVQ传送数据传送MOVQ 48, AX // 把 48 传送到 AX
LEAQ传送地址传送LEAQ AX, BX // 把 AX 有效地址传送到 BX
PUSHQ传送栈压入PUSHQ AX // 将 AX 内容送入栈顶位置
POPQ传送栈弹出POPQ AX // 弹出栈顶数据后修改栈顶指针
ADDQ运算相加并赋值ADDQ BX, AX // 等价于 AX+=BX
SUBQ运算相减并赋值SUBQ BX, AX // 等价于 AX-=BX
CMPQ运算比较大小CMPQ SI CX // 比较 SI 和 CX 的大小
CALL转移调用函数CALL runtime.printnl(SB) // 发起调用
JMP转移无条件转移指令JMP 0x0185 //无条件转至 0x0185 地址处
JLS转移条件转移指令JLS 0x0185 //左边小于右边,则跳到 0x0185

Go汇编和x86的区别

go tool compile -S -N -l test_map.go
image.png

gdb调试中的汇编

TEXT runtime·profileloop(SB),NOSPLIT,$8MOVQ	$runtime·profileloop1(SB), CXMOVQ	CX, 0(SP)CALL	runtime·externalthreadhandler(SB)RET

主要区别如下:

  1. go的汇编里面有Q后缀,例如MOVQ,含义等同于MOV,Q的意思是64位的汇编指令
  2. 汇编读取的顺序和x86的汇编是反的
    1. 例如:MOVQ $5,CX = mov rcx,5 # 移动5到CX寄存器,go的汇编是从左到右。x86是从右向左
  3. go源码中也有用汇编实现的功能,有兴趣可以看看:/usr/lib/go/src/math/big

找到map的赋值指令

一开始是打算直接看汇编找到map的赋值操作,然后取寄存器里面的value。可惜失败了,简单测试了以下几种map的赋值:

  1. 直接赋值常量,类似于m[“hello”] = “world”
  2. 调用函数赋值,类似于m[“hello”] = getWrold()
  3. 变量赋值,类似于m[“hello”] = test
# 直接赋值常量
mov rdx, (rax)# 调用函数获取world
mov rax 0x88(rsp)# 通过变量的方式
mov rcx, (rax)

然后分别查看这几种赋值的汇编,发现跟逆向的那个程序都不一样,从寄存器里面取不到,也没找到类似的赋值指令。
那可咋办呢?是否可以根据map的内存布局来找到value呢?理论上来说map存储的时候key和value都是连续的,我们只要找到存储的buckets,然后就可以通过偏移量找到值了才对。

Go中map的内存布局

看到这个标题,我死去的八股文记忆开始攻击我。这里面的东西挺多的,不是本文的重点,因此下面借鉴源码和其他人的博客简单介绍下。

gdb中查看map结构

image.png
可以看到,默认的buckets有8个桶。跟八股文对上了,map的动态扩容,以及溢出检测。

map的存储结构

type hmap struct {count     intflags     uint8B         uint8noverflow uint16hash0     uint32buckets    unsafe.Pointeroldbuckets unsafe.Pointernevacuate  uintptrextra *mapextra
}# hmap的简单描述count : map中存储了几个元素
flags: 状态标识正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段)
B : 计算buckets的个数,2的B次方。比如B=2代表需要4个buckets
buckets: 指针,数组的类型为[]bmap,实际存储的数据在这里
hash0: hash因子
oldbuckets: 扩容时使用,存放扩容前的buckets
noverflow: 溢出桶里bmap大致的数量# 有数据时候的bmap
type bmap struct {tophash   [8]uint8keys        [8]keytypevalues     [8]valuetypepad         uintptroverflow uintptr
}# bmap的简单描述
tophash: 计算hash的,遍历时使用
keys,values: 存储键值对
pad: 内存对齐使用
overflow: 指向的 hmap.extra.overflow 溢出桶里的 bmap

map的内存布局

参考:https://www.edony.ink/deep-insight-of-golang-map-with-gdb-ebpf/
image.png

计算bmap偏移量

根据上面的截图,我们可以计算一个bmap占用多少字节。

# map定义: map[string]string
# string在go中是结构体,包含一个len和指向data的指针。
# 默认的指针和int都是8字节,因此一个string是16字节 8 +        # tophash
16 * 8 +   # key[8]
16 * 8 +    # value[8]
8          # overflow bmap pointer
= 272

计算偏移量是为了方面找到bmap中的数据。比如有了第一个bmap的地址之后,可以通过+272的方式,找到第二个bmap的地址。

根据map的地址获取key和value

终于到重点了,有了前置知识之后,我们就可以使用gdb来操作map的地址,通过偏移量计算来获取到实际的值。
测试代码是随便写的一个map,实际逆向程序的时候也是类似的原理。

测试代码

package mainfunc main() {m := make(map[string]string)m["hello1"] = getWorld("1")m["hello2"] = getWorld("2")m["hello3"] = getWorld("3")
}func getWorld(n string) string {return "world" + n
}

获取key和value的值

image.png

如图所示,我们根据map的地址就拿到了实际存储的key和value。

操作指令介绍

根据map地址获取buckets地址
x/3gx addr: 获取map中3个8字节的地址。第一个8字节: count(int=4字节) 第二个8字节:flags(uint8=1字节) + B(uint8=1字节) + noverflow + hash0(uint32=4字节)第三个8字节:buckets的入口地址

根据buckets地址获取keys的值
x/8gx addr: 获取buckets中8个8字节的数据,也就是8个int地址第一个8字节: tophash: 8个unit8 = 8字节第二个8字节: keys数组的入口x/s addr: 打印addr的字符串类型的值返回的是我们定义的hello1,hello2,hello3的值。# 为什么字符串这么多
内存中的字符串存储可能是连续的,除了我们存的24个字节之外,可能会有其他的数据在后面。
所以实际取字符串的值,应该是:地址+偏移量 

根据keys地址获取values的值
x/16gx addr + 16*8: 偏移量是因为keys是string类型,且有8个,所以要计算偏移量第一个8字节: 字符串的len第二个8字节: values的第一个值x/s addr: 依次打印地址对应的字符串即可

总结

这篇博客主要也是对于八股文的一次实践吧,类似的八股文大家肯定也都看到过,但是实践可能会少一些。博主刚开始也没想到通过map的内存布局去找到value,中间走了不少弯路,也学到了很多,值得记录并分享给大家。
可能博主做这个事情的动机大家无法参考,但是中间的过程,工具的使用还是很有意义的。也越发明白了刚入行听到的那句话“源码面前,了无秘密”。
所有的高级语言最终都会编译成汇编,机器也只能识别二进制的数据。那么掌握这些知识,就像是手握锤子一样,看什么都像钉子,遇到什么疑难杂症都先敲两下,不至于束手无策了。

end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/637844.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习-番外1】Win10系统搭建VSCode+Anaconda+Pytorch+CUDA深度学习环境和框架全过程

专栏的老读者们都知道,以前的文章以使用MATLAB的为多。 不过后续陆续开始展开深度学习算法的应用,就会逐渐引入Python语言了(当然MATLAB的代码也会同步更新),这是由于在深度学习领域,Python应用更为广泛。…

uni-app为图片添加自定义水印(升级版)

前置内容 uni-app为图片添加自定义水印&#xff08;解决生成图片不全问题&#xff09; UI 升级 现在水印样式变成这样了&#xff1a; 代码 <template><canvas v-if"waterMarkParams.display" canvas-id"waterMarkCanvas" :style"canv…

如何使用JSONB类型在PostgreSQL中存储和查询复杂的数据结构?

文章目录 解决方案1. 创建包含JSONB列的表2. 插入JSONB数据3. 查询JSONB数据4. 创建索引以优化查询性能 示例代码结论 在PostgreSQL中&#xff0c;JSONB是一种二进制格式的JSON数据类型&#xff0c;它允许你在数据库中存储和查询复杂的JSON数据结构。与普通的JSON类型相比&…

Springboot的Test单元测试操作

Springboot的Test单元测试操作 简单总结需要操作的步骤 1&#xff0c;导入依赖 2&#xff0c;创建目录&#xff08;目录和启动类的目录保持一致&#xff09; 3&#xff0c;添加注解 4&#xff0c;写方法测试 1&#xff0c;导入依赖 <dependency><groupId>org.spri…

Ts支持哪些类型和类型运算(下)

目录 1、条件判断 &#xff08;extends &#xff1f;&#xff09; 2、推导 infer 3、联合 | 4、交叉 & 5、映射类型 1、条件判断 &#xff08;extends &#xff1f;&#xff09; ts里的条件判断&#xff0c;语法为 T extends XXX ? true : false &#xff0c;叫做…

Docker - WEB应用实例

原文地址&#xff0c;使用效果更佳&#xff01; Docker - WEB应用实例 | CoderMast编程桅杆Docker - WEB应用实例 在之前的章节中&#xff0c;仅对普通容器进行了演示&#xff0c;但在实际中常常使用到 Docker 容器中的 WEB 应用程序。 运行一个WEB应用 拉取镜像 创建一个容器…

ROS机器人实战,对标古月老师HRMRP机器人(一)——机器人总体方案设计

咳咳&#xff01;这个是自己的毕业设计&#xff0c;内容比较多就拆开发。设计实现了一款SLAM移动机器人&#xff0c;加机械臂完成视觉识别抓取的&#xff0c;同时还有语音识别控制、QT上位机控制、Web网页控制。前几年看古月老师的视频&#xff0c;看到古月老师设计的HRMRP&…

Hadoop1X,Hadoop2X和hadoop3X有很大的区别么?

Hadoop的演进从Hadoop 1到Hadoop 3主要是为了提供更高的效率、更好的资源管理、更高的可靠性以及对更多数据处理方式的支持。下面是Hadoop 1, Hadoop 2, 和 Hadoop 3之间的主要区别和演进的原因&#xff1a; Hadoop 1 特点&#xff1a; 主要包括两大核心组件&#xff1a;HDFS&a…

【Hadoop】-HDFS的Shell操作[3]

目录 前言 一、HDFS集群启停命令 1.一键启停脚本可用 2.独立进程启停可用 二、文件系统操作命令 1、创建文件夹 2、查看指定目录下内容 3、上传文件到HDFS指定目录下 4、查看HDFS文件内容 5、下载HDFS文件 6、拷贝HDFS文件 7、追加数据到HDFS文件中 8、HDFS数据移…

【Python性能优化】list、array与set

list、array与set 详述测试代码 详述 本文对比 list 与 set 在插入和取值时的性能差异&#xff0c;以提供一条什么时候该选择什么数据类型的建议。先上结果&#xff1a; array 与 list 的不同&#xff1a; 内存方面 array 是 C array 的包装&#xff0c;它直接存储数据&#xf…

Sulley入门教学——简介、安装(Win7、VMware)

1、简介 Sulley 是由 Pedram Amini 和 Aaron Portnoy 开发的开源工具。它以 Python 编写&#xff0c;可以轻松地在不同平台上部署和使用。Sulley 提供了一个灵活且功能强大的框架&#xff0c;允许用户定义协议消息的结构、字段类型、边界条件和模糊测试策略。用户可以使用 Sul…

打破国外垄断|暴雨发布纯血国产电脑

要说现在国产手机这边已然进入纯自研模式&#xff0c;但电脑这边却还是仍未打破国外技术垄断。但就在刚刚&#xff0c;暴雨发布自研架构台式机open Station X &#xff0c;这是纯血鸿蒙系统之后国产又一款纯血产品发布&#xff01;标志的我们已经彻底打破西方在硬件及软件方面的…