专栏名称: 吾爱破解论坛
吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调而奢华的技术交流与探索却
目录
相关文章推荐
铝途  ·  中铝集团正式接入DeepSeek ·  5 小时前  
中国电建市政集团  ·  复工复产,解锁春日无限潜能 ·  20 小时前  
小李哥的交易系统  ·  大超预期! ·  2 天前  
小李哥的交易系统  ·  大超预期! ·  2 天前  
数据法盟  ·  微信接入DeepSeek后会搜索聊天记录吗? ... ·  2 天前  
鞭牛士  ·  微信已接入DeepSeek-R1模型 ·  3 天前  
鞭牛士  ·  微信已接入DeepSeek-R1模型 ·  3 天前  
51好读  ›  专栏  ›  吾爱破解论坛

2025解题领红包之七 Windows高级Writeup

吾爱破解论坛  · 公众号  · 互联网安全  · 2025-02-15 16:26

正文

作者 坛账号:Command (排行榜第五名同学)

前言

又过了一年, 终于能做出高级题了, 也马上就要中考了...... 嗯......

顺带发现还有一种答案 (同样提示 Correct 但交上去不认可...... 后来证实是题目设计不足) 文中也将写到

(就是 客户端只验证key,服务端验证key和生成key的参数,如果本地验证通过,服务端通不过,请检查key参数 这个提示的来源)

正常解法

找main函数

拖进IDA, main 是这么个东西

 复制代码 隐藏代码
int __fastcall main(int argc, const char **argv, const char **envp)
{
  return ((__int64 (__fastcall *)(int, const char **, const char **))off_140026AD8)(argc, argv, envp);
}

off_140026AD8 如果你直接点进去, 你会发现他指向的是一个虚假的函数 sub_140001390

 复制代码 隐藏代码
mov     eax, 0FFFFFFFFh
retn

此时需要要使用IDA的 Xrefs(X )寻找 off_140026AD8 的引用,


可以发现在 sub_140001190 进行判断后被重新赋值为 sub_1400017A0

 复制代码 隐藏代码
__int64 sub_140001190()
{
  __int64 result; // rax
  __int64 (__fastcall *v1)(); // rcx

  result = sub_140001C50(0LL, 0LL);
  v1 = off_140026AD8;
  if ( !result )
    v1 = sub_1400017A0;
  // ......底部省略一堆+16
  return result;
}

跟进 sub_1400017A0 , 可以发现这才是真正的main函数!

算法分析

先说一下椭圆曲线加密的一些知识

椭圆曲线 y²= x³+ax+b (mod p)

a 和 b 决定了曲线的形状.

p 是素数, 定义了有限域. 所有运算都在该域内进行.

G 是椭圆曲线上的一个特定点, 通常是曲线的"基点". 它是所有其他点的基础, 用于生成公钥, 是一个在曲线上的点, Gx与Gy是已知的.

概括(不想看代码的可以直接看这个):

程序先将 UID 整十秒的时间戳(10s后过期) 进行异或然后平方并转为字符串 (如果字符串长度超过16就截断)

程序中使用了 椭圆曲线加密算法(ECC) , 而 sub_1400013B0 (代码中 point_add )是 点加法运算

然后将用户输入的字符串转为数组 (一次6个字符然后parseInt, 数组长度为108/6=18)

并将 Gx , Gy (定值) 通过 ECC 进行加密, 然后将刚才数组中最后两项作为解密时的 a , p

并对前16项进行 (char) ((IntArray[i] + -10 * Gy - i * 10202) / Gx) 得到字符串并与 UID操作后得到的字符串 进行比较 (i为索引, IntArray为数组)

 复制代码 隐藏代码
__int64 sub_1400017A0()
{
  __int64 a; // r12
  __int64 Gx; // r14
  __int64 Gy; // r15
  int v4; // r13d
  int FlagLength; // ecx
  _BYTE *v6; // rax
  __int64 v7; // rsi
  char *Flag_1; // rdi
  __int64 v9; // rbx
  __int64 v10; // rbx
  __int64 v11; // rdx
  __int64 v12; // rdi
  __int64 v13; // rsi
  __int64 p; // r13
  __int64 v15; // rbx
  __int64 v16; // r8
  __int64 de_p; // r14
  __int64 *v18; // rbx
  __int64 de_a; // r15
  __int64 v20; // r12
  unsigned __int64 x1; // rcx
  __int64 y1; // rax
  __int64 v23; // rbx
  int v24; // eax
  const char *v25; // rcx
  __int64 d; // [rsp+40h] [rbp-C0h]
  __int64 res[2]; // [rsp+48h] [rbp-B8h] BYREF
  __int64 result[2]; // [rsp+58h] [rbp-A8h] BYREF
  char Format[4]; // [rsp+68h] [rbp-98h] BYREF
  char a1[4]; // [rsp+6Ch] [rbp-94h] BYREF
  char v31[4]; // [rsp+70h] [rbp-90h] BYREF
  char v32[4]; // [rsp+74h] [rbp-8Ch] BYREF
  unsigned __int64 UID; // [rsp+78h] [rbp-88h] BYREF
  __int64 IntArray[18]; // [rsp+80h] [rbp-80h] BYREF
  char Str1[32]; // [rsp+110h] [rbp+10h] BYREF
  char Str2[32]; // [rsp+130h] [rbp+30h] BYREF
  _OWORD FlagInput[8]; // [rsp+150h] [rbp+50h] BYREF

  a = ::a;
  Gx = ::Gx;
  Gy = ::Gy;
  d = ::d;
  res[0] = ::p;
  // 上述值在sub_140001000中被sub_140001520初始化; a, d与时间有关
  printf_0_0(::Format);
  printf_0_0(InputYourUID);
  scanf_s_0(::Format, &UID);
  if ( UID - 1 > 0x5F5E0FE )
  {
    printf_0_0(&Error);
    exit(-1);
  }
  UID ^= 60 * (time(0LL) / 60) / 10;
  sprintf_s<32>((char (*)[32])Str2, ::Format, abs64(UID * UID)); // UID与时间取整之后除以10得到的值进行异或然后平方
  Str2[16] = 0;
  memset(FlagInput, 0, sizeof(FlagInput));
  printf_0_0(InputYourKey);
  if ( scanf_s_0(asc_140026AC4, FlagInput, 128LL) )
  {
    v4 = 0;
    FlagLength = 0;
    v6 = FlagInput;
    do
    {
      ++FlagLength;
      ++v6;
    }
    while ( *v6 );
    if ( FlagLength == 108 * (FlagLength / 108) ) // 判断长度是否为108
    {
LABEL_10:
      v7 = 0LL;
      Flag_1 = (char *)FlagInput + 1;
      a1[2] = 0;
      v31[2] = 0;
      v32[2] = 0;
      do
      {
        a1[0] = *(Flag_1 - 1);
        a1[1] = *Flag_1;
        v31[0] = Flag_1[1];
        v31[1] = Flag_1[2];
        v32[0] = Flag_1[3];
        v32[1] = Flag_1[4];
        v9 = 100 * ParseInt(a1);
        v10 = 100 * (v9 + ParseInt(v31));
        Flag_1 += 6;
        IntArray[v7++] = v10 + ParseInt(v32);
      }
      while ( v7 < 18 );                        // 将输入的Key每六个字符一组, 解析数值, 变成一个长度为18的数组(IntArray)
                                                // (ParseInt是10进制, 解析时遇到非数字停止)
      v11 = d;
      v12 = Gx;
      v13 = Gy;
      if ( d == 1 ) // d will never be 1
      {
        v16 = 0LL;
      }
      else
      {
        p = res[0];
        v15 = d - 1;
        result[0] = d - 1;
        do
        {
          point_add(res, v12, v13, Gx, Gy, a, p);// 椭圆曲线加密(ECC), 问deepseek才知道... 加密的是v12, v13 (初始值为Gx, Gy)
          v12 = res[1];
          v13 = res[0];
          --v15;
        }
        while ( v15 );
        v11 = d;
        v4 = 0;
        v16 = result[0];
      }
      de_p = IntArray[17];                      // 此处从数组中下标为16与17的地方读出a与p, 我们需要控制这两个值与上方加密时的a与p相等
      v18 = IntArray;
      de_a = IntArray[16];
      v20 = 0LL;
      res[0] = (__int64)IntArray;
      Str1[0] = 0;
      *(_DWORD *)Format = 25381; // %c
      do
      {
        x1 = v12;                               // 解密后为Gx
        y1 = v13;                               // 解密后为Gy
        if ( v11 != 1 )
        {
          v23 = v16;
          do
          {
            point_add(result, x1, y1, v12, v13, de_a, de_p);// 根据我们传进来的a与p解密刚才加密的Gx, Gy
            x1 = result[1];
            y1 = result[0];
            --v23;
          }
          while ( v23 );
          v18 = (__int64 *)res[0];
        }
        sprintf_s_0_1(&Str1[v4], 2uLL, Format, (unsigned int)(char)((*v18 + -10 * y1 - v20) / x1));// 从数组中开始读数, 向Str1中追加经过处理后的字符
        v11 = d;
        ++v18;
        ++v4;
        res[0] = (__int64)v18;
        v20 += 10202LL;
        v16 = d - 1;
      }
      while ( v20 < 163232 );                   // 共循环16次 (步长10202)
      Str1[16] = 0;
      v24 = strncmp(Str1, Str2, 0x10uLL);       // 比较是否不一致 (注意, 是不一致)
      v25 = Correct;
      if ( v24 )
        v25 = &Error;
      printf_0_0(v25);
      return 0LL;
    }
  }
  // 几个else省略
}

解题

如果要解密被 ECC 加密后的 Gx Gy , 我们需要让 a p 与加密时的相等 (即: IntArray[16] == ::a && IntArray[17] == ::p )

那么我们就得想办法获取到加密时的 a p , 还要得到 Gx Gy 的值, 这里我选择用IDAPython直接Hook

如果要Hook, 选哪里呢? 我们不难注意到开头有这样一段代码

 复制代码 隐藏代码
a = ::a;
Gx = ::Gx;
Gy = ::Gy;
d = ::d;
res[0] = ::p;
 复制代码 隐藏代码
; 这是上面那段代码对应的汇编
mov     r12, cs:a
mov     r14, cs:Gx
mov     r15, cs:Gy
mov     [rsp+200h+d], rax
mov     rax, cs:p
mov     [rsp+200h+res], rax  ; 我选择在这里Hook, 获取寄存器值, 此时rax为p, r12为a, r14为Gx, r15为Gy, 完美!

这里正好有我们所需要的 a, Gx, Gy, p , 还相当好Hook

于是可以得到如下代码

 复制代码 隐藏代码
import idaapi
import idc
import time

# s是最后需要在解密时得到的字符串
def build_str(s: str, Gx: int, Gy: int) -> str:
    res = ''
    for i, v20 in zip(s, range(0, 163232, 10202)): # 步长10202模拟代码中v20 += 10202LL
        res += str(ord(i) * Gx + v20 + 10 * Gy).zfill(6) # 补齐6位
    return res

class MyDbgHook(idaapi.DBG_Hooks):
    def dbg_bpt(self, tid, ea):
        if ea == 0x1400017F8:
            UID = 1354181  # 此处写UID
            UID ^= int(60 * (int(time.time()) // 60 / 10))
            # r14: Gx, r15: Gy, r12: a, rax: p
            print(build_str(str(UID * UID)[:-1] if len(str(UID * UID)) == 17 else str(UID * UID), idc.get_reg_value("r14"), idc.get_reg_value("r15")) + str(idc.get_reg_value("r12")).zfill(6) + str(idc.get_reg_value("rax")).zfill(6))
        return 0

hook = MyDbgHook()
hook.hook()
idc.add_bpt(0x1400017F8)  # 刚才那行汇编的地址

意料之外的解法

先说一下

第一遍做完之后提交了好几次, 都提示不对, 但本地就是 correct ,然后发了个站务帖.

H大后来的回复 (嗯, 不算错但也不对...?)

找到原因了,算题目设计不足,你看下主题中昨天的提醒,主要还是让你找到程序自己的私钥,你改了私钥不行,服务端这边有验证,bin这边倒是没验证。  还在和作者沟通,看看是修复bin的验证还是增加一个说明。


不算错,只是还差一点点,加油哈~

后来服务端就修复了, 应该是加了个提示(?)

我当时的想法

一开始做题我并没有和现在一样对 ECC 有一定的了解, 也不知道要解出那俩参数 (甚至没看提示直接莽)

不过我发现了一点点规律......

同样是那段代码:

 复制代码 隐藏代码
de_p = IntArray[17];                      // 此处从数组中下标为16与17的地方读出a与p
v18 = IntArray;
de_a = IntArray[16];
v20 = 0LL;
res[0] = (__int64)IntArray;
Str1[0] = 0;
*(_DWORD *)Format = 25381; // %c
do
{
  x1 = v12;                               // 解密后甭管是啥, 总之10s内, 输入相同a, p的话这个值不会变 (rcx)
  y1 = v13;                               // 解密后甭管是啥, 总之10s内, 输入相同a, p的话这个值不会变 (rax)
  if ( v11 != 1 )
  {
    v23 = v16;
    do
    {
            point_add(result, x1, y1, v12, v13, de_a, de_p); // 根据我们传进来的a与p解密刚才加密的Gx, Gy; 解出来的对不对? 不关我事
            x1 = result[1];
            y1 = result[0];
            --v23;
    }
    while ( v23 );
    v18 = (__int64 *)res[0];
  }
  // 虽然解出的x1, y1和题中Gx, Gy不一样, 但是在10s内依旧是个定值
  sprintf_s_0_1(&Str1[v4], 2uLL, Format, (unsigned int)(char)((*v18 + -10 * y1 - v20) / x1)); // 从数组中开始读数, 向Str1中追加经过处理后的字符; 我当时选择在这里进行Hook
}

我还是Hook, 但是我选择了在 sprintf_s 处Hook得到通过我输入的 错误的p, a 解密 Gx, Gy 得到的 x1, y1

 复制代码 隐藏代码
imul    rax, -0Ah  ; 我选择hook这一条指令, 获取寄存器值
xor     edx, edx
lea     r8, [rsp+200h+Format] ; Format
sub     rax, r12
add     rax, [rbx]
div     rcx
; ......call在底下

但是由于Hook位置的缘故, 导致每次要执行到那都需先输入一遍UID, 再输入一遍 后12个字符可以不使程序闪退的Key

( 速度要快 )

在当时的想法下, 我写的Python代码  (顺带一提, 它的 parseInt 函数遇到字母会直接停止, 也就是说其实是可以有字母的...):

 复制代码 隐藏代码
import idaapi
import idc
import time

def






请到「今天看啥」查看全文


推荐文章
铝途  ·  中铝集团正式接入DeepSeek
5 小时前
中国电建市政集团  ·  复工复产,解锁春日无限潜能
20 小时前
小李哥的交易系统  ·  大超预期!
2 天前
小李哥的交易系统  ·  大超预期!
2 天前
鞭牛士  ·  微信已接入DeepSeek-R1模型
3 天前
鞭牛士  ·  微信已接入DeepSeek-R1模型
3 天前
韩国me2day  ·  深扒韩国总统朴槿惠整容疑云!
8 年前