flare-on12 5-8 writeup 思路

查看 10|回复 0
作者:失神我醉了   
今年败在了第九题,头发都掉光了,也没能理清check函数的逻辑,去年也是败在第九题,真的有毒。还是太菜了,心痛啊,我近在咫尺的奖牌啊啊啊啊。本文分享一下其他题的思路。
5.ntfsm
首先需要了解:替代数据流 (Alternate Data Streams, ADS)NTFS (New Technology File System) 文件系统的一项独特功能。我一开始不知道这个东西,然后调试时尝试改变参数,没作用,这个程序还老是注销电脑。。后来我怀疑他可能将参数保存在文件,或注册表,使用pexplorer监视才知道这个,然后问了下AI,这个确实适合干坏事。
使用PowerShell访问 ADS
Get-Item -Path .\ntfsm.exe -Stream *
Get-Content -Path .\ntfsm.exe -Stream input -Raw
Get-Content -Path .\ntfsm.exe -Stream input -Encoding Byte -ReadCount 0                #二进制内容
Get-Content .\ntfsm.exe -Stream state
Get-Content .\ntfsm.exe -Stream position
Get-Content .\ntfsm.exe -Stream transitions
删除流
Remove-Item -Path .\ntfsm.exe -Stream *
后面调试发现:
usage: ./ntfsm \nto reset the binary in case of weird behavior: ./ntfsm -r"
通过动态调试,主要靠x64dbg动态调试,修改进程flag使子进程挂起调试,和一点静态分析,弄清了整个程序逻辑,程序通过state和参数字符,来决定走向不同的分支,如果分支走对了,最后通过比较position,transitions是否都与16相同,一样的话打印correct。动态调试的时候发现分支走向是一棵树,但没有弄清state对结果的影响,然后我想着看看这颗树多大,每次都往最大的分支走,结果发现只比对了15个字符,而且transitions也是15,说明什么,说明最后一个字符是独苗。。直接搜索立即数1629C,然后往前推。


1.png (87.82 KB, 下载次数: 2)
下载附件
2025-10-24 12:27 上传

.text:000000014018C9E0 loc_14018C9E0:                          ; CODE XREF: sub_14000C0B0+9AA↑j
.text:000000014018C9E0                 rdtsc                   ; jumptable 000000014000CA5A case 57775
.text:000000014018C9E2                 shl     rdx, 20h
.text:000000014018C9E6                 or      rax, rdx
.text:000000014018C9E9                 mov     [rsp+59398h+var_680], rax
.text:000000014018C9F1
.text:000000014018C9F1 loc_14018C9F1:                          ; CODE XREF: sub_14000C0B0+18096E↓j
.text:000000014018C9F1                 rdtsc
.text:000000014018C9F3                 shl     rdx, 20h
.text:000000014018C9F7                 or      rax, rdx
.text:000000014018C9FA                 mov     [rsp+59398h+var_678], rax
.text:000000014018CA02                 mov     rax, [rsp+59398h+var_680]
.text:000000014018CA0A                 mov     rcx, [rsp+59398h+var_678]
.text:000000014018CA12                 sub     rcx, rax
.text:000000014018CA15                 mov     rax, rcx
.text:000000014018CA18                 cmp     rax, 12AD1659h
.text:000000014018CA1E                 jl      short loc_14018C9F1
.text:000000014018CA20                 movzx   eax, [rsp+59398h+var_59368]
.text:000000014018CA25                 mov     [rsp+59398h+var_4E808], al
.text:000000014018CA2C                 cmp     [rsp+59398h+var_4E808], 51h ; 'Q'
.text:000000014018CA34                 jz      short loc_14018CA38
.text:000000014018CA36                 jmp     short loc_14018CA57
.text:000000014018CA38 ; ---------------------------------------------------------------------------
.text:000000014018CA38
.text:000000014018CA38 loc_14018CA38:                          ; CODE XREF: sub_14000C0B0+180984↑j
.text:000000014018CA38                 mov     [rsp+59398h+var_668], 1629Ch
.text:000000014018CA44                 mov     rax, [rsp+59398h+var_8E0]
# 在 IDA 中运行
import ida_bytes, idaapi, idc
jpt_base = 0x140C687B8    # 跳表起始
image_base = idaapi.get_imagebase()  # 一般是 0x140000000,但用 API 更稳妥
target = loc_14018C9E0 # 你给的 loc 地址
entry = target - image_base
max_entries = 0x1629C     # 或设置更大/更小,按你 cmp 的上限
matches = []
for i in range(0, max_entries+1):
    ea = jpt_base + i*4
    try:
        v = ida_bytes.get_dword(ea)
    except:
        continue
    if (v & 0xFFFFFFFF) == (entry & 0xFFFFFFFF):
        matches.append((i, ea, v))
if not matches:
    print("没有找到匹配项(请确认 jpt_base 和 max_entries 是否正确)")
else:
    for idx, ea, v in matches:
        print("index=0x{0:X} ({0}), entry_addr=0x{1:X}, entry=0x{2:X}".format(idx, ea, v))
.\ntfsm.exe iqg0nSeCHnOMPm2Q
correct!
Your reward: [email protected]


6.Chain of Demands
这个题,我学会了EVM字节码反编译分析。。通过反编译LCGracle的字节码,发现当counter=0时,state = initial_seed,然后就恢复了整个LCG的参数,可以解密LCG-XOR消息,最后发现这对我解密RSA消息好像没有很大作用。。。可能哪里没搞明白或者搞错了。这个题目考察的是RSA N的多因子分解问题,initial_seed是他其中的一个因子,去factordb查了一下,得到其他7个因子。。。
"""
使用已知的 8 个质数解密 RSA 消息
"""
from Crypto.Util.number import inverse, long_to_bytes
from math import gcd
# 8 个质数
primes = [
    72967016216206426977511399018380411256993151454761051136963936354667101207529,
    62826068095404038148338678434404643116583820572865189787368764098892510936793,
    68446593057460676025047989394445774862028837156496043637575024036696645401289,
    69802783227378026511719332106789335301376047817734407431543841272855455052067,
    75395288067150543091997907493708187002382230701390674177789205231462589994993,
    79611551309049018061300429096903741339200167241148430095608259960783012192237,
    82836473202091099900869551647600727408082364801577205107017971703263472445197,
    88790251731800173019114073860734130032527125661685690883849562991870715928701,
]
# RSA 密文
ciphertexts = [
    "680a65364a498aa87cf17c934ab308b2aee0014aee5b0b7d289b5108677c7ad1eb3bcfbcad7582f87cb3f242391bea7e70e8c01f3ad53ac69488713daea76bb3a524bd2a4bbbc2cfb487477e9d91783f103bd6729b15a4ae99cb93f0db22a467ce12f8d56acaef5d1652c54f495db7bc88aa423bc1c2b60a6ecaede2f4273f6dce265f6c664ec583d7bd75d2fb849d77fa11d05de891b5a706eb103b7dbdb4e5a4a2e72445b61b83fd931cae34e5eaab931037db72ba14e41a70de94472e949ca3cf2135c2ccef0e9b6fa7dd3aaf29a946d165f6ca452466168c32c43c91f159928efb3624e56430b14a0728c52f2668ab26f837120d7af36baf48192ceb3002",
    "6f70034472ce115fc82a08560bd22f0e7f373e6ef27bca6e4c8f67fedf4031be23bf50311b4720fe74836b352b34c42db46341cac60298f2fa768f775a9c3da0c6705e0ce11d19b3cbdcf51309c22744e96a19576a8de0e1195f2dab21a3f1b0ef5086afcffa2e086e7738e5032cb5503df39e4bf4bdf620af7aa0f752dac942be50e7fec9a82b63f5c8faf07306e2a2e605bb93df09951c8ad46e5a2572e333484cae16be41929523c83c0d4ca317ef72ea9cde1d5630ebf6c244803d2dc1da0a1eefaafa82339bf0e6cf4bf41b1a2a90f7b2e25313a021eafa6234643acb9d5c9c22674d7bc793f1822743b48227a814a7a6604694296f33c2c59e743f4106",
]
print("=" * 80)
print(" FlareOn 12 - 最终解密")
print("=" * 80)
print("\n步骤 1: 验证质数")
print("-" * 80)
from Crypto.Util.number import isPrime
all_prime = True
for i, p in enumerate(primes):
    is_p = isPrime(p)
    status = "✓" if is_p else "✗"
    print(f"  质数 #{i+1}: {status} ({p.bit_length()} bits)")
    if not is_p:
        all_prime = False
if not all_prime:
    print("\n⚠️ 警告: 不是所有数字都是质数!")
print("\n步骤 2: 构建 RSA 密钥")
print("-" * 80)
# 计算 n = p1 * p2 * ... * p8
print("\n计算 n = p1 × p2 × ... × p8 ...")
n = 1
for p in primes:
    n *= p
print(f"  n = {str(n)[:60]}...")
print(f"  n 的位数: {n.bit_length()} bits")
# 计算 phi(n) = (p1-1) * (p2-1) * ... * (p8-1)
print("\n计算 φ(n) = (p1-1) × (p2-1) × ... × (p8-1) ...")
phi = 1
for p in primes:
    phi *= (p - 1)
print(f"  φ(n) 的位数: {phi.bit_length()} bits")
# 公钥指数
e = 65537
print(f"\n公钥指数 e = {e}")
# 检查 gcd
g = gcd(e, phi)
print(f"gcd(e, φ) = {g}")
if g != 1:
    print("❌ 错误: e 和 φ(n) 不互质!")
    exit(1)
# 计算私钥
print("\n计算私钥 d = e^(-1) mod φ(n) ...")
d = inverse(e, phi)
print(f"  d 的位数: {d.bit_length()} bits")
print("  ✓ 私钥计算成功!")
print("\n步骤 3: 解密 RSA 消息")
print("-" * 80)
for idx, ct_hex in enumerate(ciphertexts):
    print(f"\n{'='*60}")
    print(f"消息 #{idx + 1}")
    print(f"{'='*60}")
    # 密文转为整数 (little-endian)
    ct_bytes = bytes.fromhex(ct_hex)
    ct_int = int.from_bytes(ct_bytes, 'little')
    print(f"密文长度: {len(ct_bytes)} bytes")
    print(f"密文 (前50字符): {ct_hex[:50]}...")
    # RSA 解密: m = c^d mod n
    print("\n执行解密: m = c^d mod n ...")
    pt_int = pow(ct_int, d, n)
    print(f"明文整数: {str(pt_int)[:50]}...")
    # 转换为字节
    try:
        pt_bytes = long_to_bytes(pt_int)
        print(f"明文字节长度: {len(pt_bytes)} bytes")
        # 尝试解码为 UTF-8
        try:
            pt_str = pt_bytes.decode('utf-8')
            print(f"\n✅ 解密成功!")
            print(f" 明文: {pt_str}")
            # 如果包含 flag 格式
            if "flag" in pt_str.lower() or "@" in pt_str or "flare" in pt_str.lower():
                print(f"\n FLAG: {pt_str} ")
        except UnicodeDecodeError:
            print(f"\n⚠️ 无法解码为 UTF-8")
            print(f"原始十六进制: {pt_bytes.hex()}")
            # 尝试 ASCII
            try:
                pt_ascii = pt_bytes.decode('ascii', errors='ignore')
                if pt_ascii.strip():
                    print(f"ASCII (忽略错误): {pt_ascii}")
            except:
                pass
    except Exception as e:
        print(f"\n❌ 解密出错: {e}")
print("\n" + "=" * 80)
print("完成!")
print("=" * 80)


7.The Boss Needs Help
这个题存在大量算术逻辑混淆,MBA没有作用,所以我没有去混淆。期待大佬们的去混淆方案,学习一波。我直接在汇编窗口下断call指令,关注函数的输入和输出,有意思的函数就静态分析。这个题难在stage1 恢复username@computername。其实逻辑很简单,感觉这个值可能被加密过,因为这是用户数据,c2服务器加密数据也应该要用到,我得到这个值全靠猜。。
mov     rax, [rsi]      #获取服务器响应字符串
movzx  ecx, byte ptr [rax+rbx]      #将字符串第rbx位赋值给 ecx
mov     rax, cs:Block       #获取block表的地址
movzx   r10d, byte ptr [rcx+rax]    #根据字符串的值,获取block表对应地址的值
sub     al, bl          #al=0xff-bl
add     r10b, al   
mov     rax, rbx        #rbx 最大150
div     rdi     #rdi   username@computername 长度     
mov     rax, qword ptr [rbp+170h+var_50]  #username@computername
xor     r10b, [rdx+rax]
inc     rbx
call    sub_7FF63E11EE00  #保存r10
mov     rax, [rsi+8]
sub     rax, [rsi]
cmp     rbx, rax        rax=0x97=151
这里通过服务器返回的16进制数据查表,以username@computername作为key,恢复加密数据。往后面分析,发现恢复的数据是json格式,跳过parse_json的函数后,发现提取key为"ack"的数据,所以一开始我使用{"ack": "这9个字符去尝试恢复username@computername,因为computername的长度不允许超过15个,并且限制为字符,数字和连字符,还有个限制条件是json数据最后的两个字符一定是"},所以满足条件的username@computername至少11个字符,只有长度为14  17  19  20  22  23满足条件,150mod他们要>=10 ,通过测试,发现长度为19时,不可打印字符最少,并且后面的数据解析到了, ",所以意识到ack所在位置不对,然后分析流量包,发现线索:{"sta": "received"},Host: theannualtraditionofstaringatdisassemblyforweeks.torealizetheflagwasjustxoredwiththefilenamethewholetime.com:8080,一切都通了,怪不得像,sta长度一样的,出来混全靠蒙。。。将ack换为sta,解密得到TheBoss@THUNDERNODE,恢复数据:{"sta": "excellent", "ack": "peanut@theannualtraditionofstaringatdisassemblyforweeks.torealizetheflagwasjustxoredwiththefilenamethewholetime.com:8080"}。
stage1能出来,后面解密流量包就很简单了,就是标准AES解密,动态调试发现key的生成,与前面可解密c2服务器的指令数据有关,IV没变化
import hashlib, binascii
A = hashlib.sha256(b"TheBoss@THUNDERNODE").digest()
#print(binascii.hexlify(A))
B0 = hashlib.sha256(b"peanut06").digest()
#'06'与时间有关,直接爆破
B1 = hashlib.sha256(b"TheBoss@THUNDERNODE06").digest()
B2 = hashlib.sha256(b"miami06").digest()
IV = b'000102030405060708090A0B0C0D0E0F'
#print(binascii.hexlify(B))
xor0 = bytes(x^y for x,y in zip(A,B0))
xor1 = bytes(x^y for x,y in zip(A,B1))
xor2 = bytes(x^y for x,y in zip(A,B2))
#print(xor)
key0 = binascii.hexlify(xor0)[0:64]
key1 = binascii.hexlify(xor1)[0:64]
key2 = binascii.hexlify(xor2)[0:64]
print(key0,key1,key2)
{"sta": "excellent", "ack": "peanut@theannualtraditionofstaringatdisassemblyforweeks.torealizetheflagwasjustxoredwiththefilenamethewholetime.com:8080"}
{"msg": "cmd", "d": {"cid": 6, "dt": 20, "np": "TheBoss@THUNDERNODE"}}
{"msg": "cmd", "d": {"cid": 6, "dt": 25, "np": "miami"}}


8.FlareAuthenticator
Qt程序,一样存在混淆,对去混淆没啥想法,比较懒,主要是菜。通过静态分析,发现三个槽函数,输入,删除,验证。那么没有去混淆的情况下,怎么快速定位主要逻辑呢,我的方法是使用x64dbg trace 验证函数到弹窗wrong passwd的过程,通过不同输入,比对trace的不同,发现每个trace文件中有一块内存各不相同,其余的变化都在寄存器,这块内存肯定与输入数据相关。


2.png (180.22 KB, 下载次数: 1)
下载附件
2025-10-25 01:44 上传

通过对这块内存下硬件访问断点,可以在验证槽函数里找到最后数据的比对地方
mov     rax, [rax+78h]      #输入相关数据
mov     rcx, 0BC42D5779FEC401h      #目标值
sub     rax, rcx
setz    al
既然验证槽函数没有输入数据相关信息,说明数据的处理在输入槽函数,继续对那个内存点下硬件访问,写入断点,可以在输入槽函数还原整个逻辑。
mov     rcx, [rbp+0FC0h+var_948]
mov     rdx, [rbp+0FC0h+var_BC0]  0x100,0x200,0x300....
mov     al, [rbp+0FC0h+var_C01]
movsx   r9d, al  #输入0-9的值
mov     eax, r9d
not     eax
mov     r11d, edx
not     r11d
or      r11d, eax
mov     eax, edx
add     eax, r9d
mov     r10d, eax
mov     r8d, r11d
lea     r8d, [r8+r10+1]
or      edx, r9d
sub     eax, edx
mov     edx, eax
or      edx, r8d
and     eax, r8d
add     eax, edx
mov     dx, ax
mov     rax, cs:off_7FF676A6FE40
mov     r8, 64ED705730BC6591h
add     rax, r8
call    rax         #
mov     rcx, rax
mov     rax, [rbp+0FC0h+var_C00] #每个框值固定,不同框值不同
imul    rax, rcx
mov     [rbp+0FC0h+var_C78], rax
mov     rax, [rbp+0FC0h+var_948]
mov     r9, [rbp+0FC0h+var_C78]  
mov     rdx, [rax+78h]   #前一次计算的值
mov     rcx, r9
not     rcx
mov     r8, rdx
not     r8
or      r8, rcx
mov     rcx, rdx
add     rcx, r9
lea     r8, [r8+rcx+1]
or      rdx, r9
sub     rcx, rdx
mov     rdx, rcx
or      rdx, r8
and     rcx, r8
add     rcx, rdx
mov     [rax+78h], rcx  #最终结果值
虽然逻辑还原了,但是我是蒙的,一个25个框,每一个框0-9对应不同的数据,进行多次累加操作与最后的目标值比对,我是不知道怎么解决这个问题,所以我把他丢给了chatgpt。他告诉我,本质上在求解一个离散的模加法方程组
$$
\sum_{i=0}^{N-1} table[x_i] \bmod 2^{64} = TARGET
$$
chatgpt告诉我只需要r9的值就够了,call rax 的逻辑不重要。chatgpt真强
#!/usr/bin/env python3
# solve_auth_by_inverse.py
# 纯 Python:验证汇编等价性并用逆推回溯求密码(0..9 per position)
# 运行: python3 solve_auth_by_inverse.py
from functools import lru_cache
import time, random
MASK = (1 TIMEOUT:
        return False
    nodes += 1
    # pruning by remaining min/max
    if current_sum + suf_min[idx] > target_T:
        return False
    if current_sum + suf_max[idx]  target_T:
            continue
        if new_sum + suf_max[idx+1]
这次比赛尝试了一些新思路,get了一些想法,还是收获良多的。遗憾的是最后一题没解出来,,静态分析功力不够,理不清check函数的逻辑,更别说1万个了。。。最后对完成的大神表示崇高的敬意,特别是前面的,大佬带带我!!神的世界我依然理解不了!!!


3.png (114.97 KB, 下载次数: 2)
下载附件
2025-10-25 01:44 上传

数据, 函数

您需要登录后才可以回帖 登录 | 立即注册

返回顶部