期末考试前一天和学长一起做的,差一点出了,全部逻辑已经弄清楚,就是没想到动态调试下的加密密钥是假的,导致最后没有解出flag。而且这比赛晋级线下赛名额好像很多,甚至出了这一题感觉都能晋级,气死了,真是蠢死了😅。
复现是因为这题有值得学习的地方,并且以这题为例说说 intel Pin 在CTF当中的简单应用,这个才是我要复现的重点。
无题目描述
运行一下可执行程序
但是这里的搜索关键字符串找不到主要逻辑,所有符合都被删除了,这也是这题的恶心之处,那就只能从start函数开始动态调试了。
在sub_4B5470函数这个位置发现开始输出 plz input your flag
点进去 off_5BA6D0 发现是一个函数表
这里会按顺序执行,这里发现函数表当中的sub_4053A5执行完之后会打印plz input your flag,说明函数里有关键逻辑,但是我们反编译只能得到
__int64 sub_4053A5()
{
return -16LL;
}
这里转到汇编发现是有一大片汇编指令,但是反编译却没有伪代码,因为这一题的全部关键函数都是这种情况,当时我卡在这里了,根本没有想到是花指令的影响,后续学长提醒了我才知道,看来能骗过IDA的花指令有很多,得多见识才行
如图,都是花指令得实现,是多余得指令,这里可以直接nop掉左边分支的retn指令即可成功反编译,后续的函数都可以这样处理。
sub_4053A5函数代码如下
unsigned __int64 __fastcall sub_4053A5(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5, int a6)
{
unsigned __int64 result; // rax
int i; // [rsp+8h] [rbp-28h]
int j; // [rsp+Ch] [rbp-24h]
__int64 v9; // [rsp+10h] [rbp-20h]
_QWORD v10[2]; // [rsp+18h] [rbp-18h]
unsigned __int64 v11; // [rsp+28h] [rbp-8h]
v11 = __readfsqword(0x28u);
if ( sub_529980(0, 0, 0, 0, a5, a6, 0) )
{
for ( i = 0; i <= 13; ++i )
key[i] ^= 0x45u;
}
v9 = 6220400490607890259LL;
v10[0] = 0x450351564C5A0357LL;
*(_DWORD *)((char *)v10 + 7) = 0x44424F45;
for ( j = 0; j <= 18; ++j )
sub_4E4060((unsigned int)(char)(*((_BYTE *)&v10[-1] + j) ^ 0x23));
sub_4E4060(0xALL);
result = v11 - __readfsqword(0x28u);
if ( result )
sub_52BE90(0xALL);
return result;
}
sub_4E4060函数就是输出plz input your flag的函数,这里的key是后续的加密密钥
data=[ 0x53, 0x4F, 0x59, 0x03, 0x4A, 0x4D, 0x53, 0x56, 0x57, 0x03,
0x5A, 0x4C, 0x56, 0x51, 0x03, 0x45, 0x4F, 0x42, 0x44]
for i in range(len(data)):
data[i]^=0x23
data=''.join([chr(x) for x in data])
print(data)
#plz input your flag
当时做题可能有点神志不清,觉得因为删除符号表,主逻辑要找到比较麻烦,但是现在认真复现才发现在start函数当中
这个sub_405559函数其实就是main函数,而这个sub_4B5470是__libc_start_main函数
一个带符号的程序通常start函数是这样的
看来我真是昏头了当时,所以main函数其实就在这,然后出题人在main函数之前塞了一些其他逻辑
sub_405559函数代码如下
__int64 __fastcall sub_405559(__int64 a1, __int64 a2, int a3, int a4, int a5, int a6)
{
__int64 result; // rax
__int64 v7[33]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v8; // [rsp+108h] [rbp-8h]
v8 = __readfsqword(0x28u);
sub_4D9660(&aS_13, &input, a3, a4, a5, a6, 0);
//要求输入
memset(v7, 0, 256);
//给S盒分配空间
(*(*qword_5C6D28 + 16LL))(qword_5C6D28, v7, &key, 14LL);
(*(*qword_5C6D28 + 24LL))(qword_5C6D28, v7, &input, 50LL);
//两次函数调用
result = 0LL;
if ( v8 != __readfsqword(0x28u) )
sub_52BE90();
return result;
}
两个函数的代码如下
//第一个函数
unsigned __int64 __fastcall sub_405848(__int64 a1, __int64 a2, __int64 a3, unsigned __int64 a4)
{
unsigned __int64 result; // rax
char v5; // [rsp+27h] [rbp-119h]
unsigned int i; // [rsp+28h] [rbp-118h]
unsigned int j; // [rsp+28h] [rbp-118h]
unsigned int v8; // [rsp+2Ch] [rbp-114h]
__int64 v9[33]; // [rsp+30h] [rbp-110h] BYREF
unsigned __int64 v10; // [rsp+138h] [rbp-8h]
v10 = __readfsqword(0x28u);
LOBYTE(v8) = 0;
memset(v9, 0, 256);
for ( i = 0; i <= 0xFF; ++i )
{
*(i + a2) = i;
*(v9 + i) = *(i % a4 + a3);
}
for ( j = 0; j <= 0xFF; ++j )
{
v8 = (v8 + *(j + a2) + *(v9 + j));
v5 = *(j + a2);
*(j + a2) = *(v8 + a2);
*(a2 + v8) = v5;
}
result = v10 - __readfsqword(0x28u);
if ( result )
sub_52BE90(a1);
return result;
}
//第二个函数
unsigned __int64 __fastcall sub_405EAA(__int64 a1, __int64 a2, __int64 a3, unsigned __int64 a4)
{
unsigned __int64 result; // rax
char v5; // [rsp+2Bh] [rbp-15h]
unsigned int v6; // [rsp+2Ch] [rbp-14h]
unsigned int v7; // [rsp+30h] [rbp-10h]
unsigned __int64 i; // [rsp+38h] [rbp-8h]
LOBYTE(v6) = 0;
LOBYTE(v7) = 0;
for ( i = 0LL; ; ++i )
{
result = i;
if ( i >= a4 )
break;
v6 = (v6 + 1);
v7 = (*(v6 + a2) + v7);
v5 = *(v6 + a2);
*(v6 + a2) = *(v7 + a2);
*(a2 + v7) = v5;
*(a3 + i) ^= *((*(v6 + a2) + *(v7 + a2)) + a2) ^ 0x23;
//此处异或了一个0x23
}
return result;
}
所以就是一个RC4加密算法,最后多异或了一个0x23,但是和flag的比对逻辑不在主函数里面,我们直接在处理后的input内存处下一个硬件断点,跑一下就能找到比对的地方,代码如下
__int64 __fastcall sub_405CAA(__int64 a1, __int64 a2)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-44h]
int j; // [rsp+24h] [rbp-3Ch]
int v5; // [rsp+2Bh] [rbp-35h]
char v6; // [rsp+2Fh] [rbp-31h]
__int64 v7[5]; // [rsp+30h] [rbp-30h]
unsigned __int64 v8; // [rsp+58h] [rbp-8h]
v8 = __readfsqword(0x28u);
v7[0] = 0xD3581C51AF54CD25LL;
v7[1] = 0xD45D83EC564F4BA8LL;
v7[2] = 0xA5B073E06F4A47F6LL;
v7[3] = 0xF6F42B5E8117C3A8LL;
v7[4] = 0x579963A8FF2FEA71LL;
for ( i = 0; i <= 39; ++i )
{
if ( *(i + a2) != *(v7 + i) )
return 1LL;
}
v5 = -504438533;
v6 = -3;
for ( j = 0; j <= 4; ++j )
{
a1 = *(&v5 + j) ^ 0x89u;
//right字符串处理
sub_4E4060(a1);
}
result = v8 - __readfsqword(0x28u);
if ( result )
sub_52BE90(a1);
return result;
}
现在有密文,有key,就可以解密,但是按照程序的流程写逆向脚本,却得不到正确的flag,脚本里面不对密钥异或一个0x45之后就可以得到正确的脚本了,脚本如下
def rc4(key, data):
"""RC4 加密/解密函数"""
S = list(range(256))
j = 0
output = bytearray()
# KSA (Key Scheduling Algorithm)
key_length = len(key)
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i], S[j] = S[j], S[i]
# PRGA (Pseudo-Random Generation Algorithm)
i = j = 0
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
output.append(byte ^ S[(S[i] + S[j]) % 256]^0x23)
return bytes(output)
def main():
key = [0x92, 0x1C, 0x2B, 0x1F, 0xBA, 0xFB, 0xA2, 0xFF, 0x07, 0x69,
0x7D, 0x77, 0x18, 0x8C]
key = bytearray(key)
encrypted_data = bytes([ 0x25, 0xCD, 0x54, 0xAF, 0x51, 0x1C, 0x58, 0xD3, 0xA8, 0x4B,
0x4F, 0x56, 0xEC, 0x83, 0x5D, 0xD4, 0xF6, 0x47, 0x4A, 0x6F,
0xE0, 0x73, 0xB0, 0xA5, 0xA8, 0xC3, 0x17, 0x81, 0x5E, 0x2B,
0xF4, 0xF6, 0x71, 0xEA, 0x2F, 0xFF, 0xA8, 0x63, 0x99, 0x57])
decrypted_data = rc4(key, encrypted_data)
print("解密后的数据:", decrypted_data)
#解密后的数据: b'dart{y0UD0ntL4cKg0oD3y34T0F1nDTh3B4aUtY}'
if __name__ == "__main__":
main()
为啥会出现这种情况,我们可以回到刚刚的地方看
这里的sub_529980应该是一个反调试函数,检测到调试状态就会对密钥进行异或处理,所以调试状态下获得的密钥是一个假密钥,才导致没有解出。
反思没有解出来的最主要原因是当时没弄清楚程序的执行流,main函数就在眼前,但是去除符号表就没反应过来,只是盲目的动态调试,逻辑很乱。这题在main函数执行前对key异或,在main函数执行之后进行flag的比对,将代码逻辑分散并去除符号表从而加大了逆向难度,虽然也挺简单的,但是头脑得清晰。
所以flag是dart{y0UD0ntL4cKg0oD3y34T0F1nDTh3B4aUtY}
接下来说说怎么利用 intel Pin 去爆破flag,首先我们注意到,在最后对输入进行check的时候,是在for循环内部,每一位进行比较的,代码如下
for ( i = 0; i <= 39; ++i )
{
if ( *(i + a2) != *(v7 + i) )
return 1LL;
}
并且加密算法为RC4,也就是说flag中的每一位在加密的时候都是互相独立的,一位的改变不会影响flag中其他位的加密过程,这在RC4中体现不会改变最后的密钥流。
以上是我们进行爆破的前提条件,而当flag是每一位每一位的进行check的时候,也就是说你的输入加密后的结果每对一位,执行的汇编指令就会变多,而通过 intel Pin 插桩,我们可以得到每一次程序执行的指令数。对于每一个位置,显然正确的flag字符在该位置上时的程序执行指令数会和Table中其他字符的程序执行指令数不同,而Table中错误字符的程序执行指令数都会相同。
爆破每一位的正确输入的时候,对于Table中的每一个字符,我们都与 ‘(‘ 字符的程序执行指令数进行比较,因为flag中通常不会有 ‘(‘ 字符。
对于Table的处理,为了提高爆破的速率,首先将所有UUID可能包括的字符放前面,有些CTF中flag都为UUID的形式,后续则为在flag中出现频率比较高的字符。
from pwn import *
# 修正1:将不可变的bytes转换为可变的bytearray
sendstring = bytearray(b'a' * 40) # 使用bytearray支持修改
Table="0123456789abcdef{}-glyrsthijkmnopquvwABCDEFGHIJKLMNOPQRSTUVWxzXYZ!@"
command = [
"./pin",
"-t", "/home/daki/pin-3.23-98579-gb15ab7903-gcc-linux/source/tools/MyPinTool/obj-intel64/MyPinTool.so",
"--",
"/home/daki/Desktop/chall"
]
# 修正2:封装进程操作为函数
def run_process(input_data):
with process(command) as p: # 使用with自动管理进程
p.sendline(input_data)
output = p.recvall(timeout=2).decode()
# 添加错误处理防止split失败
if "Count: " not in output:
return None
return output.split("Count: ")[-1].strip()
# 主循环逻辑
for i in range(40):
original_char = sendstring[i]
# 测试原始字符的计数值
sendstring[i] = ord('(') # bytearray需要赋值整数字节值
compare_output = run_process(sendstring)
if not compare_output:
print(f"Error: No 'Count' found at position {i}")
continue
# 修正3:遍历Table时避免修改外层循环的i
found = False
for x in Table.encode(): # 直接遍历字节数值
sendstring[i] = x
current_output = run_process(sendstring)
if current_output != compare_output:
print(f"Found mismatch at pos {i}: char {chr(x)}")
found = True
break # 找到正确字符后跳出内层循环
# 如果未找到匹配字符则恢复原值
if not found:
sendstring[i] = original_char
print(f"Warning: No valid char found at position {i}")
print(f"Final flag: {sendstring.decode()}")
MyPinTool.cpp的代码就直接用自带的 /source/tools/Tests 的icout1.cpp
代码如下
/*
* Copyright (C) 2004-2021 Intel Corporation.
* SPDX-License-Identifier: MIT
*/
#include <stdio.h>
#include "pin.H"
#include <iostream>
using std::endl;
UINT64 icount = 0;
VOID docount() { icount++; }
VOID Instruction(INS ins, VOID* v) { INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END); }
VOID Fini(INT32 code, VOID* v) { std::cerr << "Count: " << icount << endl; }
int main(int argc, char* argv[])
{
PIN_Init(argc, argv);
INS_AddInstrumentFunction(Instruction, 0);
PIN_AddFiniFunction(Fini, 0);
// Never returns
PIN_StartProgram();
return 0;
}