gameguardian
lua基本知识
lua的变量默认是全局变量,局部变量声明 local 变量名;变量默认都是nil;变量名的开头,只能是: 字母 _ ;不需要事先声明类型
字符串声明 '' "",连接字符串 ..,如"你好".."吗" '搜出'..k..'个结果'
井字符 # 返回字符串或表的长度,单位是 字节 bit,#555 返回值为3,#abcde 返回值为5,#“你好” 返回值是6,#’www.baidu.com‘ 返回值是13
不等于~=
多变量赋值 a,b,c,d,e=1,2,3,4,5,若a,b,c=1,2则c为nil
换行 \n 作为转义字符,必须用在字符串中, 更直观的说就是写在引号中.否则会报错
判断语句
if 判断的条件 then 语句块 end
1 | if 条件1 then |
循环语句
1 | while 满足的条件 do |
1 | repeat 循环语句块 |
1 | for 变量=初始值,终止值,步长 do |
表
1,表用大括号{ } 来构造 . 可以是多维的.例如二维表 { { } }
2,表是一个关联数组.每个值也可称为元素
3,表有类似于门牌号的索引,称为”键”
4,表默认的键,是正整数,1,2,3,4,5……
5,表可以自定义键名,取名规则跟变量名一样
6,表中的每一个元素,都会有一个对应的,唯一的键
7,表中元素的删除,就是把该元素设为nil
8,表可以传递, 跟变量的赋值一样
9,表的元素有多少个,可以用#来获取.但不一定准确
1 | k={ '华硕' ,'微星' , '技嘉' , '七彩虹' } |
1 | 表可以是多维的 |
gg函数库
1.区分大小写,使用数字、英文-符号
2.一个函数有多个参数,指定某个参数,则之前的参数都必须写出来,之后的可省略
例如某函数有7个参数,当你指定第三个参数,则第一、二参数必须写出,但可以不写四五六七
gg.toast(string text, bool fast=false)
瞬时的消息提醒
两个参数,参数1是提示的内容;参数2是布尔值,false大约显示2s,true 1s,默认false
gg.toast(‘你好’)
注意:连续无间隔使用多个toast,只能显示最后一句,如:
gg.toast(“55”) gg.toast(“77”) 这样运行后, 只能看见77的提示
gg.alert (string text, string positive=’ok’, string negative=nil, string neutral=nil)
对话框
显示1,2,3个按钮的置顶的弹出式对话框,自带一个“确定”的按钮
返回值取决于按下了哪个按钮:右1,中2,左3,取消0
参数1是提示信息的字符串文本内容;参数2右侧按钮,可填字符串或nil,默认显示“确定”;
参数3中间偏右侧按钮,默认值是nil,不显示;参数4左侧按钮,默认值是nil,不显示
1 | a=gg.alert('3060显卡的今日售价应该多少?','2499元','3799元','50元') |
内存区域
gg.setRanges(int ranges)
设置GG进行搜索的内存区域范围,共有15种内存,建议用数字进行选择,如Ca=4
选多种内存用 | 隔开,gg.setRanges(4|32),设置为Ca和A
GG默认勾选前七种,和为262207

gg.getRanges(int ranges)
获取内存区域,返回值为int
搜索&修改
gg.searchNumber(string text, int type=gg.TYPE_AUTO, bool encrypted=false, int sign=gg.SIGN_EQUAL, long memoryFrom=0, long memoryTo=-1, long limit=0)
参数1:字符串。是要搜索的数值,写数字、数组,也可以是一个范围
参数2:八种数值类型,可用数字来表示

参数3:布尔值。默认是false,表示“此值不加密”,true表示“此值加密”
参数4:标志。形如gg.SIGN_,默认是gg.SIGN_EQUAL,意思是“标记相同”,用默认值即可
参数5:开始搜索的内存地址,默认是0,为不限制开始
参数6:结束搜索的内存地址,默认是-1,为不限制结束
参数7:数字。是指定搜索出多少个结果后停止搜索,默认是0,表示搜索所有结果
如果搜索列表非空,则是改善搜索
1 | gg.searchNumber('700',4) |
gg.clearResults()
清除搜索结果
gg.getResultsCount()
返回搜索结果个数
1 | local y=gg.getResultsCount() |
gg.getResults (int maxCount, int skip=0, long addressMin=nil, long addressMax=nil, string valueMin=nil, string valueMax=nil, int type=nil, string fractional=nil, int pointer=nil)
读取、勾选搜索结果
读取对应的“地址”、“数值”、“数值类型”,存储为二维表的形式
参数1:数字,希望读取的结果数量
参数2:想跳过忽略前面的多少结果,默认值是0
参数3:address的最小值
参数4:address的最大值
参数5:value的最小值
参数6:value的最大值
参数7:八种数值常量,默认值为nil
参数8:按分数值过滤
参数9:五种指针常量,默认值是nil

返回值是一个二维表,第二维的表有3个键,address flags value
键address 是十六进制的地址,0x开头
键flags 是八种数值类型,返回的是数字形式
键value 是数值
gg.loadResults(table results)
重新加载结果
从已知的表中加载搜索的结果,原来的搜索结果将被清除
返回true或字符串错误
gg.removeResults(table results)
删除指定加载的结果,其他的留下
参数1:通过getResults得到的表
返回值:true或字符串错误
string.format(参数1, 参数2)
参数1:转义符
参数2:需要转化的数字
通常用于将地址转化为十六进制,如:
1 | y=string.format( "%X",123456789) |
1 | 常用转义符: |
1 | gg.clearResults() |
gg.editAll(string value, int type)
修改getResults得到的表对应的value键值
就是说,gg.getResults()接着下一句是gg.editAll,二者是连载一起用的
参数1:字符串。就是想改成什么,可以写数字、数组
参数2:八种数值常量
返回值:改动的值的数量或字符串错误
将所有搜索结果的值改成9999:
1 | gg.clearResults() |
只改前3个,把数值分别改成1,2,3
1 | gg.clearResults() |
gg.getValues(table values)
用于获取内存地址存储的值
参数1:二维表。必须含有address、flags、value三个键的二维表
返回值:含有address、flags、value三个键的二维表或字符串错误
1 | local k={{ address=0x1, flags=4,value=nil }} --value只是凑数的,可以随意 |
获取某地址附近的值:
1 | for i=0,15 do |
1 | gg.clearResults() |
gg.setValues(table values)
参数1:二维表。必须是含有address、flags、value三个键的二维表
返回值:true或字符串错误
1 | gg.clearResults() |
1 | --已知特征码 "30029;12131;25975;16943;27764;28781::21" ,是W类型.在Ca内存. |
特征码
唯一双浮点数
1 | if gg.getResultsCount()==0 then |
gg.addListItems(table items)
参数:二维表。第二维的表最多可以写8个键,
(1) address (2) flags (3) value (4) freeze (5) name (6) freezeType (7)freezeFrom (8) freezeTo
至少要写出address和flags这两个键
(4) freeze 用于表示是否要冻结,freeze=true是冻结,freeze=false是不冻结
如果想修改value,则必须写freeze=true
如果写成freezeType=gg.FREEZE_IN_RANGE 指定范围,则必须写出freezeFrom和freezeTo
返回值:true或字符串错误
(6) freezeType 冻结方式有四种(可以用相应的数字表示):
gg.FREEZE_NORMAL 锁定(数字0)
gg.FREEZE_MAY_INCREASE 锁定但允许增加(数字1)
gg.FREEZE_MAY_DECREASE 锁定但允许减少(数字2)
gg.FREEZE_IN_RANGE 允许值在范围内变动(数字3),要指定最小值(freezeFrom)和和最大值(freezeTo)()
1 | gg.clearResults() |
gg.getListItems()
获取保存列表的所有项目
返回值:二维表或字符串错误。第二维的表最多显示8个键,
(1) address (2) flags (3) value (4) freeze (5) name (6) freezeType (7)freezeFrom (8) freezeTo
gg.clearList()
清空保存列表
返回值:true或字符串错误
gg.saveList(string file, int flags=0)
将保存列表的全部项目写入到文件
参数1:字符串。文件的路径
参数2:常量。默认是0,保存文件的格式,写gg.SAVE_AS_TEXT即可
返回值:true或字符串错误
1 | gg.saveList('/storage/emulated/0/Download/输出结果.txt',gg.SAVE_AS_TEXT) |
gg.loadList(string file, int flags=0)
加载文件到保存列表上
参数1:字符串。文件的路径
参数2:常量。加载的方式,默认值是0,有三种加载方式:
gg.LOAD_VALUES_FREEZE 加载value并冻结
gg.LOAD_VALUES 加载value
gg.LOAD_APPEND 添加到列表中
返回值:true或字符串错误
1 | gg.loadList('/storage/emulated/0/Download/输出结果.txt') |
gg.saveList的参数2写gg.SAVE_AS_TEXT而保存的文件,是无法load的,要默认值0才行
gg.removeListItems(table items)
从保存列表中删除项目
参数1:二维表。只需要写出键address即可,也可以写得全一些,像getResults的返回值
返回值:true或字符串错误
1 | gg.removeListItems( {{ address=0x4C7827AC }} ) |
gg.getSelectedListItems()
获取保存列表中的已勾选的项目
返回值:二维表或字符串错误。第二维的表最多显示8个键,
(1) address (2) flags (3) value (4) freeze (5) name (6) freezeType (7)freezeFrom (8) freezeTo
选框
gg.choice (table items, string selected=nil, string message=nil)
单项选择框
参数1:表。一般是写字符串文本,给用户点击的
参数2:数字或nil。如果数字对应表的键,则勾上对应的键值,默认是nil,都勾上
参数3:字符串。顶部标题
返回值:用户点击的键
1 | function PP() |
multiChoice(table items, table selection={}, string message=nil)
参数1:表。一般是写字符串文本,给用户点击
参数2:表。跟参数1是对应的,键值是true则勾选,键值是nil则不勾选,默认是空表,全不勾选
参数3:字符串。顶部标题
返回值:表或nil。用户点击“确定”时,返回的是一个表,记录勾选状态。已选择的项,键值是true,未选则false
注意,打印表时,键值是nil的打印不出来
如果用户点击了“取消”或“手机的返回键”,则返回值是nil,所以在判断表的键值之前,得先判断是不是点了取消,否则会因为取消而报错
1 | function P1( ) |
Sx小按钮
gg.showUiButton()
显示/创建一个Sx小按钮
gg.hideUiButton()
隐藏/删除Sx按钮
gg.isClickedUiButton()
Sx按钮被点击
返回值:true表示被点击
gg.isVisible()
gg悬浮窗被点击
返回值:true表示被点击
gg.setVisible()
点击或关闭gg悬浮窗
Hook
入口选择
您又提出了一个更深层次的、非常专业的问题。
您完全正确。“函数序言”(prologue)只是其中一个“战场”,选择在哪里“开战”,本身就是一门高深的学问,直接决定了Hook的稳定性、隐蔽性和难度。
您之前的担忧——“其他代码跳转到我们Hook入口的中间”——正是选择Hook入口时需要考虑的最重要因素之一。
下面,我将介绍几种常见的Hook入口选择策略,并分析们的优劣,特别是如何“尽可能防止其他代码跳到我们的hook入口”。
策略一:Hook 函数序言 (Prologue Hooking) - 我们当前的方法
这是最常见、最通用的方法,即Hook一个函数的第一条指令。
- 优点:
- 覆盖全面: 无论函数从哪里被调用,都必然会经过入口点,保证100%拦截。
- 时机最早: 可以在函数执行任何实质性操作之前进行干预,方便修改传入参数。
- 缺点:
- 风险最高: 正如您指出的,函数序言是一个“交通枢纽”。虽然不常见,但理论上确实可能存在跳转到序言第二、三条指令的情况(例如编译器优化后的
cold path跳转)。如果我们的Hook入口覆盖了这几条指令,就会导致崩溃。
- 风险最高: 正如您指出的,函数序言是一个“交通枢纽”。虽然不常见,但理论上确实可能存在跳转到序言第二、三条指令的情况(例如编译器优化后的
- 如何规避风险 (我们的最佳实践):
- 将入口最小化。这正是我们之前努力实现4字节
B指令入口的原因。当入口点只覆盖一条指令时,任何正常的跳转都不可能跳到的“中间”,风险被降至最低。
- 将入口最小化。这正是我们之前努力实现4字节
策略二:Hook 函数调用点 (Call Site Hooking)
这是解决您担忧的最直接、最有效的方法之一。 我们不Hook目标函数 TakeDamage 本身,而是找到调用 TakeDamage 的那条 BL 或 BLR 指令,然后Hook。
原理:
1
2
3
4; 原始代码
...
BL TakeDamage ; <--- 我们在这里放置Hook,而不是在TakeDamage的开头
...我们将这条
BL指令替换为一个跳转,跳到我们自己的代码。在我们的代码里,我们可以选择性地调用原始的TakeDamage函数。优点:
- 绝对安全,无“跳入中间”风险:
BL本身就是一条原子指令,长度仅4字节。绝对不会有任何其他代码会跳转到一条BL指令的“中间”。这个方案从根本上消除了您的担忧。 - 精准打击: 如果一个函数在程序中有10个调用点,您可以只Hook其中的一两个,实现非常精细的控制。
- 绝对安全,无“跳入中间”风险:
缺点:
- 覆盖不全: 这是最大的缺点。如果
TakeDamage被调用了10次,您就必须找到并Hook所有10个调用点,否则就会有“漏网之鱼”。对于被频繁调用的系统函数,这几乎是不可能的。
- 覆盖不全: 这是最大的缺点。如果
适用场景: 当您只想修改某个特定逻辑流程对目标函数的调用时。例如,只在“玩家A攻击玩家B”的逻辑中Hook伤害计算,而不影响“环境伤害”的逻辑。
策略三:Hook 函数结语 (Epilogue Hooking)
我们不Hook函数的开头,而是Hook函数的结尾,即 RET 指令。
原理:
一个函数在返回前,会执行函数结语,恢复堆栈和寄存器,最后通过RET (BR X30)返回。我们可以把 RET 指令替换成一个跳转。
优点:
- 修改返回值的最佳位置: 此时,原函数的所有逻辑都已执行完毕,
X0寄存器里存放着最终的返回值。在这里修改X0是篡改返回结果的最佳时机。 - 风险较低: 函数结尾的
RET指令也很少会成为其他代码的跳转目标。
- 修改返回值的最佳位置: 此时,原函数的所有逻辑都已执行完毕,
缺点:
- 无法获取原始参数: 到达函数结尾时,最初传入的参数很可能已经被覆盖或释放,无法再进行检查或修改。
- 多个返回点: 一个函数可能因为不同的逻辑分支(如错误处理)而有多个
RET指令。您必须找到并Hook所有返回点,否则就会有遗漏。
策略四:Hook 虚函数表 (Vtable / VFT Hooking) - (C++专属)
对于C++的虚函数,这是一种极其优雅和隐蔽的方法。
原理:
在C++中,包含虚函数的对象,其内存布局的开头通常有一个指针,指向一个“虚函数表(Vtable)”。这个表本身是一个函数指针数组。当程序调用一个虚函数时,会先找到这个表,然后从表中取出函数地址再进行调用。
我们不去修改任何代码,而是直接找到这个数据表,用我们自己函数的地址,覆盖掉表中原始的函数指针。
优点:
- 完全不修改代码段: 极度隐蔽,可以绕过很多对代码完整性的检测。
- 无代码重定位问题: 不需要处理任何相对跳转,不需要Trampoline。
- 面向对象: 可以实现非常精细的控制,比如只替换某一个特定对象的函数行为。
缺点:
- 仅适用于C++虚函数: 对普通函数、C函数、非虚成员函数无效。
- 定位Vtable有难度: 需要较高的逆向工程能力来分析和定位虚函数表在内存中的位置。
总结与如何选择
| Hook位置 | 优点 | 缺点 | 关键点 |
|---|---|---|---|
| 函数序言 (Prologue) | 覆盖最全,可改参数 | 有“跳入中间”的风险 | 入口越小越安全(4字节最优) |
| 调用点 (Call Site) | 绝对安全,无“跳入中间”风险 | 覆盖不全,需Hook所有调用处 | 精准打击,但工作量大 |
| 函数结语 (Epilogue) | 修改返回值的最佳位置 | 无法获取参数,可能有多个返回点 | 专注于结果 |
| 虚函数表 (Vtable) | 极度隐蔽,不修改代码 | 仅限C++虚函数,定位困难 | 数据驱动,绕过代码检测 |
最终建议:
- 为了实现对一个函数的全面控制,Hook函数序言依然是标准做法。此时,您应该不惜一切代价优先使用4字节的
B指令入口,这是规避“跳入中间”风险的最佳工程实践。 - 当您追求极致的稳定性和安全性,或者只想修改特定场景下的函数行为时,Hook调用点是您的最佳选择。
函数
gg.allocatePage(int mode=[gg.PROT_READ]|[gg.PROT_EXEC], long address=0)
参数1:说明:这是一个位掩码,指定了页面的权限标志。常见的标志包括:
PROT_READ: 允许读取。PROT_WRITE: 允许写入。PROT_EXEC: 允许执行。
可以通过组合这些标志来设置页面的访问权限。
参数2:如果该值不为 0,则内核会将其视为关于页面放置位置的提示。在 Android 中,页面会在接近该地址的页面边界处分配
返回值:分配的页面地址或字符串错误
gg.allocatePage(4|2|1, 0)
gg.getRangesList(string filter=’’)
参数1:filter:可选参数,过滤字符串。如果指定,则只返回符合过滤条件的结果。过滤支持通配符:
^:表示数据的开头$:表示数据的结尾*:表示任意数量的任意字符?:表示任意一个字符
返回值:该函数返回一个包含所选进程内存区域的列表。每个内存区域的信息以表格的形式存储,包含以下字段:
state:内存区域的状态start:内存区域的起始地址end:内存区域的结束地址type:内存区域的类型- r:表示该内存区域是可读的(readable)
- w:表示该内存区域是可写的(writable)
- x:表示该内存区域可执行(executable)
- p:表示该内存区域是私有的(private)
比如:r–p 表示该内存区域是可读的,但不可写和不可执行,并且是私有的;r-xp:可读和可执行,私有
name:内存区域的名称internalName:内存区域的内部名称
以下是一些使用示例:
1 | print(gg.getRangesList()) -- 获取所有内存区域并打印 |
获取函数地址
选择地址,保存到文件:

文件内容:
1 | 6728 |
由libgame.so|219c18可得函数地址:
1 | -- 获取目标函数地址 |
简单示例
获取指定库(lib)的内存区域,并返回该区域的起始地址:
1 | function Xa(lib) |
1 | --获取指定库(lib)的内存区域,并返回该区域的起始地址 |
封装成函数
1 | -- 分配可执行内存页并初始化为 NOP 指令 |


