Frida in CTF Android Reverse Engineering

j1ya Lv5

原理

Frida是一款跨平台的动态代码插桩框架,核心原理是向目标进程注入包含 Frida Runtime 的代码库。该 Runtime 在进程内构建轻量级 JavaScript 环境,并利用底层引擎(Gum)动态修改目标函数的内存指令。通过插入跳转指令(Inline Hook)等,将执行流重定向到代理函数(trampoline/thunk),代理函数负责调用用户编写的 JavaScript 回调,实现拦截、修改参数/返回值或追踪逻辑

Frida 通过多种方式(例如 Android 的 ptrace,Windows 的 DLL 注入,iOS 的 Dylib)将自己的动态库注入到目标进程地址空间中,核心思想是在目标进程内存中加载 Frida 动态库并初始化 Gum 插桩引擎,最终建立双向通信通道,使 JavaScript 脚本能动态操控原生代码执行流

组成

frida-server (目标端): 在目标设备或模拟器上运行的后台服务

frida-gadget (嵌入式): 核心功能与frida-server相同,存在形式为动态链接库 (*.so/*.dylib/*.dll),可静态嵌入到目标应用程序中,无需每次注入,适合在无 root 环境中使用

frida-core: 由 server 或 gadget 加载的核心库,包含 Frida运行最重要的组件gum,提供了将 Native 与 JavaScript连接起来的桥梁

frida-gum:实现代码插桩,其中Interceptor提供hook主要功能,Stalker提供代码追踪功能

frida CLI / Python Bindings / Node.js Bindings (客户端): 开发者使用的工具或库,通过进程间通信(IPC)与目标端的 server 或 gadget 通信,Python/Node.js Bindings允许开发者在 Python 或 Node.js 环境中直接调用 Frida 的功能,frida CLI提供快速交互式操作,需提前写入 .js 文件,依赖命令行参数

环境

电脑模拟器

本地环境:雷电模拟器Android9 x64

root

先安装Magisk(面具),这里把模拟手机型号从PCLM10改为PCLM9发现就有直接安装选项

安装MagiskHidePropsConfig模块

共享文件夹在shared/Pictures目录

连接设备

windows上的模拟器选择下载frida-server-16.2.1-android-x86_64.xz,双击解压出二进制文件,然后通过adb push到/data/local/tmp

报错10061说明模拟器没开

adb devices查看设备,adb root获取root权限,adb shell`连接到远程

进入frida-server安装目录cd /data/local/tmp,然后运行frida-server

进行端口转发监听,使电脑上的 Frida 客户端能够访问模拟器里的 frida-server,如果用的是 libfrida-gadget.so 就需要adb forward tcp:27042 tcp:27042

经验总结

1.打开的方式安装不了可以拖动或者adb install

2.可通过下面路径查找日志

3.如果处于开发者模式一直未被调试

可以关闭开发者模式

4.如果是多开的模拟器需要先找到对应端口再连接:.\nox_adb -s 127.0.0.1:62026 shell

手机真机

root

方式主要有:

  • Magisk修补Boot
  • KernelSU/APatch内核层root
  • 内置root功能的第三方或 userdebug ROM(包含 Android 操作系统及相关分区文件的系统固件或刷机包),例如LineageOS userdebug

设备用的是Google的pixel手机

在手机开发者模式下打开OEM解锁+USB调试

进入Bootloader Mode找不到设备需要在设备管理器选择带黄色感叹号的设备更新驱动

解锁 Bootloader

获取与设备号和版本号一致的Factory Image,连续解压得到boot.img

https://developers.google.com/android/images?utm_source=chatgpt.com#bonito

手机安装Magisk,Magisk安装boot.img,生成magisk_patched_xxx.img

在Bootloader Mode下临时从img镜像启动:fastboot boot xxx.img

临时root正常后永久刷入Magisk patched boot:fastboot flash boot magisk_patched_xxx.img

连接设备

同一网段下手机开启开发者模式,允许usb/无线调试

无线调试Android11及以上需要先配对,手机选择使用配对码配对设备

查看手机架构,下载对应架构frida-server

Objection

Objection 在 Frida 上增加了一层命令封装

/data/local/tmp目录下用root权限启动frida-server

搜索特定进程:frida-ps -U | grep -i settings

查看进程对应真正包名:frida-ps -Ua

输入空格键会有提示与补全

静态

模拟器上启动软件后再去注入mobile包:objection -g mobilego explore

有时候进程名用空格隔开,用引号标注

在内存中所有已加载的类中搜索包含game的类:android hooking search classes game

查看com.android.settings.DisplaySettings类有哪些方法:android hooking list class_methods com.android.settings.DisplaySettings

直接生成代码但可能没参数:android hooking generate simple com.android.settings.DisplaySettings

进入显示设置,软件如果没有做校验,可以直接使用这个命令跳过注册登录:android intent launch_activity com.android.settings.DisplaySettings

动态

hook类:android hooking watch class android.bluetooth.BluetoothDevice

hook方法,查看参数、返回值和调用栈:**android hooking watch class_method game.Game.checkflag --dump-args --dump-return --dump-backtrace**

在模拟器输入符合题目要求的字符串后,再Called game.Game.checkflag(java.lang.String),得到return值

还可以import js代码,效果和直接frida类似

基本用法

包名通过frida-ps -Ua查看

Frida两种启动方式的区别:

1
2
spawn` 模式:frida 重新打开一个进程并挂起,在启动的同时注入frida代码 `frida -U -f 包名 -l js路径 
attch` 模式: attach到已经存在的进程,核心原理是ptrace修改进程内存,如果进程处于调试状态(traceid不等于0),则attach失败 `frida -U -n 包名 -l js路径

以下代码基于frida16

java层

hook自定义函数

hook native方法

1
2
3
4
5
6
7
8
9
10
11
12
13
function main() {
Java.perform(function () {
var MainActivity = Java.use("com.example.challengemobile.MainActivity");

MainActivity.a.implementation = function (bArr) {
var result = this.a(bArr);
console.log("res: ", result);
return result;
};
});
}

setImmediate(main);

hook模拟器检测和调试检测函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java.perform(function(){
let MainActivity = Java.use("work.pangbai.debugme.MainActivity");

MainActivity["isEmu"].implementation = function () {
console.log(`MainActivity.isEmu is called`);
// let result = this["isEmu"]();
// console.log(`MainActivity.isEmu result=${result}`);
return false;
};

let App = Java.use("work.pangbai.tool.App");

App.GetPPid.implementation = function() {
console.log("Hooked GetPPid()");
return "0";
};
MainActivity["g4tk4y"].implementation = function () {
console.log(`MainActivity.g4tk4y is called`);
let result = this["g4tk4y"]();
console.log(`MainActivity.g4tk4y result=${result}`);
return result;
};
});

hook系统函数

1
2
3
4
5
6
7
8
9
Java.perform(function () {
const File = Java.use("java.io.File");

File.delete.overload().implementation = function () {
console.log("[阻止删除] " + this.getAbsolutePath());
return false;
};
console.log("已Hook");
});

主动调用

hook InMemoryDexClassLoader.loadClass()方法,Checker 不是当前默认 ClassLoader 加载的类,需要反射调用getKey获取密钥

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
function main() {
Java.perform(function () {
var ClassUse = Java.use("java.lang.Class");
var InMemoryDexClassLoader = Java.use("dalvik.system.InMemoryDexClassLoader");

var loadClass = InMemoryDexClassLoader.loadClass.overload("java.lang.String");
var done = false;

loadClass.implementation = function (name) {
var clazz = loadClass.call(this, name);

if (!done && name === "com.example.challengemobile.Checker") {
done = true;

var clazzCast = Java.cast(clazz, ClassUse);
var emptyClassArray = Java.array("java.lang.Class", []);
var emptyObjectArray = Java.array("java.lang.Object", []);

var method = clazzCast.getMethod("getKey", emptyClassArray);
var key = method.invoke(null, emptyObjectArray);

console.log("[+] getKey:", key);
}

return clazz;
};
});
}

setImmediate(main);

native层

hook自定义函数

_Z10murmur3_32PKcjj是真实导出表的名字,murmur3_32是demangle后的名字

1
2
3
4
5
6
7
8
var func_addr = Module.findExportByName("libmidand.so" , "_Z10murmur3_32PKcjj");
console.log("func addr is ---" + func_addr);
Interceptor.attach(func_addr, {
onLeave: function(retval){
console.log("enter murmur3_32 retvalue->\n" + hexdump(retval));

}
});

hook之后在模拟器输入字符然后点login

发现能看到返回值

hook系统函数

v6参数(账号)会经过好几次加密,需要hook strcmp函数查看s2(密码)和比较的值

!

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
function hook_strcmp(chall) {

const strcmpAddr = Module.findExportByName(
'libc.so',
'strcmp'
);
if (strcmpAddr === null) {
console.log('[-] strcmp not found');
return;
}
const start = chall.base;
const end = chall.base.add(chall.size);

console.log('[+] libchall1.so loaded');
console.log('[+] base : ' + start);
console.log('[+] strcmp : ' + strcmpAddr);

Interceptor.attach(strcmpAddr, {

onEnter: function(args) {
this.matched = false;
// 判断strcmp调用者是否来自libchall1.so
if (
this.returnAddress.compare(start) < 0 ||
this.returnAddress.compare(end) >= 0
) {
return;
}
try {
this.s1 = args[0].readCString();
this.s2 = args[1].readCString();
this.matched = true;
} catch(e) {
console.log(
'[-] read error: ' + e
);
}
},

onLeave: function(retval) {
if(!this.matched)
return;
console.log(
'=============================='
);
console.log(
'[+] p_s1 : ' + this.s1
);
console.log(
'[+] s2 : ' + this.s2
);
// console.log(
// '[+] ret : ' + retval.toInt32()
// );
console.log(
'=============================='
);
}
});
console.log('[+] strcmp hook installed');
}

function wait_libchall1(){
let chall = Process.findModuleByName(
'libchall1.so'
);
if(chall === null){
console.log(
'[*] waiting libchall1.so ...'
);
setTimeout(
wait_libchall1,
500
);
return;
}
hook_strcmp(chall);
}
setImmediate(wait_libchall1);

主动调用主函数中未被调用的方法

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
function hook(){
var a = Module.findBaseAddress("libfrida0xa.so");
var b = Module.enumerateExports("libfrida0xa.so");
var get_flagaddress = null;
var mvaddress = null;
for(var i = 0 ; b[i]!= null ; i ++ ){
// console.log(b[i]["name"])
if(b[i]["name"] == "_Z8get_flagii"){
console.log("function get_flag : ",b[i]["address"]);
console.log((b[i]["address"] - a).toString(16));
// mvaddress = b[i]["address"] - a;
get_flagaddress = b[i]["address"];
}
}
console.log(ptr.toString(16));

var get_flag_ptr = new NativePointer(get_flagaddress);
const get_flag = new NativeFunction(get_flag_ptr,'char',['int','int']);
var flag = get_flag(1,2);
console.log(flag)
}

function main(){
Java.perform(function (){
hook();
})
}
setImmediate(main)

//也可以用偏移量的方式
const libBase = Module.findBaseAddress(
"libnative.so"
);
const funcAddr = libBase.add(0x1234);
const hidden_func = new NativeFunction(
funcAddr,
'pointer',
[
'int',
'int'
]
);
let ret = hidden_func(10,20);
console.log(ret.readCString());

frida-dump

通过 Frida 注入脚本到目标进程,动态获取进程运行时内存信息,包括已加载的 DEX 文件、Native 库以及关键内存区域,并根据内存映射信息将其导出为原始文件(如 .dex、.so)。对于经过加固、混淆或运行时解密后的代码,通常需要结合动态分析进一步恢复

在 ART 类加载过程中,FindClassDef 会遍历当前 ClassLoader 关联的 DexFile 集合,根据目标类描述符查找对应的 ClassDef。当找到目标类所在的 DexFile 及 ClassDef 后,ClassLinker::DefineClass 使用这些信息创建对应的 Class 对象,并继续完成字段解析、方法加载以及代码链接

frida-dump 的核心思想是在 ART 类加载关键路径上 Hook ClassLinker::DefineClass,获取正在加载类对应的 DexFile 对象,通过读取 DexFile 中保存的 begin 地址和 size 信息定位内存中的完整 dex 数据,并将其导出

frida-dexdump

frida-dexdump 支持在 App 启动阶段注入,通过捕获运行时已经解密并加载到内存中的 dex 文件,可以直接导出动态加载后的 dex

连接frida-server之后用 adb shell pm list packages找到真正的包名(有时候和jadx里面显示的不太一样)

frida-dexdump -U -f com.example.wyy直接dump出动态解密的dex,会自动修复

-d开启深度搜索模式

进阶用法

2024CISCN-first_game-rust_baby

字符串比较循环次数侧信道

0x140003E8是循环比较,每一个字符成功都会进行下一轮循环

因此考虑用frida插桩爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var number = 0
// frida -l .\hook.js -n rust_baby.exe
function main()
{
var base = Module.findBaseAddress("rust_baby.exe")

if(base){
Interceptor.attach(base.add(0x3EA8), {
onEnter: function(args) {
// 当函数被调用时执行的逻辑,这里增加了全局变量number的值并发送
number += 1;
send(number);
// 延迟时间单位为毫秒,Frida 的 send() 是异步消息,程序结束太快时消息可能还没送回 python;短暂延迟可以保证 python 收到计数
var delay = 0x4;
var start = new Date().getTime();
while (new Date().getTime() < start + delay);
}
});
}
}
setImmediate(main);

验证发现可行

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
import subprocess
import frida

filename = "rust_baby.exe"
cmd = ['./rust_baby.exe']

jscode = open("hook.js", "rb").read().decode()

# 自定义异常类,用于表示成功找到flag
class SUCCESSFLAG(Exception):
pass

def brute(F):
"""
通过Frida对目标程序进行动态分析,使用爆破方式获取正确的flag。

参数:
F: 用于传递给程序的输入,类型为bytes。

返回值:
result: 运行程序后的结果。

抛出:
SUCCESSFLAG: 当找到正确的flag时抛出。
"""
def on_message(message, data):
global result
if message['type'] == 'send':
result = message['payload']
else:
print(message)
# 创建子进程运行目标程序,并使用Frida附加到进程上
process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
session = frida.attach(filename)
script = session.create_script(jscode)
script.on('message', on_message)
script.load()

# 向进程输入F,并等待进程结束
process.stdin.write(F.decode())
output, error = process.communicate()

# 若输出中包含"correct flag",则打印并抛出SUCCESSFLAG异常
if "correct flag" in output:
print("正确的flag是:", F.decode())
raise SUCCESSFLAG("抛出flag成功的异常!!!")
process.terminate()
return result

def bp(startflag, old_number1, tag1):
"""
基于brute函数的辅助爆破函数,用于逐字符爆破并处理特殊情况。

参数:
startflag: 当前已知的flag部分,列表形式。
old_number1: 上一次爆破得到的数字结果。
tag1: 标志位,用于控制爆破逻辑。

返回值:
startflag: 更新后的flag部分。
old_number1: 更新后的数字结果。
tag1: 更新后的标志位。
"""
global temptag
global maxnumber
idx = 0
temp = old_number1
targetidx = -1
table = "-0123456789abcdefghijklmnopqrstuvwxyz}"

# 循环遍历可能的字符,尝试爆破
for ch in table:
startflag.append(ord(ch))
startflag.append(10)
my_bytearray = bytearray(startflag)
new_number = brute(my_bytearray)

if(new_number > old_number1):
if tag1 == 0:
old_number1 = new_number
targetidx = idx
if(tag1 > 0):
tag1 -= 1

idx += 1
startflag = startflag[:-2]

# 若未找到目标字符,进行回溯处理
if(targetidx == -1):
print("错误的:")
my_bytearray = bytearray(startflag)
print(my_bytearray.decode())
print("去除错误字符!")
startflag = startflag[:-1]
print("magic:", temp-1)
temptag = temptag + 1
print("magictag:", temptag+1)
return startflag, temp-1, temptag

# 找到目标字符,进行相应处理
bpret = table[targetidx:targetidx+1]
print("目标数据:", bpret, ord(bpret))
startflag.append(ord(bpret))
my_bytearray = bytearray(startflag)
print(my_bytearray.decode())
return startflag, old_number1, 0


# startflag1 = [102, 108, 97, 103, 123] # flag的格式是固定的,因此初始值已知
startflag = [102, 108, 97, 103, 123, 54, 101, 50, 52, 56, 48, 98, 51, 45, 52, 102, 48, 50, 45, 52, 99, 102]
startflag.append(10)
my_bytearray = bytearray(startflag)

old_number = brute(my_bytearray)
startflag = startflag[:-1] # 去除回车
temptag = 0
tag = 0
idx = 0
temp = len(startflag) # 初始长度

cleartag = 5
while True:
startflag, old_number, tag = bp(startflag, old_number, tag) # 执行爆破逻辑

if(temp < len(startflag)):
temp = len(startflag)
print("----->", temp-5)
if((temp-cleartag)%3 == 0):
print(">>>>清理temptag")
temptag = 0
cleartag += 2

可以采取一点点增加的方式,每次报错都要去掉最后一个候选字符,中间有四次爆破得不到新字符,默认从当前字符往后遍历

2023NCTF-ez_VM

opcode执行次数侧信道

其中hook的两个位置分别为opcode分发和putchar

尝试的脚本,发现可以hook

调用exit函数在获取发送过来的number之后结束程序,可以得到更快的爆破速度

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
var number = 0
function main()
{
var base = Module.findBaseAddress("ezVM.exe")
if(base){
Interceptor.attach(base.add(0x1044), {
onEnter: function(args) {
//console.log("number",number)
number+=1
}
});

Interceptor.attach(base.add(0x0113f), {
onEnter: function(args) {
send(number)
var a = 0;
for(var i = 0 ; i < 9999 ; i ++ ){
a+=1;
}
var f = new NativeFunction(base.add(0x21D8),'void',['int']);
f(0)
//当程序执行结束后把结果发送个消息处理函数
}

});
}
}
setImmediate(main);

自动化脚本,遇到报错时需要往flag字符串里添加已知正确字符才能继续爆破

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
import subprocess
import win32api
import win32con
def start_suspended_process(proc_name):
creation_flags = 0x14
process = subprocess.Popen(proc_name, creationflags=creation_flags)
print("子进程已启动并挂起")
return process.pid
import ctypes
def resume_process(pid):
try:
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.DebugActiveProcess(pid)
print(f"进程 {pid} 已恢复.")
except OSError as e:
print(f"恢复进程时发生错误: {str(e)}")

printable = "`!\"#$%&'()*+,-./:;<=>?@[\]^_{|}~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
#以`开头是因为flag中极大概率不会出现该字符 所以该字符作为一个检验的标准

import frida, sys
number = 102741
number =103833
new_number = 0
def is_right():
global new_number,number
if new_number > number:
number = new_number
return True
else:
return False

def on_message(message, data):
global new_number
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
new_number = message['payload']
# val = int(message['payload'], 16)
# script.post({'type': 'input', 'payload': str(val * 2)})
elif message['type'] == "error":
print(message["description"])
print(message["stack"])
print(message["fileName"],"line:",message["lineNumber"],"colum:",message["columnNumber"])
else:
print(message)
pass
jscode = open("h00k.js","rb").read().decode()
import subprocess
flag = "flag{O"

for index in range(len(flag),44):
for i in printable:
process = subprocess.Popen("ezVm.exe",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
tmp_flag = (flag+i).ljust(43,"A")+"}"
print(tmp_flag)
print("try index:",index ,"chr :",i)


session = frida.attach("ezVM.exe")
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
#print('[*] Start attach')
script.load()

process.stdin.write(tmp_flag)

output, error = process.communicate()
if(i == '`'):
number = new_number

elif(is_right() == True):
flag +=i
print(flag)
break
process.terminate()

最后一个字符需要爆破,不过可以根据前面内容先猜#

2024ZJCTF-Re3

题目提示debug,函数里面有tls得到1.exe

v4像key,后面是分组加密

一眼aes但是无法解密

用frida尝试去hook输入,密文,key等,执行frida -l .\hook.js -n 1.exe

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
var inter=setInterval(function () {
var sgame = Process.findModuleByName("1.exe");
if(sgame==null){
console.log("无");
return;
}

clearInterval(inter);
console.log(""+sgame.base.add(0x001EC9));
Interceptor.attach(sgame.base.add(0x001EC9), {
onEnter: function (args) {
var rax=this.context.rax;
console.log("" +rax);
// 检查密文有没有被修改
console.log(hexdump(ptr(rax),{length: 50,ansi:true}));

}
})

Interceptor.attach(sgame.base.add(0x001E2C), {
onEnter: function (args) {
console.log("input flag");
// 检查输入 flag 的时候有没有被修改
console.log(hexdump(ptr(this.context.rdx),{length: 16,ansi:true}));

}
})

Interceptor.attach(sgame.base.add(0x1380), {
onEnter: function (args) {
var rdx=this.context.rdx;
// 检查 key ,sbox , 进入函数的 flag 有没有被修改
console.log("key" );
console.log(hexdump(ptr(rdx),{length: 16,ansi:true}));
console.log("flag:" );
console.log(hexdump(ptr(this.context.rcx),{length: 16,ansi:true}));
console.log("sbox:" );
console.log(hexdump(ptr(sgame.base.add(0x5160)),{length: 256,ansi:true}));
}
})
} , 1)

input flag有变化说明进入aes加密的明文有偏移,总共33次调用sub_140001380,对应3组密文,所以密文长度为48

输入0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&’()*+,-./:;<=>?@[\]^_得到新的映射表

修改后的密文

最终脚本,PlainText1和PlainText2都是用来存放中间变量

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
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void AddRoundKey(unsigned char *plaintext, unsigned char * CipherKey)/* 轮密钥加 */
{
for (int j = 0; j < 16; j++) plaintext[j] = plaintext[j] ^ CipherKey[j];
}
void SubBytes(unsigned char *plaintext, unsigned char *plaintextencrypt, int count)/*S 盒置换 */
{
unsigned int row, column;
unsigned char Sbox[16][16] = {
/* 0 1 2 3 4 5 6 7 8 9 a b c d e f */
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16

};// 填充 Sbox 矩阵
for (int i = 0; i < count; i++)
{
row = (plaintext[i] & 0xF0) >> 4;
column = plaintext[i] & 0x0F;
plaintextencrypt[i] = Sbox[row][column];
}
}
void SubBytesRe(unsigned char *plaintext, unsigned char *plaintextencrypt, int count)/*S 盒逆置换 */
{
unsigned int row, column;
unsigned char Sbox[16][16] = {
82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37, 114, 248, 246, 100, 134, 104, 152, 22, 212, 164, 92, 204, 93, 101, 182, 146, 108, 112, 72, 80, 253, 237, 185, 218, 94, 21, 70, 87, 167, 141, 157, 132, 144, 216, 171, 0, 140, 188, 211, 10, 247, 228, 88, 5, 184, 179, 69, 6, 208, 44, 30, 143, 202, 63, 15, 2, 193, 175, 189, 3, 1, 19, 138, 107, 58, 145, 17, 65, 79, 103, 220, 234, 151, 242, 207, 206, 240, 180, 230, 115, 150, 172, 116, 34, 231, 173, 53, 133, 226, 249, 55, 232, 28, 117, 223, 110, 71, 241, 26, 113, 29, 41, 197, 137, 111, 183, 98, 14, 170, 24, 190, 27, 252, 86, 62, 75, 198, 210, 121, 32, 154, 219, 192, 254, 120, 205, 90, 244, 31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125 }; // 填充 Sbox 矩阵
for (int i = 0; i < count; i++)
{
row = (plaintext[i] & 0xF0) >> 4;
column = plaintext[i] & 0x0F;
plaintextencrypt[i] = Sbox[row][column];
}
}
void ShiftRowsRe(unsigned char *plaintextencrypt)/* 行移位的逆 */
{
unsigned char temp = 0;
for (int i = 0; i < 4; i++)// 第 i 行
{
for (int j = 0; j < 4 - i; j++)// 第 j 次左移
{
temp = plaintextencrypt[i];
for (int k = 0; k < 4; k++)
plaintextencrypt[i + 4 * k] = plaintextencrypt[i + 4 * (k + 1)];
plaintextencrypt[i + 12] = temp;
}
}
}
void ShiftRows(unsigned char *plaintextencrypt)/* 行移位 */
{
unsigned char temp = 0;
for (int i = 0; i < 4; i++)// 第 i 行
{
for (int j = 0; j < i; j++)// 第 j 次左移
{
temp = plaintextencrypt[i];
for (int k = 0; k < 4; k++)
plaintextencrypt[i + 4 * k] = plaintextencrypt[i + 4 * (k + 1)];
plaintextencrypt[i + 12] = temp;
}
}
}
unsigned char Mult2(unsigned char num)/* 列混淆 */
{
unsigned char temp = num << 1;
if ((num >> 7) & 0x01)
temp = temp ^ 27;
return temp;
}
unsigned char Mult3(unsigned char num)
{
return Mult2(num) ^ num;
}
void MixColumns(unsigned char *plaintextencrypt, unsigned char *plaintextcrypt)
{
int i;
for (i = 0; i < 4; i++)
plaintextcrypt[4 * i] = Mult2(plaintextencrypt[4 * i]) ^ Mult3(plaintextencrypt[4 * i + 1]) ^ plaintextencrypt[4 * i + 2] ^ plaintextencrypt[4 * i + 3];
for (i = 0; i < 4; i++)
plaintextcrypt[4 * i + 1] = plaintextencrypt[4 * i] ^ Mult2(plaintextencrypt[4 * i + 1]) ^ Mult3(plaintextencrypt[4 * i + 2]) ^ plaintextencrypt[4 * i + 3];
for (i = 0; i < 4; i++)
plaintextcrypt[4 * i + 2] = plaintextencrypt[4 * i] ^ plaintextencrypt[4 * i + 1] ^ Mult2(plaintextencrypt[4 * i + 2]) ^ Mult3(plaintextencrypt[4 * i + 3]);
for (i = 0; i < 4; i++)
plaintextcrypt[4 * i + 3] = Mult3(plaintextencrypt[4 * i]) ^ plaintextencrypt[4 * i + 1] ^ plaintextencrypt[4 * i + 2] ^ Mult2(plaintextencrypt[4 * i + 3]);
}
/* 逆列混淆 */
#define xtime(x) ((x<<1) ^ (((x>>7) & 1) * 0x1b))
#define Multiply(x,y) (((y & 1) * x) ^ ((y>>1 & 1) * xtime(x)) ^ ((y>>2 & 1) * xtime(xtime(x))) ^ ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ ((y>>4 & 1) * xtime(xtime(xtime(xtime(x))))))
void MixColumnsRe(unsigned char *state)
{

unsigned char a, b, c, d;
for (int i = 0; i < 4; i++)
{
a = state[4*i];
b = state[4*i+1];
c = state[4*i+2];
d = state[4*i+3];
state[4 * i] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09);
state[4 * i + 1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d);
state[4 * i + 2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b);
state[4 * i + 3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e);
}
}
int CharToWord(unsigned char *character, int first)/* 字节转字 */
{
return (((int)character[first] & 0x000000ff) << 24) | (((int)character[first + 1] & 0x000000ff) << 16) | (((int)character[first + 2] & 0x000000ff) << 8) | ((int)character[first + 3] & 0x000000ff);
}
void WordToChar(unsigned int word, unsigned char *character)/* 字转字节 */
{
for (int i = 0; i < 4; character[i++] = (word >> (8 * (3 - i))) & 0xFF);
}
void ExtendCipherKey(unsigned int *CipherKey_word, int round)/* 密钥扩展 */
{
unsigned char CipherKeyChar[4] = { 0 },CipherKeyCharEncrypt[4] = { 0 };
unsigned int Rcon[10] = { 0x01000000,0x02000000,0x04000000,0x08000000,0x10000000,0x20000000,0x40000000,0x80000000,0x1B000000,0x36000000 }; // 轮常量
for (int i = 4; i < 8; i++)
{
if (!(i % 4))
{
WordToChar((CipherKey_word[i - 1] >> 24) | (CipherKey_word[i - 1] << 8), CipherKeyChar);
SubBytes(CipherKeyChar, CipherKeyCharEncrypt, 4);
CipherKey_word[i] = CipherKey_word[i - 4] ^ CharToWord(CipherKeyCharEncrypt, 0) ^ Rcon[round];
}
else
CipherKey_word[i] = CipherKey_word[i - 4] ^ CipherKey_word[i - 1];
}
}
#include <stdlib.h>
void genString(int size){
char *text = (char *)malloc(size * sizeof(char));
for (size_t i = 0; i < size; i++)
{
text[i]=35+i;
}
puts(text);

}
char table[]="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ";
char res[]={0x36,0x35,0x34,0x33,0x3a,0x39,0x38,0x37,0x3e,0x3d,0x85,0x84,0x83,0x8a,0x89,0x88,0x87,0x8e,0x8d,0x8c,0x8b,0x92,0x91,0x90,0x8f,0x76,0x75,0x74,0x73,0x7a,0x79,0x78,0x77,0x7e,0x7d,0x7c,0xa5,0xa4,0xa3,0xaa,0xa9,0xa8,0xa7,0xae,0xad,0xac,0xab,0xb2,
0xb1,0xb0,0xaf,0x96,0x95,0x94,0x93,0x9a,0x99,0x98,0x97,0x9e,0x9d,0x9c,0x45,0x44,0x43,0x4a,0x49,0x48,0x47,0x4e,0x4d,0x4c,0x4b,0x52,0x51,0x50,0x4f,0x3c,0x3b,0x42,0x41,0x40,0x3f,0xa6,0x9b,0xa2,0xa1,0xa0,0x9f,0x86,0x7b,0x82,0x81,0x80,0x66,0x66
};
char getcc(char in){
for (size_t i = 0; i < 96; i++)
{
if (in==res[i])
{
return table[i];
}

}
}
void main()
{
printf("**************AES加解密***************\n");
int i = 0, k;
unsigned mm[]={0x71,0x55,0x7f,0xa8,0xfa,0x0e,0xa3,0x19,0xa0,0x5c,0xf9,0x0e,0x9b,0x0b,0x5e,0xfc,0xb5,0xa8,0x49,0xfd,0x90,0x99,0x74,0xc7,0x77,0x02,0x6a,0xf5,0x9a,0x6a,0xba,0x7f,0xfb,0xe7,0x68,0xda,0x54,0xee,0xe8,0xbb,0x78,0x01,0xe7,0xbb,0xa2,0x95,0x95,0xfa};
unsigned char PlainText[48] ="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
,
CipherKey[16] = { 0x05,0x06,0x07,0x08,0x37,0x42,0x4d,0x58,0x63,0x00,0x0a,0x0c,0x0d,0x0e,0x0f,0x10 },
CipherKey1[16] = { 0x05,0x06,0x07,0x08,0x37,0x42,0x4d,0x58,0x63,0x00,0x0a,0x0c,0x0d,0x0e,0x0f,0x10},
PlainText1[48] = {
0x71, 0x55, 0x7F, 0xA8, 0xFA, 0x0E, 0xA3, 0x19, 0xA0, 0x5C, 0xF9, 0x0E, 0x9B, 0x0B, 0x5E, 0xFC,
0xB5, 0xA8, 0x49, 0xFD, 0x90, 0x99, 0x74, 0xC7, 0x77, 0x02, 0x6A, 0xF5, 0x9A, 0x6A, 0xBA, 0x7F,
0xFB, 0xE7, 0x68, 0xDA, 0x54, 0xEE, 0xE8, 0xBB, 0x78, 0x01, 0xE7, 0xBB, 0xA2, 0x95, 0x95, 0xFA
},
PlainText2[48] = {
0x71, 0x55, 0x7F, 0xA8, 0xFA, 0x0E, 0xA3, 0x19, 0xA0, 0x5C, 0xF9, 0x0E, 0x9B, 0x0B, 0x5E, 0xFC,
0xB5, 0xA8, 0x49, 0xFD, 0x90, 0x99, 0x74, 0xC7, 0x77, 0x02, 0x6A, 0xF5, 0x9A, 0x6A, 0xBA, 0x7F,
0xFB, 0xE7, 0x68, 0xDA, 0x54, 0xEE, 0xE8, 0xBB, 0x78, 0x01, 0xE7, 0xBB, 0xA2, 0x95, 0x95, 0xFA
};
unsigned int CipherKey_word[44] = { 0 };
for (i = 0; i < 4; CipherKey_word[i++] = CharToWord(CipherKey, 4 * i));
printf("密钥:");
for (k = 0; k < 16; k++) printf("%2X ", CipherKey[k]);
printf("\n明文:");
for (k = 0; k < 16; k++) printf("%02X ", PlainText[k]);
printf("\n**************开始加密****************");
AddRoundKey(PlainText, CipherKey);
for (i = 0; i < 9; i++)
{
SubBytes(PlainText, PlainText1, 16);/*S 盒置换 */
ShiftRows(PlainText1); /* 行移位 */
MixColumns(PlainText1, PlainText2); /* 列混淆 */
(CipherKey_word + 4 * i, i);/* 子密钥生成 */
for (k = 0; k < 4; k++) WordToChar(CipherKey_word[k + 4 * (i + 1)], CipherKey + 4 * k);
for (k = 0; k < 16; k++) printf("%02X ", CipherKey[k]);
AddRoundKey(PlainText2, CipherKey);/* 轮密钥加 */
for (k = 0; k < 16; k++) PlainText[k] = PlainText2[k];
}

SubBytes(PlainText, PlainText1, 16);
ShiftRows(PlainText1);
ExtendCipherKey(CipherKey_word + 4 * i, i);
for (k = 0; k < 4;WordToChar(CipherKey_word[k + 4 * (i + 1)], CipherKey + 4 * k), k++);
AddRoundKey(PlainText1, CipherKey);
printf("\n\n最终AES加密后的密文为:");
for (i = 0; i < 16; i++) printf("%02X ", PlainText1[i]);

for (size_t i = 0; i < 16; i++)
{
PlainText1[i]=mm[i]; // 解密第二段第三段偏移 16,32 即可
}


printf("\n\n**************开始解密***************\n\n");
AddRoundKey(PlainText1, CipherKey);
for (i = 0; i < 9; i++)
{
SubBytesRe(PlainText1, PlainText, 16);/*S 盒置换 */
for (k = 0; k < 4; WordToChar(CipherKey_word[k + 40 - 4 * (i + 1)], CipherKey + 4 * k),k++);/* 子密钥生成 */
ShiftRowsRe(PlainText);/* 行移位逆 */
AddRoundKey(PlainText, CipherKey);/* 轮密钥加 */
MixColumnsRe(PlainText);/* 列混淆逆运算 */
for (k = 0; k < 16;PlainText1[k] = PlainText[k],k++);
}
printf("\n最后一次循环:");
ShiftRowsRe(PlainText);/* 行移位逆 */
SubBytesRe(PlainText, PlainText1, 16);/*S 盒置换 */
AddRoundKey(PlainText1, CipherKey1);
printf("\n最终AES解密后的明文为:");

for (i = 0; i < 16; i++) printf("%c",getcc( PlainText1[i]));
printf("\n");

}

最后三段flag合并得到:flag{fc6b0f5a-8f78-4d68-86bc-fe10662969fb}

2023巅峰极客-m1_read

RFID的门禁卡程序

识别出来是AES

白盒加密是一种特殊的加密方法,它允许加密数据在不受信任的环境中被解密而无需暴露密钥本身,不会直接保存一个明显的16字节 key,而是把密钥融合进大量查表、轮函数和编码变换中

针对白盒加密可以使用DFA攻击,在倒数第一轮列混淆和倒数第二轮列混淆之间修改中间结果的一个字节,会导致最终密文和正确密文有4个字节不同。通过多次的修改,得到多组错误的密文,然后通过正确密文和这些错误密文推算出加密模式下第10轮的密钥,从而能推算出原始密钥

aes+异或0x66

1
2
3
4
5
6
7
8
9
10
11
12
13
import frida
import os

currentDir = os.getcwd()
print(currentDir)
session = frida.attach("m1_read.exe")

def getJsScript(fileName="getKey.js", fileDir=currentDir) -> str:
with open(fileDir + "\\" + fileName, encoding='utf-8') as jsScript:
return jsScript.read().rstrip()

script = session.create_script(getJsScript())
script.load()

用frida hook key,在第9轮时随机修改中间状态的1个字节,第一次输出可以看作正确密文,后面多次输出是在第9轮随机改一个字节后的错误密文

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
var baseAddr = Module.findBaseAddress("m1_read.exe")
// AES函数偏移:0x4BF0
var whiteAES = new NativeFunction(baseAddr.add(0x4BF0), 'pointer', ['pointer','pointer'])
// count = 9,这样第一轮数据恰好对应正确的数据
var count = 9

Interceptor.attach(baseAddr.add(0x4C2C),{
// 列混淆处的偏移:0x4C2C
// 若不确定,可以通过测试来看:
// 1. 若正确数据与异常数据全部不同,则修改太早
// 2. 若正确数据与异常数据仅一个不同,则修改太晚
onEnter: function(args){
count++;
// 第九轮修改中间结果
if(count == 9){
// 参考汇编代码可以发现rdi记录第九轮中间数据的地址
// "add(Math.floor(Math.random()*16))":
// 表示对操作的地址进行偏移,值属于{1,2,3}
// (0,4的可能性基本为0)
//
// "writeU8(Math.floor(Math.random()*256))":
// 表示对写入的值进行修改,值属于{1,2,3,...,E,F}
// (0,16的可能性基本为0)
// 综上,这实现了对中间数据的单个Byte修改,取消注释下述代码即可看出
var p = ptr(this.context.rdi)
// console.log("ori: ", p.readByteArray(16), "\n")
p.add(Math.floor(Math.random()*16)).writeU8(Math.floor(Math.random()*256))
// console.log("out: ", p.readByteArray(16), "\n")
}
},
onLeave:(retval) =>{
}
})

for (let index = 0; index < 33; index++){
// 取消注释下述代码即可得到每一轮的数据
var l = Memory.allocAnsiString("0123456789abcdef");
var b = Memory.alloc(16);
whiteAES(l, b);
// console.log(b.readByteArray(16));
count = 0;
}
# 异或处理脚本
data = """
00000000 72 93 98 12 0f 00 94 f4 03 7a 44 ee dd 99 20 6f r........zD... o
00000000 72 93 98 5a 0f 00 aa f4 03 2d 44 ee 16 99 20 6f r..Z.....-D... o
00000000 72 56 98 12 b6 00 94 f4 03 7a 44 30 dd 99 b9 6f rV.......zD0...o
00000000 1c 93 98 12 0f 00 94 89 03 7a ce ee dd c8 20 6f .........z.... o
00000000 72 93 98 c4 0f 00 d4 f4 03 d4 44 ee ac 99 20 6f r.........D... o
00000000 72 17 98 12 c1 00 94 f4 03 7a 44 7a dd 99 02 6f r........zDz...o
00000000 72 93 98 e5 0f 00 ad f4 03 0a 44 ee 08 99 20 6f r.........D... o
00000000 72 84 98 12 6a 00 94 f4 03 7a 44 68 dd 99 a6 6f r...j....zDh...o
00000000 d2 93 98 12 0f 00 94 57 03 7a 31 ee dd 36 20 6f .......W.z1..6 o
00000000 72 93 65 12 0f 7c 94 f4 90 7a 44 ee dd 99 20 0f r.e..|...zD... .
00000000 72 93 98 3a 0f 00 e8 f4 03 aa 44 ee 98 99 20 6f r..:......D... o
00000000 72 93 98 44 0f 00 49 f4 03 b2 44 ee 24 99 20 6f r..D..I...D.$. o
00000000 73 93 98 12 0f 00 94 1f 03 7a 76 ee dd 6b 20 6f s........zv..k o
00000000 72 7d 98 12 7e 00 94 f4 03 7a 44 bf dd 99 23 6f r}..~....zD...#o
00000000 72 20 98 12 8a 00 94 f4 03 7a 44 c4 dd 99 35 6f r .......zD...5o
00000000 5e 93 98 12 0f 00 94 35 03 7a 1a ee dd 20 20 6f ^......5.z... o
00000000 72 93 98 a5 0f 00 1e f4 03 77 44 ee 15 99 20 6f r........wD... o
00000000 72 93 98 5c 0f 00 07 f4 03 a5 44 ee 06 99 20 6f r..\......D... o
00000000 72 93 eb 12 0f e0 94 f4 13 7a 44 ee dd 99 20 2a r........zD... *
00000000 72 93 98 97 0f 00 f8 f4 03 c5 44 ee 2a 99 20 6f r.........D.*. o
00000000 72 93 98 ab 0f 00 99 f4 03 d0 44 ee 5b 99 20 6f r.........D.[. o
00000000 72 93 98 cb 0f 00 bd f4 03 9d 44 ee 58 99 20 6f r.........D.X. o
00000000 72 73 98 12 05 00 94 f4 03 7a 44 c5 dd 99 3d 6f rs.......zD...=o
00000000 72 dd 98 12 bf 00 94 f4 03 7a 44 b9 dd 99 45 6f r........zD...Eo
00000000 72 93 67 12 0f 5c 94 f4 d6 7a 44 ee dd 99 20 bb r.g..\...zD... .
00000000 72 84 98 12 42 00 94 f4 03 7a 44 da dd 99 f3 6f r...B....zD....o
00000000 72 68 98 12 b2 00 94 f4 03 7a 44 5c dd 99 b1 6f rh.......zD\...o
00000000 72 e5 98 12 66 00 94 f4 03 7a 44 27 dd 99 42 6f r...f....zD'..Bo
00000000 72 fb 98 12 6e 00 94 f4 03 7a 44 7c dd 99 cc 6f r...n....zD|...o
00000000 72 93 98 86 0f 00 27 f4 03 18 44 ee e0 99 20 6f r.....'...D... o
00000000 72 93 63 12 0f 29 94 f4 8a 7a 44 ee dd 99 20 b0 r.c..)...zD... .
00000000 72 93 98 10 0f 00 cc f4 03 01 44 ee cd 99 20 6f r.........D... o
00000000 72 e0 98 12 77 00 94 f4 03 7a 44 ba dd 99 9a 6f r...w....zD....o
"""

def cut(obj, sec):
return [obj[i:i+sec] for i in range(0, len(obj), sec)]

data = cut(data, 76)
data = data[:-1]
for i in range(len(data)):
data[i] = data[i][11:-18]
for i in data:
tmp = i.split(' ')
res = ''
for j in tmp:
res += '{:02x}'.format(int(j, 16) ^ int("66", 16))
print(res)

利用phoenixAES实现DFA

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
# s1是前文中的33轮数据,以字符串形式存储(包含换行,否则无法识别)
import phoenixAES
s1 = """
14f5fe746966f292651c2288bbff4609
14f5fe3c6966cc92654b228870ff4609
1430fe74d066f292651c2256bbffdf09
7af5fe746966f2ef651ca888bbae4609
14f5fea26966b29265b22288caff4609
1471fe74a766f292651c221cbbff6409
14f5fe836966cb92656c22886eff4609
14e2fe740c66f292651c220ebbffc009
b4f5fe746966f231651c5788bb504609
14f50374691af292f61c2288bbff4669
14f5fe5c69668e9265cc2288feff4609
14f5fe2269662f9265d4228842ff4609
15f5fe746966f279651c1088bb0d4609
141bfe741866f292651c22d9bbff4509
1446fe74ec66f292651c22a2bbff5309
38f5fe746966f253651c7c88bb464609
14f5fec3696678926511228873ff4609
14f5fe3a6966619265c3228860ff4609
14f58d746986f292751c2288bbff464c
14f5fef169669e9265a322884cff4609
14f5fecd6966ff9265b622883dff4609
14f5fead6966db9265fb22883eff4609
1415fe746366f292651c22a3bbff5b09
14bbfe74d966f292651c22dfbbff2309
14f50174693af292b01c2288bbff46dd
14e2fe742466f292651c22bcbbff9509
140efe74d466f292651c223abbffd709
1483fe740066f292651c2241bbff2409
149dfe740866f292651c221abbffaa09
14f5fee069664192657e228886ff4609
14f50574694ff292ec1c2288bbff46d6
14f5fe766966aa9265672288abff4609
1486fe741166f292651c22dcbbfffc09
"""
with open("testfile", "wb") as t:
t.write(s1.encode())

phoenixAES.crack_file("testfile")
# 自动打印出来第10轮的密钥
# 结果如下
# B4EF5BCB3E92E21123E951CF6F8F188E

用Stark提供一轮密钥暴力破解AES-128

在提供的out.bin里根据M1卡的存储格式,第一行中,4B为校验位,剩下为厂商信息,需要解密的部分为第三行

1
2
3
4
5
6
7
8
9
10
11
12
13
from Crypto.Cipher import AES

s = "0B987EF5D94DD679592C4D2FADD4EB89"

key = bytes.fromhex("00000000000000000000000000000000")
enc = bytearray(bytes.fromhex(s))
cipher = AES.new(key, AES.MODE_ECB)

for i in range(16):
enc[i] ^= 0x66

flag = cipher.decrypt(enc)
print(flag)

flag{cddc8d28dabb4ea9}

2024ByteCTF-first_game-极限逃脱

搜索ViewController发现有两个按钮,第一个按钮是输入的数要和随机数相同

第二关限制格式为uuid,之后去掉ByteCTF{},再按 - 切 5 段

{a67be199da4b-b092-bd3e-e777-a67be199da4b}调用CC_SHA256,最后是一些字符映射

需要越狱iPhone运行ipa文件才能hook最终比较的字符串

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
const moduleName = 'ByteCTFDemo'
const resolver = new ApiResolver('objc');
const baseAddr = Module.findBaseAddress(moduleName);
function info(x,y){
if(y !== undefined && y != null){
console.log(x+' => '+y);
}
else{
console.log(x);
}
}
function logX0_ObjC(x){
Interceptor.attach(baseAddr.add(x), {
onEnter: function(){
var tmp = new ObjC.Object(this.context.x0);
console.log('log[+]:0x' + x.toString(16) + '[x0]' + ' => '+tmp);
// console.log("Type of x0 -> " + new ObjC.Object(this.context.x0).$className)
}
});
}

function passFirst(){
Interceptor.attach(baseAddr.add(0x8FD0), {
onEnter: function(args) {
},
onLeave: function(ret){
ret.replace(0);
}
});
Interceptor.attach(baseAddr.add(0x948C), {
onEnter: function(args) {
},
onLeave: function(ret){
ret.replace(0);
}
});
}

function hookSHA256(){
var buf;
Interceptor.attach(baseAddr.add(0x8F94), {
onEnter: function(args) {
info(args[0].readCString());
info('[x1]',args[1]);

buf=this.context.x2;
},
onLeave: function(ret){
info(buf.readByteArray(0x20));
}
});
}

function hookEqual(){
Interceptor.attach(baseAddr.add(0x94AC), {
onEnter: function(args) {
info(new ObjC.Object(this.context.x24) + '?=' + new ObjC.Object(this.context.x2))
},
onLeave: function(ret){
// ret.replace(1);
}
});

}
function main(){
if(ObjC.available){ //判断Object-C类方法是否已经加载进来
console.log('n[*] Starting Hooking');
}
passFirst();
logX0_ObjC(0x82A8);
hookSHA256();
hookEqual();
}

main()

参考链接:

https://bbs.kanxue.com/thread-280758.htm

https://bbs.kanxue.com/thread-280812.htm#msg_header_h2_1

https://blog.csdn.net/Ruaki/article/details/135580772

https://www.bilibili.com/video/BV1wT411N7sV/

https://www.cnblogs.com/lxh2cwl/p/14842544.html

  • 标题: Frida in CTF Android Reverse Engineering
  • 作者: j1ya
  • 创建于 : 2024-10-28 18:15:48
  • 更新于 : 2026-10-05 21:57:39
  • 链接: https://j1ya-22.github.io/2024/10/28/frida/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论