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
2
3
4
5
6
if 条件1 then
语句块1
elseif 条件2 then
语句块2
else 语句块3
end

循环语句

1
2
3
while 满足的条件 do
循环语句块
end
1
2
repeat 循环语句块
until 停止条件
1
2
3
for 变量=初始值,终止值,步长 do
语句块
end

1,表用大括号{ } 来构造 . 可以是多维的.例如二维表 { { } }
2,表是一个关联数组.每个值也可称为元素
3,表有类似于门牌号的索引,称为”键”
4,表默认的键,是正整数,1,2,3,4,5……
5,表可以自定义键名,取名规则跟变量名一样
6,表中的每一个元素,都会有一个对应的,唯一的键
7,表中元素的删除,就是把该元素设为nil
8,表可以传递, 跟变量的赋值一样
9,表的元素有多少个,可以用#来获取.但不一定准确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
k={ '华硕' ,'微星' , '技嘉' , '七彩虹'  }
print(k[1], k[2], k[3], k[4])

k={ '铭瑄' , '昂达' , s='华擎' } --自定义键
print(k)
print(k.s)

k={ 10 , 20 , c=30 , a=40 , _=50 , '猪' } --程序分配键的时候,会跳过自定义键.再接着分配

k[1]=nil 让该元素为nil;但是,程序并不会重新分配键,这点要注意,原来是键2的,并不会变成键1

table.remove(表名,键位)
table.remove(k,1])
这个效果,跟上面的k[1]=nil ,是有区别的
remove函数删除键之后,整体会往前移动,取代删除的位置

table.insert(表名,放在哪个键位, 新加的值)
table.insert(k , 2 , '矿难到来' )
这里意思是,给表k加入新元素,键是2,值是'矿难到来'
原来的键值会集体往后挪动一个位置,原来的键2变成键3了,以此类推
table.insert(k , '天台飞人')
不写键位的话默认就是加到最后面
1
2
3
4
5
6
7
8
表可以是多维的
y={ {4,5,6 } , {7,8,9} , {13,15,17} }
键值虽然是一个表. 也能直接赋值
print(y[3][2]) --结果是15
y= {{a=4,f=5,v=6 },{a=7,f=8,v=9},{ a=13,f=15,v=17}}
print(y[2]['f'])
print(y[2].f)

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
2
3
4
5
6
a=gg.alert('3060显卡的今日售价应该多少?','2499元','3799元','50元')
if a==1 then print('这是首发价,早该降价了.')
elseif a==2 then print('黑心商家,无良奸商.')
elseif a==3 then print('对,就应该卖50元.')
else os.exit()
end

内存区域

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
2
3
4
gg.searchNumber('700',4) 
gg.searchNumber('-1~1',16)
gg.searchNumber("59~61;80;99~101::65",2)
gg.searchNumber("60;80;100",2,false,gg.SIGN_EQUAL,0,-1,12) --只搜索出12个结果

gg.clearResults()

清除搜索结果

gg.getResultsCount()

返回搜索结果个数

1
2
3
4
5
6
local y=gg.getResultsCount()
if y==0 then
gg.alert('搜不到数据!','请检查')
os.exit()
else gg.toast('搜索已完成.')
end

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
2
y=string.format( "%X",123456789)
print(y)
1
2
3
4
5
6
7
8
9
10
11
12
13
常用转义符:
%c - 接受一个数字, 并将其转化为[ASCII码](https://so.csdn.net/so/search?q=ASCII码&spm=1001.2101.3001.7020)表中对应的字符
%d, %i - 接受一个数字并将其转化为有符号的整数格式
%o - 接受一个数字并将其转化为八进制数格式
%u - 接受一个数字并将其转化为无符号整数格式
%x - 接受一个数字并将其转化为十六进制数格式, 使用小写字母
%X - 接受一个数字并将其转化为十六进制数格式, 使用大写字母
%e - 接受一个数字并将其转化为科学记数法格式, 使用小写字母e
%E - 接受一个数字并将其转化为科学记数法格式, 使用大写字母E
%f - 接受一个数字并将其转化为浮点数格式
%g(%G) - 接受一个数字并将其转化为%e(%E, 对应%G)及%f中较短的一种格式
%q - 接受一个字符串并将其转化为可安全被Lua编译器读入的格式
%s - 接受一个字符串并按照给定的参数格式化该字符串
1
2
3
4
5
6
7
8
9
gg.clearResults()
gg.setRanges(4)
gg.searchNumber('60;80;100::200',2)
y=gg.getResultsCount()
k=gg.getResults(y)
print('一共有'..y..'个搜索结果')
for i=1,y do
print( '地址'..i..' : '..string.format('%X', k[i].address) ..' 类型 : '..k[i].flags ..' 值 : '..k[i].value)
end

gg.editAll(string value, int type)

修改getResults得到的表对应的value键值

就是说,gg.getResults()接着下一句是gg.editAll,二者是连载一起用的

参数1:字符串。就是想改成什么,可以写数字、数组

参数2:八种数值常量

返回值:改动的值的数量或字符串错误

将所有搜索结果的值改成9999:

1
2
3
4
5
gg.clearResults()
gg.setRanges(4)
gg.searchNumber('60;80;100',2)
gg.getResults(gg.getResultsCount())
gg.editAll('9999',2)

只改前3个,把数值分别改成1,2,3

1
2
3
4
5
6
7
gg.clearResults()
gg.setRanges(4)
gg.searchNumber('60;80;100',2)
gg.getResults(3)
gg.editAll('1;2;3',2) --数组,一一对应赋值

如果数组不能与搜索结果个数匹配,则会自动轮询赋值,即 1;2;3;1;2;3;1;2;3...

gg.getValues(table values)

用于获取内存地址存储的值

参数1:二维表。必须含有address、flags、value三个键的二维表

返回值:含有address、flags、value三个键的二维表或字符串错误

1
2
3
local k={{ address=0x1, flags=4,value=nil }}   --value只是凑数的,可以随意
local y=gg.getValues(k) --获取真实的value
print(y)

获取某地址附近的值:

1
2
3
4
5
for i=0,15 do
local x=i*0x4
local k=gg.getValues( {{ address=0xA787C174+x, flags=4,value=7 }} )
print(k[1].value)
end
1
2
3
4
5
6
7
8
9
10
gg.clearResults()  
gg.setRanges(4)
gg.searchNumber("100;180;260::65",4)
local k=gg.getResults(3)
print('第500个值是目标....本脚本的作用是,打印目标附近1000个值.')
for i=1,1000 do
local y=(i-500)*0x4
local yy=gg.getValues( {{ address=k[1].address+y, flags=4, value=7 }})
print(i..' '.. yy[1].value )
end

gg.setValues(table values)

参数1:二维表。必须是含有address、flags、value三个键的二维表

返回值:true或字符串错误

1
2
3
4
5
6
7
gg.clearResults() 
gg.setRanges(4)
gg.searchNumber("100;180;260::65",4)
local y=gg.getResults(1)
gg.setValues( {{ address=y[1]. address+0,flags=4,value=1 }} )
gg.setValues( {{ address=y[1]. address+0x20,flags=4,value=2 }} )
gg.setValues( {{ address=y[1]. address+0x40,flags=4,value=3 }} )
1
2
3
4
5
6
7
8
9
10
--已知特征码 "30029;12131;25975;16943;27764;28781::21" ,是W类型.在Ca内存.
--已知第一个特征码,到3个目标地址的偏移是 -0x22C , -0x20C , -0x1EC
--试写出把3个目标数值改成1的脚本.
gg.clearResults()
gg.setRanges(4)
gg.searchNumber("30029;12131;25975;16943;27764;28781::21",2)
local y=gg.getResults(1)
gg.setValues( {{ address=y[1]. address-0x22C,flags=2,value=1 }} )
gg.setValues( {{ address=y[1]. address-0x20C,flags=2,value=1 }} )
gg.setValues( {{ address=y[1]. address-0x1EC,flags=2,value=1 }} )

特征码

唯一双浮点数

1
2
3
4
5
6
7
8
9
10
11
12
if gg.getResultsCount()==0 then
gg.alert('自己先搜出目标\n并放在搜索列表第一位','先自己去找出目标','自己先搜') os.exit()
else k=gg.getResults(1) end
print("--复制粘贴时,不要忘记删除第一行的 脚本已结束::")
print( ' local KK={ ' )

for i=-100,100 do
local y=gg.getValues( {[1]={ address=k[1].address+i*0x4 , flags=64, value=nil }})
if y[1].value~=0 then print(' " ' .. y[1].value .. ' "; ' ) end
end

print( "} \n for i=1,#KK do \n gg.searchNumber( KK[i] , 64 ) \n if gg.getResultsCount()==1 then print( KK[i] ) end \n gg.clearResults() \n end ") --逐一搜索以确定唯一性

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
2
3
4
5
6
gg.clearResults()
gg.searchNumber(2,4)
local k=gg.getResults(3)
k[2].freeze=true
k[2].value=7
gg.addListItems(k)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function PP() 
k=gg.choice( {"冷却","距离" ,"退出"} , nil , '瓶子' ) --返回表的键
if k==1 then P1( )
elseif k==2 then P2( )
else os.exit()
end
end

function P1( )
gg.clearResults( )
gg.setRanges(4)
gg.searchNumber('600;400;250::65',2)
gg.getResults(100)
gg.editAll('1',2)
end

function P2( )
gg.clearResults( )
gg.setRanges(4)
gg.searchNumber('190;240;300::65',2)
gg.getResults(100)
gg.editAll('3000',2)
end
--注意自定义函数声明要写在函数调用前
PP()

multiChoice(table items, table selection={}, string message=nil)

参数1:表。一般是写字符串文本,给用户点击

参数2:表。跟参数1是对应的,键值是true则勾选,键值是nil则不勾选,默认是空表,全不勾选

参数3:字符串。顶部标题

返回值:表或nil。用户点击“确定”时,返回的是一个表,记录勾选状态。已选择的项,键值是true,未选则false

注意,打印表时,键值是nil的打印不出来

如果用户点击了“取消”或“手机的返回键”,则返回值是nil,所以在判断表的键值之前,得先判断是不是点了取消,否则会因为取消而报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function P1( )
gg.clearResults( )
gg.setRanges(4)
gg.searchNumber('600;400;250::65',2)
gg.getResults(100)
gg.editAll('1',2) print('冷却修改成功!')
end

function P2( )
gg.clearResults( )
gg.setRanges(4)
gg.searchNumber('190;240;300::65',2)
gg.getResults(100)
gg.editAll('3000',2) print('距离修改成功!')
end

function P()
local k=gg.multiChoice ( {'冷却' , '距离' , '退出脚本' } , { } , '瓶子' )
if k==nil then os.exit() end --先判断是否取消、返回了
if k[1]==true then P1( ) end
if k[2]==true then P2( ) end
if k[3]==true then os.exit() end
end

local y=gg.choice( { '瓶子' , '粑粑' , '星星' , '退出脚本'} )
if y==1 then P( )
elseif y==2 or y==3 then print('功能还未添加')
else os.exit() end

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指令入口的原因。当入口点只覆盖一条指令时,任何正常的跳转都不可能跳到的“中间”,风险被降至最低。

策略二:Hook 函数调用点 (Call Site Hooking)

这是解决您担忧的最直接、最有效的方法之一。 我们不Hook目标函数 TakeDamage 本身,而是找到调用 TakeDamage 的那条 BLBLR 指令,然后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
2
3
4
5
6
7
8
print(gg.getRangesList())  -- 获取所有内存区域并打印
local t = gg.getRangesList() -- 获取所有内存区域并存储在变量 t 中
print(t[1].start) -- 打印第一个内存区域的起始地址
print(t[1]['end']) -- 打印第一个内存区域的结束地址(使用方括号,因为 'end' 是 Lua 的关键字)
print(gg.getRangesList('libc.so')) -- 获取名称为 'libc.so' 的内存区域
print(gg.getRangesList('lib*.so')) -- 获取名称以 'lib' 开头并以 '.so' 结尾的内存区域
print(gg.getRangesList('^/data/')) -- 获取名称以 '/data/' 开头的内存区域
print(gg.getRangesList('.so$')) -- 获取名称以 '.so' 结尾的内存区域

获取函数地址

选择地址,保存到文件:

文件内容:

1
2
6728
Var #7771241C18|7771241c18|4|8b140108|0|0|0|0|r-xp|/data/app/~~1O6xQK6NAw89lRWO6UrLaA==/com.carrot.iceworld-3wox2ImH0yL4eiFW3YPD8g==/lib/arm64/libgame.so|219c18

libgame.so|219c18可得函数地址:

1
2
3
4
5
6
7
8
9
10
11
-- 获取目标函数地址
local function getExecutableBase(libName)
for _, v in ipairs(gg.getRangesList(libName)) do
if v.state == "Xa" or v.type:sub(2, 3) == "-x" then
return v.start
end
end
gg.toast("找不到可执行区域: "..libName)
end

local addr = getExecutableBase("libgame.so") + 0x219C18 --函数地址

简单示例

获取指定库(lib)的内存区域,并返回该区域的起始地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Xa(lib)
ranges = {}
for i, v in pairs(gg.getRangesList(lib)) do
modjs = v.type:sub(2, 3)
if modjs == '-x' then
table.insert(ranges, v)
end
end
return ranges[1].start
end
function Cd(lib)
ranges = {}
for i, v in pairs(gg.getRangesList(lib)) do
modjs = v.type:sub(2, 2)
if modjs == 'w' then
modsj = v.type
table.insert(ranges, v)
end
end
return ranges[1].start
end
function Cb(lib)
ranges = {}
for i, v in pairs(gg.getRangesList(lib)) do
modjs = v.name:sub(6, 7)
if modjs == ':.' then
table.insert(ranges, v)
end
end
return ranges[1].start
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
--获取指定库(lib)的内存区域,并返回该区域的起始地址
function Xa(lib)
ranges = {}
for i, v in pairs(gg.getRangesList(lib)) do
modjs = v.type:sub(2, 3)
if modjs == '-x' then
table.insert(ranges, v)
end
end
return ranges[1].start
end

local addr = Xa("libgame.so") + 0x219C18 --函数地址
local alloc = gg.allocatePage(4|2|1, 0)
--原代码
local o1 = gg.getValues({[1] = {address = addr, flags = 4}})
local o2 = gg.getValues({[1] = {address = addr + 0x4, flags = 4}})
local o3 = gg.getValues({[1] = {address = addr + 0x8, flags = 4}})
local o4 = gg.getValues({[1] = {address = addr + 0xC, flags = 4}})
local o5 = gg.getValues({[1] = {address = addr + 0x10, flags = 4}})
gg.addListItems(o5)

--修改代码逻辑
local p = 0
gg.setValues({[1] = {address = alloc + p, value = "~A8 MOV X20, #0x64", flags = 4 }})
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = o1[1].value, flags = 4 }})
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = o2[1].value, flags = 4 }})
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = o3[1].value, flags = 4 }})
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = o4[1].value, flags = 4 }})
--返回
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = "~A8 LDR X29, [PC,#0x8]", flags = 4 }})
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = "~A8 BR X29", flags = 4 }})
p = p + 4
gg.setValues({[1] = {address = alloc + p, value = o5[1].address, flags = 32 }})

--跳转到分配的内存(绝对跳转LDR+BR)
gg.setValues({[1] = {address = addr, value = "~A8 LDR X20, [PC,#0x8]", flags = 4 }})
gg.setValues({[1] = {address = addr + 0x4, value = "~A8 BR X20", flags = 4 }})
gg.setValues({[1] = {address = addr + 0x8, value = alloc, flags = 32 }})

封装成函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
-- 分配可执行内存页并初始化为 NOP 指令
function allocateNopPage()
-- 分配可读、可写、可执行的内存页
local alloc = gg.allocatePage(gg.PROT_READ | gg.PROT_WRITE | gg.PROT_EXEC)
if alloc == nil then
gg.toast("内存分配失败")
return nil
end

-- 获取页面大小(通常为4096字节)
local PAGE_SIZE = 4096
local nopCount = PAGE_SIZE // 4 -- 每条NOP指令占4字节

-- 批量写入NOP指令
local nopTable = {}
for i = 0, nopCount - 1 do
table.insert(nopTable, {
address = alloc + i * 4,
flags = gg.TYPE_DWORD,
value = -721215457 -- ARM64 NOP指令
})
end

-- 批量设置NOP指令
local success, result = pcall(gg.setValues, nopTable)

if not success then
gg.toast("初始化NOP失败: "..tostring(result))
-- 释放分配的内存
gg.freeMemory(alloc)
return nil
end

-- 添加到列表(可选)
--gg.addListItems({{address = alloc, flags = gg.TYPE_QWORD, name = "NOP Page"}})

return alloc
end

-- 获取目标函数地址
function getExecutableBase(libName)
for _, v in ipairs(gg.getRangesList(libName)) do
if v.state == "Xa" or v.type:sub(2, 3) == "-x" then
return v.start
end
end
gg.toast("找不到可执行区域: "..libName)
end

-- 字节数组扫描
function AOBScan(byteArray)
gg.clearResults()
gg.searchNumber(byteArray)
-- 阳光增加 00001A0B205302B9
-- gg.searchNumber('5Ch;0~~0;0Bh;4Bh;0~~0;9Bh;11h;7Bh::8', gg.TYPE_BYTE)
t = gg.getResults(1)
t[1].flags = gg.TYPE_DWORD
gg.addListItems(t)
return t[1].address
end

-- 在分配的内存中查找第一个可用的 NOP 区域
-- 参数:
-- base_addr: 分配的内存基址
-- step: 搜索步长 (默认为40字节)
-- size: 需要查找的连续NOP大小 (默认为4字节)
-- 返回: 可用区域的起始地址,或 nil 如果未找到
function findFirstNopArea(base_addr, step, size)
base_addr = base_addr or allocNopPage
if base_addr == nil then
gg.toast("无效的内存地址")
return nil
end

step = step or 40 -- 默认步长10字节
size = size or 4 -- 默认4字节NOP区域

local PAGE_SIZE = 4096
local end_addr = base_addr + PAGE_SIZE - size

-- 遍历内存页
for addr = base_addr, end_addr, step do
-- 读取当前位置的值
local result = gg.getValues({{
address = addr,
flags = gg.TYPE_DWORD
}})
gg.toast("值为" .. result[1].value)
-- 检查是否为NOP指令 (ARM64 NOP = -721215457)
if result[1].value == -721215457 then
-- 检查后续字节是否也是NOP (如果需要)
if size > 4 then
local success = true
for i = 4, size - 1, 4 do
local next_addr = addr + i
if next_addr >= base_addr + PAGE_SIZE then
success = false
break
end

local next_val = gg.getValues({{
address = next_addr,
flags = gg.TYPE_DWORD
}})[1].value

if next_val ~= -721215457 then
success = false
break
end
end

if success then
return addr
end
else
return addr
end
end
end

gg.toast("未找到可用的NOP区域")
return nil
end

--[[
Hook 函数封装
参数说明:
targetAddr:目标地址
hookCode: Hook 代码表 (每条指令为一个字符串)
reg: 可选,指定临时寄存器 (默认 X29)
backupReg:可选,指定备用寄存器 (默认 X30)
saveValue:可选,临时保存的值,可能需要修改
]]--
function hookRegFunction(targetAddr, hookCode, reg, backupReg, saveValue)
-- 设置默认寄存器
reg = reg or "X29"
backupReg = backupReg or "X30"
saveValue = saveValue or nil
--gg.toast("目标函数: "..string.format("%X", targetAddr))

-- 分配可执行内存 (RWE)
local alloc = findFirstNopArea()
if alloc == nil then
gg.toast("无法分配NOP内存")
end

-- 构造新代码到分配的内存
local newCode = {}
local pc = 0 -- 当前写入位置

-- 存储需要改变的地址
if saveValue then
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = saveValue
})
pc = pc + 4
end

-- 添加 Hook 代码
for _, code in ipairs(hookCode) do
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = code
})
pc = pc + 4
end

local o6 = gg.getValues({[1] = {address = targetAddr + 20, flags = 4}})
gg.addListItems(o6)

-- 添加返回原函数的跳转
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = "~A8 LDR "..backupReg..", [PC,#0x8]"
})
pc = pc + 4

table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = "~A8 BR "..backupReg
})
pc = pc + 4

table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_QWORD, -- 64位地址
value = o6[1].address -- 原函数第6条指令地址
})

-- 写入新代码
gg.setValues(newCode)

-- 修改原函数入口
local patch = {
{ -- 入栈
address = targetAddr,
flags = gg.TYPE_DWORD,
value = "~A8 STP "..reg..", "..backupReg..", [SP,#-16]!"
},
{ -- 跳转到 Hook 代码
address = targetAddr + 4,
flags = gg.TYPE_DWORD,
value = "~A8 LDR "..reg..", [PC,#0x8]"
},
{
address = targetAddr + 8,
flags = gg.TYPE_DWORD,
value = "~A8 BR "..reg
},
{ -- 写入分配的内存地址 (64位)
address = targetAddr + 12,
flags = gg.TYPE_QWORD,
value = saveValue and alloc+4 or alloc
},
{ -- 出栈
address = targetAddr + 20,
flags = gg.TYPE_DWORD,
value = "~A8 LDP "..reg..", "..backupReg..", [SP], #16"
}
}
gg.setValues(patch)
return alloc
end

-- 不临时保存用作跳转的寄存器原来的值
function hookFunction(targetAddr, hookCode, reg, backupReg, saveValue)
-- 设置默认寄存器
reg = reg or "X29"
backupReg = backupReg or "X30"
saveValue = saveValue or nil

--gg.toast("目标函数: "..string.format("%X", targetAddr))

-- 分配可执行内存 (RWE)
local alloc = findFirstNopArea()
if alloc == nil then
gg.toast("无法分配NOP内存")
end

-- 构造新代码到分配的内存
local newCode = {}
local pc = 0 -- 当前写入位置

-- 存储需要改变的地址
if saveValue then
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = saveValue
})
pc = pc + 4
end

-- 添加 Hook 代码
for _, code in ipairs(hookCode) do
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = code
})
pc = pc + 4
end

local o6 = gg.getValues({[1] = {address = targetAddr + 20, flags = 4}})
gg.addListItems(o6)

-- 添加返回原函数的跳转
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = "~A8 LDR "..backupReg..", [PC,#0x8]"
})
pc = pc + 4

table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = "~A8 BR "..backupReg
})
pc = pc + 4

table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_QWORD, -- 64位地址
value = o6[1].address -- 跳回原函数第6条指令地址
})

-- 写入新代码
gg.setValues(newCode)

-- 修改原函数入口
local patch = {
{ -- 跳转到 Hook 代码
address = targetAddr,
flags = gg.TYPE_DWORD,
value = "~A8 LDR "..reg..", [PC,#0x8]"
},
{
address = targetAddr + 4,
flags = gg.TYPE_DWORD,
value = "~A8 BR "..reg
},
{ -- 写入分配的内存地址 (64位)
address = targetAddr + 8,
flags = gg.TYPE_QWORD,
value = saveValue and alloc+4 or alloc
}
}
gg.setValues(patch)
return alloc
end

function hookCallFunction(targetAddr, hookCode, saveValue)

saveValue = saveValue or nil

--gg.toast("目标函数: "..string.format("%X", targetAddr))

-- 分配可执行内存 (RWE)
local alloc = findFirstNopArea()
if alloc == nil then
gg.toast("无法分配NOP内存")
end

-- 构造新代码到分配的内存
local newCode = {}
local pc = 0 -- 当前写入位置

-- 存储需要改变的地址
if saveValue then
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = saveValue
})
pc = pc + 4
end

-- 添加 Hook 代码
for _, code in ipairs(hookCode) do
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = code
})
pc = pc + 4
end

local o5 = gg.getValues({[1] = {address = targetAddr + 16, flags = 4}})
gg.addListItems(o5)

-- 添加返回原函数的跳转
table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = "~A8 LDR X30, [PC,#0x8]"
})
pc = pc + 4

table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_DWORD,
value = "~A8 BR X30"
})
pc = pc + 4

table.insert(newCode, {
address = alloc + pc,
flags = gg.TYPE_QWORD, -- 64位地址
value = o5[1].address -- 跳回原函数第5条指令地址
})

-- 写入新代码
gg.setValues(newCode)

-- 修改原函数入口
local patch = {
{ -- 跳转到 Hook 代码
address = targetAddr,
flags = gg.TYPE_DWORD,
value = "~A8 LDR X30, [PC,#0x8]"
},
{
address = targetAddr + 4,
flags = gg.TYPE_DWORD,
value = "~A8 BR X30"
},
{ -- 写入分配的内存地址 (64位)
address = targetAddr + 8,
flags = gg.TYPE_QWORD,
value = saveValue and alloc+4 or alloc
}
}
gg.setValues(patch)
return alloc
end

function restoreFunction(targetAddr, originalCode)

local pc = 0
local Code = {}
-- 尝试恢复
for _, code in ipairs(originalCode) do
table.insert(Code, {
address = targetAddr + pc,
flags = gg.TYPE_DWORD,
value = code
})
pc = pc + 4
end
gg.setValues(Code)
gg.toast("函数恢复成功")
return true
end


-- 示例使用 --
local myHook = {
"~A8 LDR W30, [PC,#-0x4]",
"~A8 MUL W22, W30, W22",
"~A8 ADD W8, W8, W22",
"~A8 CMP W0, W8",
"~A8 CSEL W8, W0, W8, LT",
"~A8 EOR W8, W8, W9"
}

allocNopPage = allocateNopPage()
local targetAddr = getExecutableBase("libSrc.so") + 0x1e3a8fc
-- 应用 Hook
local hookInfo = hookFunction(
targetAddr,
myHook, -- Hook 代码
3
)

local myHook = {
"~A8 CMP X20, #0x0",
"~A8 B.GE [PC,#0x8]",
"~A8 MOV X20, XZR",
"~A8 ADD X8, X8, X20",
"~A8 ADD X9, X9, X20",
"~A8 STR X8, [X19,#0x588]",
"~A8 STR X9, [X19,#0x6F8]",
"~A8 LDP X29, X30, [SP,#0x10]",
"~A8 LDP X20, X19, [SP], #0x20"
}
-- 应用 Hook
local hookInfo = hookRegFunction(
"libgame.so", -- 目标库
0x219C18, -- 函数偏移
myHook -- Hook 代码
)