基于 Frida 对单字节加密验证程序侧信道爆破
2024-6-12 18:34:6 Author: mp.weixin.qq.com(查看原文) 阅读量:2 收藏

目前的许多 CTF 二进制竞赛中出现了许多单字节加密验证题,例如 NCTF-ezVM、TPCTF-Poly 等。这些单字节验证程序中,很多都是虚拟机(VM)类型的。VM 程序通常有一个指令分发器,在分发指令时需要一个类似于 EIP(指令指针寄存器)的累加器来确保指令正确执行。而在单字节验证的过程中,如果出现一个字节不匹配的情况,程序会立即退出。这里存在一个相关关系:当当前字节验证正确时,EIP 会向下移动,累加器也会相应增加。

因此,我们使用 Frida 在指令分发器处插入代码,在程序退出时检查累加器的变化,以此判断当前位是否验证成功。同理,若程序不是 VM 类型,则我们只需要在判断位数的累加器处插入代码即可。

通过单字节验证,验证时间随验证字节长度呈线性增长,这远远优于传统爆破方法的指数增长。结合之前的描述,这种优化源于对虚拟机程序的特定操作方式,利用 EIP 的变化关系,在单字节验证过程中进行精准的判断和优化。


相关工具

IDA

IDA(Interactive Disassembler)是一款强大的反汇编软件,用于逆向工程和漏洞分析。它允许用户将机器码转换成可读的汇编代码,以便更好地理解程序的运行方式和结构。IDA 提供了丰富的功能,包括自动分析、图形化表示程序的控制流、数据流和函数调用关系,以及交互式地修改和调试程序。它广泛应用于软件逆向工程、漏洞分析、恶意代码分析等领域,是安全研究人员和逆向工程师的首选工具之一。IDA 的界面和功能使得用户可以深入分析程序,了解其内部结构和运行机制,有助于发现漏洞、理解程序逻辑以及编写补丁。

Frida

Frida 是一个强大的动态分析工具,可用于在运行时修改和监视应用程序。它提供了跨平台的框架,可以对多种操作系统、架构和应用程序类型进行操作。Frida 允许你通过 JavaScript 或 Python 等脚本语言来编写脚本,然后将这些脚本注入到目标应用程序中,从而实现对目标应用程序的监视、跟踪和修改。


可行性验证

示例程序:

#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int status;
typedef int selemtype;
#include<windows.h>
char flag[] = "flag{12321213}";
char w[] = "Wrong!";
char r[] = "Right!";

int main ()
{
char input[256] = {0};
gets(input);
for(int i = 0 ; flag[i] ; i ++ ){
if(flag[i]!=input[i]){
puts("Wrong!");
Sleep(0x10);
return 0;
}
}
puts("Right!");
}

这段代码实现了一个简单的单字节比较程序,与之前介绍的虚拟机指令分发不同,这里只是进行了简单的单字节比较。但与之前相似的地方在于存在一个类似累加器的概念,我们可以通过观察这个累加器的变化来判断当前字节是否正确。

Hook 地址计算

将程序使用 MinGW GCC 9.2.0 32-bit Debug 并且关闭基地址随机化编译,我们得到 test1.exe。并使用 IDA 分析。

图(1)

图(2)

从图(1)中可以确定i++指令的地址是 0x4014AB,而根据图(2)判断错误时进入puts函数的第一条地址是 0x401489。因此,我们需要 hook 的地址是这两个记录的地址。在i++处,我们要植入的代码是使我们的插桩数加一,在puts处,我们要植入的代码是回显我们的插桩数。

Hook 代码:

var number = 0
function main()
{
var base = Module.findBaseAddress("test1.exe")

if(base){
Interceptor.attach(base.add(0x1489), {
onEnter: function(args) {
send(number);
}
});
Interceptor.attach(base.add(0x14AB), {

onEnter: function(args) {
number+=1;
}

});
}
}
setImmediate(main);

该代码检测了内存中是否存在 test1.exe 程序,并且获取了 test1.exe 的基地址,通过偏移量确定了之前测定的 0x4014AB 与 0x401489。

Hook 执行

运行 test1.exe 程序,并且执行如下命令

frida -l .\h00k.js -n test1.exe

图(3)

图(4)

如果程序执行成功,你将会得到类似图(3)的界面。当我们输入'a',你会看到类似图(4)的回显。我们已知程序正确验证为 'flag{12321213}',所以再输入'f'来查看回显。

输入 'f' 后,你会看到类似以下信息:

message: {'type': 'send', 'payload': 1} data: None

然后输入 'fl' 后,你会看到类似以下信息:

message: {'type': 'send', 'payload': 2} data: None

显然,正确字节越多,得到的 number 数字也随之增加。因此,这个 number 可作为我们判断当前字节是否正确的侧信道。


对于 VM 程序验证逻辑实战

样例程序 1(VM 类型)

程序分析

程序名称 easyChallenge.exe,使用 IDA 查看该程序可以得到如下所示的伪代码

easyChllenge.exe Main 函数伪代码:

int main()
{
char Str[100]; // [esp+10h] [ebp-A0Ch] BYREF
_DWORD v2[513]; // [esp+74h] [ebp-9A8h] BYREF
_DWORD v3[100]; // [esp+878h] [ebp-1A4h] BYREF
int v4; // [esp+A08h] [ebp-14h]
size_t i; // [esp+A0Ch] [ebp-10h]

__main();
memset(v3, 0, sizeof(v3));
qmemcpy(v2, &unk_404020, sizeof(v2));
memset(Str, 0, sizeof(Str));
puts("input the flag:");
scanf("%s", Str);
if ( strlen(Str) == 32 )
{
for ( i = 0; strlen(Str) > i; ++i )
v3[i + 1] = Str[i];
v4 = 0;
while ( v4 <= 512 )
{
switch ( v2[v4] )
{
case 1:
s += v2[++v4];
goto LABEL_20;
case 2:
s -= v2[++v4];
goto LABEL_20;
case 3:
s *= v2[++v4];
goto LABEL_20;
case 4:
s /= (int)v2[++v4];
goto LABEL_20;
case 5:
s ^= v2[++v4];
goto LABEL_20;
case 6:
s = v3[v3[0]];
goto LABEL_20;
case 7:
v3[v3[0]] = s;
goto LABEL_20;
case 8:
if ( v2[v4 + 1] != s )
goto LABEL_22;
++v4;
goto LABEL_20;
case 9:
puts("right!");
return 1;
case 0xA:
s = v3[0];
goto LABEL_20;
case 0xB:
v3[0] = s;
goto LABEL_20;
default:
LABEL_20:
++v4;
break;
}
}
}
LABEL_22:
puts("wrong!");
return 0;
}

根据分析,发现 v2 存放指令集,v4 存放 EIP(指令指针寄存器)的值。程序利用 v2 中的指令集对输入进行处理,并逐字节验证。验证失败时,程序立即退出,不再继续执行。这显示了程序在验证中的严谨性,一旦发现验证失败就停止后续步骤。

因此,我们选择在 switch 语句处进行 hook 代码的插入,以便在运行到该点时对插装数进行 +1 操作。随后,我们继续在 puts 函数的地址处进行插桩数验证。这些插桩的位置如图(5)和(6)所示。

图(5)

图(6)

Hook 代码:

var number = 0
function main()
{

var base = Module.findBaseAddress("easyChallenge.exe")
if(base){
Interceptor.attach(base.add(0x3D08), {
onEnter: function(args) {
send(number);
}
});
Interceptor.attach(base.add(0x155A), {
onEnter: function(args) {
number+=1;
}
});

}
}
setImmediate(main);

同样我们运行 easyChallenge.exe 后执行如下命令。

frida -l .\h00k.js -n easyChallenge.exe

输入简单的 'a' 后,得到回显:

[Local::easyChallenge.exe ]-> message: {'type': 'send', 'payload': 0} data: None

我们观察到程序并没有经过我们插桩的点。这是因为我们的输入不足 32 位,导致程序没有进入虚拟机的分发器。

输入 '12345678911234567891123456789112' 继续查看回显:

[Local::easyChallenge.exe ]-> Process terminated

[Local::easyChallenge.exe ]->

输入 '12345678911234567891123456789112' 后,程序终止并没有返回 number 数字的回显。这可能是因为程序在返回 number 数字之前就被终止了,导致我们无法看到完整的回显信息。这种情况表明程序运行时间不足以完成整个流程,需要使用其他方法来延长程序的运行时间。观察程序发现程序中出现了 puts 函数,那么

我们可以劫持 puts 函数循环输出,来增加程序 IO 时间从而等待 number 数回传。

操作如下:

var st = Memory.allocUtf8String("Suprise!");
var f = new NativeFunction(base.add(0x3D08),'void',['pointer']);
for(var i = 0 ; i < 999 ; i ++ ){
f(st);
}

这段代码则是调用了程序中原本存在的 puts 函数,当我们放在发送 number 操作之后,程序则会等待我们回传 number。

执行操作后再重复输入 '12345678911234567891123456789112' ,就可以得到如下回显了:

[Local::easyChallenge.exe ]-> message: {'type': 'send', 'payload': 197} data: None

重复皂搓输入'f2345678911234567891123456789112',得到回显:

[Local::easyChallenge.exe ]-> message: {'type': 'send', 'payload': 203} data: None

发现当第一位输入正确时,插桩数则增大。

Exploit 编写

程序调用以及 hook 函数:

def brute(F):

def on_message(message, data):
global result
if message['type'] == 'send':
result = message['payload']
#print(result)
else:
print(message)
process = subprocess.Popen(filename, 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()
process.stdin.write(F.decode())
#print(F.decode())
output, error = process.communicate()
#print(output)

#print(f"number:{result}")
process.terminate()
return result

这段代码实现了一个名为brute的函数,函数接受一个参数F。函数解读如下:

1.定义了一个内部函数on_message(message, data),这个函数用于处理 Frida 脚本发送的消息。它检查消息类型,如果收到的消息类型是'send',则提取其中的'payload'值并将其保存在全局变量result中。

2.使用subprocess.Popen创建了一个新的进程process,并传入了一些参数,其中filename是一个可执行文件或者要执行的程序文件路径。这个进程被用于 Hook 操作。

3.使用frida.attach()方法来连接到这个进程,并创建一个 Frida 脚本script。这个脚本通过session.create_script()创建,并传入一个叫做jscode的 JavaScript 代码。

4.通过 Frida 脚本,监听了消息事件(script.on('message', on_message))。当 Frida 脚本发送消息时,会触发on_message函数进行处理。

5.过process.stdin.write(F.decode())将传入的参数F解码后作为输入写入到进程的标准输入流中。

6.使用process.communicate()方法,执行进程并等待它完成。这个函数会等待进程执行完毕并收集输出信息。

7.终止了进程的执行,使用process.terminate()方法。

8.返回result,即之前保存的消息中的'payload'值。

这段代码的目的是通过 Frida 脚本监控进程的执行,在进程执行过程中,当收到特定类型的消息时(例如'send'),将其中的'payload'提取出来作为结果返回。

调用该函数传入我们的输入,我们则可以根据函数返回的 result,来分析输入是否正确了。

爆破主体代码:

count = 0

new_number = brute(flag)
number = new_number

while count < flaglen:
number = brute(flag)
if number > new_number:
print(flag.decode())
new_number = number
count += 1
else:
flag[count] += 1
while(flag[count] > 127):
flag[count] = 33
count -= 1
flag[count] += 1
print(flag.decode())

这段代码是一个循环,尝试逐字节破解一个 flag。

让我们逐步解读:

1.count = 0:初始化一个计数器,用于追踪已经尝试的字节位置。

2.new_number = brute(flag):调用brute()函数并将结果保存在new_number中。

3.number = new_number:将new_number赋值给number

4.while count < flaglen::开始一个循环,条件是count小于flaglen,即 flag 的长度。

5.number = brute(flag):再次调用brute()函数,更新number的值。

6.如果number大于new_number

-打印出 flag 解码后的字符串。
-将 new_number 更新为 number。
-计数器 count 加一。

7.如果number不大于new_number

-对当前 flag 的 count 位置进行修改,尝试下一个字符。

-如果当前字符超出 ASCII 可打印字符范围(33 到 126),则将其重置为 33,前一个字符位置加一,继续尝试下一个字符。

8.最后输出解码后的flag字符串。

这段代码在不断尝试对一个密文进行破解,通过比较不同位置的字符对应的数字(是一个侧信道攻击中的某种特定指标)来判断解密进展,逐字节地逼近正确的结果。

最终我们获得完整 Exploit:

import subprocess
import frida
import sys
import win32api
import win32con

number = 0
flaglen = 32
filename = "easyChallenge.exe"
flag = bytearray(b'!' * flaglen)
jscode = open("h00k.js", "rb").read().decode()
new_number = 0

result = 0
def brute(F):

def on_message(message, data):
global result
if message['type'] == 'send':
result = message['payload']
#print(result)
else:
print(message)
process = subprocess.Popen(filename, 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()
process.stdin.write(F.decode())
#print(F.decode())
output, error = process.communicate()
#print(output)

#print(f"number:{result}")
process.terminate()
return result
import time

count = 0

new_number = brute(flag)
number = new_number
t = time.time()
st = t

while count < flaglen:
number = brute(flag)
if number > new_number:
print(f"本位耗时:{time.time()-t}s,正确字符为:{chr(flag[count])}")
t = time.time()
print(flag.decode())
new_number = number
count += 1
else:
flag[count] += 1
while(flag[count] > 127):
flag[count] = 33
count -= 1
flag[count] += 1
print(flag.decode())
print(f"总耗时{time.time()-st}")

程序运行结果如图(7)所示:

图(7)

平均每位耗时 2.7s,32 位程序总耗时 78s。

样例程序 2(VM 类型)

样例程序分析

程序 Main 函数伪代码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int8 v3; // r15
__int64 v4; // rdi
__int64 v5; // rax
__int64 v6; // rcx
unsigned __int8 v7; // cl
__int16 v8; // cx
int v9; // ecx
__int64 v10; // rcx
int v11; // ebp
unsigned __int64 v12; // rbx
unsigned __int8 v13; // r14
rsize_t v14; // rdx
__int64 v15; // rax
__int64 v16; // rdx
__int64 v17; // rcx

v3 = 0;
LODWORD(v4) = 0;
LABEL_2:
v5 = (unsigned int)dword_14001FF94;
while ( 2 )
{
v6 = (unsigned int)v4;
v4 = (unsigned int)(v4 + 1);
switch ( byte_140004040[v6] )
{
case 0xBu:
v5 = (unsigned int)(v5 + 8);
dword_14001FF94 = v5;
continue;
case 0xCu:
v8 = *(_WORD *)&byte_140004040[v4];
v5 = (unsigned int)(v5 - 2);
dword_14001FF94 = v5;
LODWORD(v4) = v4 + 2;
*(_WORD *)&byte_14002058F[v5 + 1] = v8;
continue;
case 0xEu:
v5 = (unsigned int)(v5 - 1);
dword_14001FF94 = v5;
byte_14002058F[(unsigned int)v5 + 1] = byte_14002058F[(unsigned int)(v5 + 1) + 1];
continue;
case 0x19u:
return *(_DWORD *)&byte_140004040[v4];
case 0x32u:
v7 = byte_140004040[v4];
v5 = (unsigned int)(v5 - 1);
dword_14001FF94 = v5;
LODWORD(v4) = v4 + 1;
byte_14002058F[v5 + 1] = v7;
continue;
case 0x49u:
byte_14002058F[(unsigned int)(v5 + 1) + 1] = ~byte_14002058F[(unsigned int)(v5 + 1) + 1];
continue;
case 0x71u:
v16 = (unsigned int)(v5 + 1);
byte_14002058F[v16 + 1] &= byte_14002058F[v5 + 1];
v5 = (unsigned int)v16;
dword_14001FF94 = v16;
continue;
case 0x72u:
byte_14002058F[v5 + 1] = ~byte_14002058F[v5 + 1];
continue;
case 0x7Bu:
v14 = (char)byte_14002058F[v5 + 1];
v15 = (unsigned int)(v5 - 7);
dword_14001FF94 = v15;
*(_QWORD *)&byte_14002058F[v15 + 1] = Buffer;
gets_s(Buffer, v14);
goto LABEL_2;
case 0x7Cu:
v5 = (unsigned int)(v5 - 2);
dword_14001FF94 = v5;
byte_14002058F[v5 + 1] = byte_14002058F[(unsigned int)(v5 + 2) + 1];
byte_14002058F[(unsigned int)(v5 + 1) + 1] = byte_14002058F[(unsigned int)(v5 + 3) + 1];
continue;
case 0x8Du:
if ( !byte_14002058F[v5 + 1] )
LODWORD(v4) = *(_DWORD *)&byte_14002058F[v5 + 2];
v5 = (unsigned int)(v5 + 5);
dword_14001FF94 = v5;
continue;
case 0x8Eu:
v5 = (unsigned int)(v5 + 2);
dword_14001FF94 = v5;
continue;
case 0x91u:
v9 = *(_DWORD *)&byte_140004040[v4];
v5 = (unsigned int)(v5 - 4);
dword_14001FF94 = v5;
LODWORD(v4) = v4 + 4;
*(_DWORD *)&byte_14002058F[v5 + 1] = v9;
continue;
case 0x99u:
v5 = (unsigned int)(v5 + 4);
dword_14001FF94 = v5;
continue;
case 0xADu:
v5 = (unsigned int)(v5 - 1);
dword_14001FF94 = v5;
byte_14002058F[(unsigned int)v5 + 1] = v3;
continue;
case 0xB5u:
if ( byte_14002058F[v5 + 1] )
byte_14002058F[v5 + 1] = 1;
continue;
case 0xB7u:
v3 = byte_14002058F[v5 + 1];
goto LABEL_9;
case 0xB8u:
byte_14002058F[v5 + 1] = *(_BYTE *)(byte_14002058F[v5 + 1] + *(_QWORD *)&byte_14002058F[v5 + 2]);
continue;
case 0xD3u:
v10 = *(_QWORD *)&byte_140004040[v4];
v5 = (unsigned int)(v5 - 8);
dword_14001FF94 = v5;
LODWORD(v4) = v4 + 8;
*(_QWORD *)&byte_14002058F[v5 + 1] = v10;
continue;
case 0xEAu:
LABEL_9:
v5 = (unsigned int)(v5 + 1);
dword_14001FF94 = v5;
continue;
case 0xFBu:
v11 = byte_140004040[v4];
v12 = 0i64;
v4 = (unsigned int)(v4 + 1);
v13 = byte_140004040[v4] ^ v11;
LODWORD(v4) = v4 + 1;
if ( v13 )
{
do
{
putchar(v11 ^ byte_14002058F[v13 + (unsigned int)v5 - v12]);
LODWORD(v5) = dword_14001FF94;
++v12;
}
while ( v12 < v13 );
}
dword_14001FF94 = v13 + (_DWORD)v5;
putchar(10);
goto LABEL_2;
case 0xFFu:
v17 = byte_14002058F[v5 + 1];
if ( (_BYTE)v17 )
{
if ( (_BYTE)v17 == 1 )
{
if ( (unsigned int)v5 >= 0x100ui64 )
{
_report_rangecheckfailure(v17, (unsigned int)v5, envp);
__debugbreak();
}
byte_14002058F[v5 + 1] = 0;
}
}
else
{
byte_14002058F[v5 + 1] = 1;
}
continue;
default:
continue;
}
}
}

本程序也属于 VM 类型,相较于上一程序,本程序分析起来更加复杂,验证逻辑更为繁琐,因此我们也可以考虑道使用之前提出的侧信道解法来爆破本程序的正确输入。我们 hook 的点位仍然是指令分发器的位置以及 putchar 位置,分别为图(8)和(9):

图(8)

图(9)

HOOK 脚本如下:

var number = 0
function main()
{
var base = Module.findBaseAddress("ezVM.exe")
if(base){
Interceptor.attach(base.add(0x1044), {

onEnter: function(args) {
number+=1

}

});
Interceptor.attach(base.add(0x113f), {
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);

由于本题在 hook 程序之后导致程序退出异常缓慢,因此在程序中可以找到 exit 函数的地址,通过调用 exit 函数来让程序退出。并且,发现最后一位如果争取的 eip 运行次数会减少。那么我们更改 exp 为检测到 number 变化即为正确字符。

Exploit 编写

import subprocess
import frida
import sys
import win32api
import win32con

number = 0
flaglen = 43
filename = "ezVM.exe"
# flag{O1SC_VM_1s_h4rd_to_r3v3rs3_#a78abffaa#}
flag = bytearray(b'flag{O1SC_VM_1s_h4rd_!!!!!!!!!!!!!!!!!!!!!!}')
jscode = open("h00k.js", "rb").read().decode()
new_number = 0

result = 0
def brute(F):

def on_message(message, data):
global result
if message['type'] == 'send':
result = message['payload']
#print(result)
else:
print(message)
process = subprocess.Popen(filename, 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()
process.stdin.write(F.decode())
#print(F.decode())
output, error = process.communicate()
#print(output,error)

#print(f"number:{result}")
process.terminate()
return result
import time

count = 21

new_number = brute(flag)
number = new_number
t = time.time()
st = t

while count < flaglen:
number = brute(flag)
print(flag.decode())
if number != new_number:
print(f"本位耗时:{time.time()-t}s,正确字符为:{chr(flag[count])}")
t = time.time()
print(flag.decode())
new_number = number
count += 1
else:
flag[count] += 1
while(flag[count] > 127):
flag[count] = 33
count -= 1
flag[count] += 1
print(flag.decode())
print(f"总耗时{time.time()-st}")

运行效果如图(10):

图(10)

样例程序 3(复杂加密类型)

例程序分析

Main 程序伪代码:

__int64 sub_405B4E()
{
__int64 v0; // rax
__int64 v1; // rax
unsigned int v2; // ebx
unsigned __int64 v3; // rbx
char *v4; // rax
__int64 v5; // rax
__int64 v6; // rax
__int64 result; // rax
unsigned int v8; // [rsp+10h] [rbp-70h] BYREF
int i; // [rsp+14h] [rbp-6Ch]
int j; // [rsp+18h] [rbp-68h]
int k; // [rsp+1Ch] [rbp-64h]
int m; // [rsp+20h] [rbp-60h]
int v13; // [rsp+24h] [rbp-5Ch]
int v14; // [rsp+28h] [rbp-58h]
unsigned int v15; // [rsp+2Ch] [rbp-54h]
unsigned int *v16; // [rsp+30h] [rbp-50h]
__int64 v17; // [rsp+38h] [rbp-48h]
char v18[40]; // [rsp+40h] [rbp-40h] BYREF
unsigned __int64 v19; // [rsp+68h] [rbp-18h]

v19 = __readfsqword(0x28u);
v0 = sub_470AD0(&unk_9DD480, "Welcome to the world of polynomial!");
sub_46F640(v0, sub_470400);
sub_4762C0(v18);
sub_470AD0(&unk_9DD480, "Please input the flag: ");
sub_407F00(&unk_9DD5A0, v18);
v13 = sub_4765F0(v18);
if ( v13 == 41 )
{
v14 = 42;
v15 = 2 << sub_405FD2(43LL);
v16 = (unsigned int *)sub_4DF5D0(4LL * (int)v15);
v17 = sub_4DF5D0(4LL * (int)v15);
sub_4010A0(v16, 0LL, 4LL * (int)v15);
sub_4010A0(v17, 0LL, 4LL * (int)v15);
sub_405FF2(&v8, 1LL);
*v16 = v8;
for ( i = 1; ; ++i )
{
v3 = i;
if ( v3 > sub_4765F0(v18) )
break;
v4 = (char *)sub_4767E0(v18, i - 1);
sub_405FF2(&v8, (unsigned int)*v4);
v16[i] = v8;
}
sub_40588B(v16, v17, v15);
sub_404F7A(v17, v16, v15);
sub_40561C(v16, v17, v15);
for ( j = 0; j < (int)v15; ++j )
{
sub_405FF2(&v8, dword_5DA140[j]);
sub_406186(v17 + 4LL * j, v8);
}
sub_4054EC(v17, v16, v15);
sub_405FF2(&v8, 1LL);
*v16 = v8;
sub_4054EC(v16, v17, v15);
for ( k = 0; k < (int)v15; ++k )
sub_4061EA(v17 + 4LL * k, 19260817LL);
sub_40561C(v17, v16, v15);
sub_404EAF(v16, v17, v15);
for ( m = 0; m < (int)v15; ++m )
{
if ( *(_DWORD *)(4LL * m + v17) != dword_6DA140[m] )
{
v5 = sub_470AD0(&unk_9DD480, "Failed, please try again!");
sub_46F640(v5, sub_470400);
v2 = m;
goto LABEL_18;
}
}
v6 = sub_470AD0(&unk_9DD480, "Congratulations! You got the flag!");
sub_46F640(v6, sub_470400);
sub_4DF910(v16);
sub_4DF910(v17);
v2 = 0;
}
else
{
v1 = sub_470AD0(&unk_9DD480, "Failed, please try again!");
sub_46F640(v1, sub_470400);
v2 = 1;
}
LABEL_18:
sub_4763D0(v18);
result = v2;
if ( __readfsqword(0x28u) != v19 )
sub_52FA20();
return result;
}

本程序加密逻辑十分繁琐以及复杂,但是依旧满足单字节加密验证。

Exploit

var number = 0
function main()
{
var base = Module.findBaseAddress("poly_11ea8ad97a9ac01e68a1b5011128fa34")
if(base){
Interceptor.attach(base.add(0x5ED8), {

onEnter: function(args) {
number+=1;
//console.log("number:",number);
}

});
//console.log("执行退出");
//var f = new NativeFunction(base.add(0x21D8),'void',['int']);
//f(0)
Interceptor.attach(base.add(0x5EA9), {
onEnter: function(args) {
send(number)
var a = 0;
for(var i = 0 ; i < 9999 ; i ++ ){
a+=1;
}
}

});

Interceptor.attach(base.add(0x5EDE), {
onEnter: function(args) {
send(number)
var a = 0;
for(var i = 0 ; i < 9999 ; i ++ ){
a+=1;
}
}

});
}
}
setImmediate(main);

import subprocess
import frida
import sys

number = 0
flaglen = 41
filename = "./poly_11ea8ad97a9ac01e68a1b5011128fa34"
# TPCTF{wELCoME_T0_thE_W0r1D_0f_poLynoM141}
flag = bytearray(b'!'*41)
jscode = open("h00k.js", "rb").read().decode()
new_number = 0

result = 0
def brute(F):

def on_message(message, data):
global result
if message['type'] == 'send':
result = message['payload']
#print(result)
else:
print(message)
process = subprocess.Popen(filename, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)

session = frida.attach("poly_11ea8ad97a9ac01e68a1b5011128fa34")
script = session.create_script(jscode)
script.on('message', on_message)
script.load()
process.stdin.write(F.decode())
#print(F.decode())
output, error = process.communicate()
#print(output,error)

#print(f"number:{result}")
process.terminate()
return result
import time

count = 0

new_number = brute(flag)
number = new_number
t = time.time()
st = t

while count < flaglen:
number = brute(flag)
#print(flag.decode())
if number > new_number:
print(f"本位耗时:{time.time()-t}s,正确字符为:{chr(flag[count])}")
t = time.time()
print(flag.decode(),result)
new_number = number
count += 1
else:
flag[count] += 1
while(flag[count] > 127):
print("waahahahaha")
flag[count] = 33
count -= 1
flag[count] += 1
print(flag.decode())
print(f"总耗时{time.time()-st}")

运行结果如图(11)

图(11)

看雪ID:Shangwendada

https://bbs.kanxue.com/user-home-979679.htm

*本文为看雪论坛精华文章,由 Shangwendada 原创,转载请注明来自看雪社区

# 往期推荐

1、Windows主机入侵检测与防御内核技术深入解析

2、BFS Ekoparty 2022 Linux Kernel Exploitation Challenge

3、银狐样本分析

4、使用pysqlcipher3操作Windows微信数据库

5、XYCTF两道Unity IL2CPP题的出题思路与题解

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458558816&idx=2&sn=301572776d31e17bdf1f160805921553&chksm=b18d91ea86fa18fce05e9924c236f162a1c4d94b2cc7383a5b5b5e2aa1175e71a8e7bc38bcff&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh