随笔 - 8  文章 - 0  评论 - 1  阅读 - 1104

BUUCTF Reverse题解:第二部分(已完结)

Welcome again, to C12AK's Re journal !



题目传送门


前言

就是不想上课……Re比上课有意思多了qwq
既然开学了,那就偶尔闲下来的时候写一篇wp吧,时间少的话就不写了。


1. [GWCTF 2019]pyre

下载的文件是.pyc格式,它是.py经编译后的字节码文件,可以使用在线工具进行反编译。我们把文件放入,得到如下代码:

#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 2.7

print 'Welcome to Re World!'
print 'Your input1 is your flag~'
l = len(input1)
for i in range(l):
    num = ((input1[i] + i) % 128 + 128) % 128
    code += num

for i in range(l - 1):
    code[i] = code[i] ^ code[i + 1]

print code
code = [
    '%1f',
    '%12',
    '%1d',
    '(',
    '0',
    '4',
    '%01',
    '%06',
    '%14',
    '4',
    ',',
    '%1b',
    'U',
    '?',
    'o',
    '6',
    '*',
    ':',
    '%01',
    'D',
    ';',
    '%',
    '%13']

此处“%”表示十六进制数。代码逻辑比较简单,就是输入正确的flag,经过两次操作就能变成code字符串。可以直接编写脚本,由code逆向求出flag:

#include<bits/stdc++.h>
using namespace std;

int main(){
    char c[] = {
    '\x1f',
    '\x12',
    '\x1d',
    '(',
    '0',
    '4',
    '\x01',
    '\x06',
    '\x14',
    '4',
    ',',
    '\x1b',
    'U',
    '?',
    'o',
    '6',
    '*',
    ':',
    '\x01',
    'D',
    ';',
    '%',
    '\x13'};
    for(int i = 22; i >= 0; i--) c[i] ^= c[i + 1];
    for(int i = 0; i < 23; i++) c[i] = ((c[i] - i) % 128 + 128) % 128;
    for(int i = 0; i < 23; i++) cout << c[i];
    return 0;
}

回到顶部


2. rsa

不看别人的题解完全做不出来……明明是crypto好吧/ll

看到这个题的第一反应:什么是rsa?
然后下载压缩包,解压得到有两个文件,从文件名看上去,一个是加密后的flag,另一个是公钥。用IDA对公钥文件逆向,可以看到:

seg000:0000000000000000 ;
seg000:0000000000000000 ; +-------------------------------------------------------------------------+
seg000:0000000000000000 ; |      This file was generated by The Interactive Disassembler (IDA)      |
seg000:0000000000000000 ; |           Copyright (c) 2023 Hex-Rays, <support@hex-rays.com>           |
seg000:0000000000000000 ; +-------------------------------------------------------------------------+
seg000:0000000000000000 ;
seg000:0000000000000000 ; Input SHA256 : 1A6AE01E99B0EB25A35A92109AA927421BAE2C205D469DD7C7BAA400AE08CA06
seg000:0000000000000000 ; Input MD5    : 23A052C3E8FF2E31094775AED2376C27
seg000:0000000000000000 ; Input CRC32  : 184C9631
seg000:0000000000000000
seg000:0000000000000000 ; File Name   : C:\Users\c12h4\Desktop\41c4e672-98c5-43e5-adf4-49d75db307e4\output\pub.key
seg000:0000000000000000 ; Format      : Binary file
seg000:0000000000000000 ; Base Address: 0000h Range: 0000h - 008Ah Loaded length: 008Ah
seg000:0000000000000000
seg000:0000000000000000                 .686p
seg000:0000000000000000                 .mmx
seg000:0000000000000000                 .model flat
seg000:0000000000000000
seg000:0000000000000000 ; ===========================================================================
seg000:0000000000000000
seg000:0000000000000000 ; Segment type: Pure code
seg000:0000000000000000 seg000          segment byte public 'CODE' use64
seg000:0000000000000000                 assume cs:seg000
seg000:0000000000000000                 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
seg000:0000000000000000                 db  2Dh ; -
seg000:0000000000000001                 db  2Dh ; -
seg000:0000000000000002                 db  2Dh ; -
seg000:0000000000000003                 db  2Dh ; -
seg000:0000000000000004                 db  2Dh ; -
seg000:0000000000000005                 db 42h, 45h, 47h
seg000:0000000000000008                 dq 494C425550204E49h, 2D2D2D59454B2043h, 447777444D0A2D2Dh
seg000:0000000000000020                 dq 68495A6F4B4A5951h, 42424551414E6376h, 4B7741774B444151h
seg000:0000000000000038                 dq 4C7A414D41684941h, 4C59636B726B7846h, 4D43313268637732h
seg000:0000000000000050                 dq 3959704656516B32h, 4B76412F0A2B372Bh, 647A63517A723172h
seg000:0000000000000068                 dq 3D454141424D6741h, 4E452D2D2D2D2D0Ah, 43494C4255502044h
seg000:0000000000000080                 dq 2D2D2D2D59454B20h
seg000:0000000000000088                 db  2Dh ; -
seg000:0000000000000089                 db  0Ah
seg000:0000000000000089 seg000          ends
seg000:0000000000000089
seg000:0000000000000089
seg000:0000000000000089                 end

中间一大段16进制码应该就是公钥了。将其选中,按A转为字符串形式,再将得到的字符串 解析公钥 得到指数e和模数n:

接着编写脚本将n转换为十进制:

nhx = 'C0332C5C64AE47182F6C1C876D42336910545A58F7EEFEFC0BCAAF5AF341CCDD'
n = int(nhx, 16)
print(n)

由于 n=p*q,t=(p-1)*(q-1) ,那么先 通过n解析p和q

图中前一项是p,后一项是q。
此时我们已经得到了t(由p和q求得)和n,就可以得到d,进而组合出私钥,然后解码得到flag。脚本如下:

import gmpy2
import rsa
e = 65537
n = 86934482296048119190666062003494800588905656017203025617216654058378322103517
p = 285960468890451637935629440372639283459
q = 304008741604601924494328155975272418463

t = (p - 1) * (q - 1)
d = gmpy2.invert(e, t)
key = rsa.PrivateKey(n, e, int(d), p, q)
f = open("C:\\Users\\c12h4\\Desktop\\41c4e672-98c5-43e5-adf4-49d75db307e4\\output\\flag.enc", "rb+")
fr = f.read()
print(rsa.decrypt(fr, key))

[补充]:rsa解密脚本的一般写法([SUCTF2019]SignIn)

这段代码和上面的区别是,上面是从文件读入的密文,而下面的代码中的密文(变量c)是直接赋值的。上面用到的 rsa.decrypt(fr, key) 这个函数,要求fr是bytes类型,这对于python小白来说十分难办,所以推荐采用下面的写法。

import gmpy2
import rsa
import binascii

e = 65537
n = 103461035900816914121390101299049044413950405173712170434161686539878160984549
c = 0xad939ff59f6e70bcbfad406f2494993757eee98b91bc244184a377520d06fc35
p = 282164587459512124844245113950593348271
q = 366669102002966856876605669837014229419

t = (p - 1) * (q - 1)
d = gmpy2.invert(e, t)
key = rsa.PrivateKey(n, e, int(d), p, q)
flag = gmpy2.powmod(c, d, n)
print(binascii.unhexlify(hex(flag)[2:]))

关于最后一行中的“[2:]”: binascii.unhexlify() 是将参数看作一个不带“0x”的十六进制数字字符串,每两位数转化出一个ascii字符;而 hex() 转化出的结果是带有“0x”的十六进制数,所以必须把“0x”去掉,即从下标为2的位置开始。

回到顶部


3. CrackRTF

首先查看与flag直接相关的位置,此题即main_0函数:

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  DWORD v3; // eax
  DWORD v4; // eax
  char Str[260]; // [esp+4Ch] [ebp-310h] BYREF
  int v7; // [esp+150h] [ebp-20Ch]
  char String1[260]; // [esp+154h] [ebp-208h] BYREF
  char Destination[260]; // [esp+258h] [ebp-104h] BYREF

  memset(Destination, 0, sizeof(Destination));
  memset(String1, 0, sizeof(String1));
  v7 = 0;
  printf("pls input the first passwd(1): ");
  scanf("%s", Destination);
  if ( strlen(Destination) != 6 )
  {
    printf("Must be 6 characters!\n");
    ExitProcess(0);
  }
  v7 = atoi(Destination);
  if ( v7 < 100000 )
    ExitProcess(0);
  strcat(Destination, "@DBApp");
  v3 = strlen(Destination);
  sub_40100A((BYTE *)Destination, v3, String1);
  if ( !_strcmpi(String1, "6E32D0943418C2C33385BC35A1470250DD8923A9") )
  {
    printf("continue...\n\n");
    printf("pls input the first passwd(2): ");
    memset(Str, 0, sizeof(Str));
    scanf("%s", Str);
    if ( strlen(Str) != 6 )
    {
      printf("Must be 6 characters!\n");
      ExitProcess(0);
    }
    strcat(Str, Destination);
    memset(String1, 0, sizeof(String1));
    v4 = strlen(Str);
    sub_401019((BYTE *)Str, v4, String1);
    if ( !_strcmpi("27019e688a4e62a649fd99cadaafdb4e", String1) )
    {
      if ( !(unsigned __int8)sub_40100F(Str) )
      {
        printf("Error!!\n");
        ExitProcess(0);
      }
      printf("bye ~~\n");
    }
  }
  return 0;
}

分析大致逻辑:输入两个密码,这两个密码都是六位的,且第一个密码是不小于100000的整数。如果两次都输对了,就执行sub_40100F函数————这个函数是什么操作?点进去看看:

char __cdecl sub_4014D0(LPCSTR lpString)
{
  LPCVOID lpBuffer; // [esp+50h] [ebp-1Ch]
  DWORD NumberOfBytesWritten; // [esp+58h] [ebp-14h] BYREF
  DWORD nNumberOfBytesToWrite; // [esp+5Ch] [ebp-10h]
  HGLOBAL hResData; // [esp+60h] [ebp-Ch]
  HRSRC hResInfo; // [esp+64h] [ebp-8h]
  HANDLE hFile; // [esp+68h] [ebp-4h]

  hFile = 0;
  hResData = 0;
  nNumberOfBytesToWrite = 0;
  NumberOfBytesWritten = 0;
  hResInfo = FindResourceA(0, (LPCSTR)0x65, "AAA");
  if ( !hResInfo )
    return 0;
  nNumberOfBytesToWrite = SizeofResource(0, hResInfo);
  hResData = LoadResource(0, hResInfo);
  if ( !hResData )
    return 0;
  lpBuffer = LockResource(hResData);
  sub_401005(lpString, (int)lpBuffer, nNumberOfBytesToWrite);
  hFile = CreateFileA("dbapp.rtf", 0x10000000u, 0, 0, 2u, 0x80u, 0);
  if ( hFile == (HANDLE)-1 )
    return 0;
  if ( !WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) )
    return 0;
  CloseHandle(hFile);
  return 1;
}

看不懂,总之是创建了一个.rtf文件。这个文件里应该就是flag。那我们只要求出两个密码分别是什么就好了。

这个程序如何检验密码对不对呢?再看主函数,可知是对输入的两个字符串分别进行sub_40100A操作和sub_401019操作,然后与已知字符串比较。分别点进去,可以看出是哈希加密。那么到底是哪种哈希加密?

CryptCreateHash(phProv, 0x8004u, 0, 0, &phHash)

上网搜索,得知这个函数的第二个参数是算法标识符,用来选定采用哪种加密算法。继续搜索,搜到了 哈希加密算法的算法标识符 ,据此可知sub_40100A是sha1加密,sub_401019是MD5加密。由于第一个密码的可能取值不多,直接写python脚本暴力破解:

import hashlib
scat = '@DBApp'
for i in range(100000, 999999):
    ans = str(i) + scat
    rst = hashlib.sha1(ans.encode("utf-8"))
    rst = rst.hexdigest()
    if rst == "6e32d0943418c2c33385bc35a1470250dd8923a9":
        print(ans)
        break

运行结果是“123321@DBApp”,所以第一个密码是“123321”。
第二个密码难以暴力破解,所幸有 MD5在线解密 ,解出的结果是“~!3a@0123321@DBApp”。所以第二个密码是“~!3a@0”。

运行程序,输入两个密码:

就可以得到一个.rtf文件。打开文件就看到flag了。

[注] 我做完另一个题后突发奇想:这道题可不可以把两个strcmpi都nop掉呢?我试了一下,然后程序坏掉了……我看网上好像没人这么干,所以到底是不可行还是我太菜啊?欢迎大佬们在评论区指教!

回到顶部


4. [FlareOn6]Overlong

补充:大家请看下面我半年后重做的题解!这篇做的太sb了,不过我没删,就当日记留着了。

好久没更博客了,今天这道题的解出过程有点不一样,来写一下。

一如既往地把文件拖进IDA,随便点来点去点进加密函数,看到:

unsigned int __cdecl sub_401160(char *a1, int a2, unsigned int a3)
{
  unsigned int i; // [esp+4h] [ebp-4h]

  for ( i = 0; i < a3; ++i )
  {
    a2 += sub_401000(a1, a2);
    if ( !*a1++ )
      break;
  }
  return i;
}
int __cdecl sub_401000(_BYTE *a1, char *a2)
{
  int v3; // [esp+0h] [ebp-8h]
  char v4; // [esp+4h] [ebp-4h]

  if ( (int)(unsigned __int8)*a2 >> 3 == 30 )
  {
    v4 = a2[3] & 0x3F | ((a2[2] & 0x3F) << 6);
    v3 = 4;
  }
  else if ( (int)(unsigned __int8)*a2 >> 4 == 14 )
  {
    v4 = a2[2] & 0x3F | ((a2[1] & 0x3F) << 6);
    v3 = 3;
  }
  else if ( (int)(unsigned __int8)*a2 >> 5 == 6 )
  {
    v4 = a2[1] & 0x3F | ((*a2 & 0x1F) << 6);
    v3 = 2;
  }
  else
  {
    v4 = *a2;
    v3 = 1;
  }
  *a1 = v4;
  return v3;
}

看起来好像还挺友善,那就稍微分析一下,结果直接被绕晕了,投降。再一想,这题的加密方式好像并不常见,那是不是有捷径可走呢?

考虑到主函数里有一个MessageBox,结合以前的做题经验,不如直接点开.exe文件,看看弹窗输出了什么。

输出这么少吗?刚才在IDA里乱点的时候明明看到:

rdata:00402008 unk_402008      db 0E0h                 ; DATA XREF: start+B↑o
.rdata:00402009                 db  81h
.rdata:0040200A                 db  89h
.rdata:0040200B                 db 0C0h
.rdata:0040200C                 db 0A0h
.rdata:0040200D                 db 0C1h
.rdata:0040200E                 db 0AEh
.rdata:0040200F                 db 0E0h
.rdata:00402010                 db  81h
.rdata:00402011                 db 0A5h
.rdata:00402012                 db 0C1h
.rdata:00402013                 db 0B6h
.rdata:00402014                 db 0F0h
.rdata:00402015                 db  80h ; €
.rdata:00402016                 db  81h
.rdata:00402017                 db 0A5h

——————————————此处省略无数行————————————————

.rdata:004020AA                 db 0AFh
.rdata:004020AB                 db  6Eh ; n
.rdata:004020AC                 db 0C0h
.rdata:004020AD                 db 0AEh
.rdata:004020AE                 db 0F0h
.rdata:004020AF                 db  80h ; €
.rdata:004020B0                 db  81h
.rdata:004020B1                 db 0A3h
.rdata:004020B2                 db  6Fh ; o
.rdata:004020B3                 db 0F0h
.rdata:004020B4                 db  80h ; €
.rdata:004020B5                 db  81h
.rdata:004020B6                 db 0ADh
.rdata:004020B7                 db    0

从402009到4020B7,即使多位加密成一位,也绝对不止这么一小段,而且输出的字符串最后是冒号,有理由怀疑输出并不完整。再回到IDA找找控制输出的部分:

int __stdcall start(int a1, int a2, int a3, int a4)
{
  char Text[128]; // [esp+0h] [ebp-84h] BYREF
  unsigned int v6; // [esp+80h] [ebp-4h]

  v6 = sub_401160(Text, (int)&unk_402008, 0x1Cu);
  Text[v6] = 0;
  MessageBoxA(0, Text, Caption, 0);
  return 0;
}

注意到sub_401160这个函数,根据目前输出的弹窗,猜测最后一个参数0x1Cu(也就是28)表示输出字符串的长度,只要把它改得足够大就好了。于是用OD打开原文件尝试修改。我不懂汇编,但是进来后直接发现了1C这个数,猜测就是这个位置,那就把它改大一点,比如99(一开始改的EF,但OD不让改这么大,不知道为啥,欢迎大佬指教)

然后右键单击,Copy to executable,All modifications,然后关闭新窗口并保存新可执行文件,别又忘了怎么操作的!(对自己说的……)

最后打开新的.exe,flag就出来……嗯???

是不是改得太大了啊,要不再改小点,比如7F。

嗯,这次出来了。但是不明白为啥,我果然还是太弱了。

回到顶部


【补充】本题重做

我靠我之前咋那么菜啊……

很明显,开的Text数组大小为128,但程序只解密了前28个元素,所以输出不完整。只要把28改到128就好了。至于上面第一次做的题解,OD里1C改成99不对,就是因为0x99超过了128(Text数组空间)。

左键单击这个“28”,然后按Tab跳转到汇编窗口对应位置。因为左侧“6A 1C”就是右侧“push 1Ch”的机器码,所以Patch byte,将左侧1C改成80(0x80 = 128),然后Apply patches生成新文件。打开新文件直接拿到flag。

回到顶部


5. [ACTF新生赛2020]Universe_final_answer

用IDA打开,寻找关键位置,看到如下代码:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v4; // [rsp+0h] [rbp-A8h] BYREF
  char v5[104]; // [rsp+20h] [rbp-88h] BYREF
  unsigned __int64 v6; // [rsp+88h] [rbp-20h]

  v6 = __readfsqword(0x28u);
  __printf_chk(1LL, "Please give me the key string:", a3);
  scanf("%s", v5);
  if ( (unsigned __int8)sub_860(v5) )
  {
    sub_C50(v5, &v4);
    __printf_chk(1LL, "Judgement pass! flag is actf{%s_%s}\n", v5);
  }
  else
  {
    puts("False key!");
  }
  return 0LL;
}

大致就是在sub_860的位置对v5做了一些操作和检查,如果满足一定条件,就是正确的flag。点进去看看:

bool __fastcall sub_860(char *a1)
{
  int v1; // ecx
  int v2; // esi
  int v3; // edx
  int v4; // r9d
  int v5; // r11d
  int v6; // ebp
  int v7; // ebx
  int v8; // r8d
  int v9; // r10d
  bool result; // al
  int v11; // [rsp+0h] [rbp-38h]

  v1 = a1[1];
  v2 = *a1;
  v3 = a1[2];
  v4 = a1[3];
  v5 = a1[4];
  v6 = a1[6];
  v7 = a1[5];
  v8 = a1[7];
  v9 = a1[8];
  result = 0;
  if ( -85 * v9 + 58 * v8 + 97 * v6 + v7 + -45 * v5 + 84 * v4 + 95 * v2 - 20 * v1 + 12 * v3 == 12613 )
  {
    v11 = a1[9];
    if ( 30 * v11 + -70 * v9 + -122 * v6 + -81 * v7 + -66 * v5 + -115 * v4 + -41 * v3 + -86 * v1 - 15 * v2 - 30 * v8 == -54400
      && -103 * v11 + 120 * v8 + 108 * v7 + 48 * v4 + -89 * v3 + 78 * v1 - 41 * v2 + 31 * v5 - (v6 << 6) - 120 * v9 == -10283
      && 71 * v6 + (v7 << 7) + 99 * v5 + -111 * v3 + 85 * v1 + 79 * v2 - 30 * v4 - 119 * v8 + 48 * v9 - 16 * v11 == 22855
      && 5 * v11 + 23 * v9 + 122 * v8 + -19 * v6 + 99 * v7 + -117 * v5 + -69 * v3 + 22 * v1 - 98 * v2 + 10 * v4 == -2944
      && -54 * v11 + -23 * v8 + -82 * v3 + -85 * v2 + 124 * v1 - 11 * v4 - 8 * v5 - 60 * v7 + 95 * v6 + 100 * v9 == -2222
      && -83 * v11 + -111 * v7 + -57 * v2 + 41 * v1 + 73 * v3 - 18 * v4 + 26 * v5 + 16 * v6 + 77 * v8 - 63 * v9 == -13258
      && 81 * v11 + -48 * v9 + 66 * v8 + -104 * v6 + -121 * v7 + 95 * v5 + 85 * v4 + 60 * v3 + -85 * v2 + 80 * v1 == -1559
      && 101 * v11 + -85 * v9 + 7 * v6 + 117 * v7 + -83 * v5 + -101 * v4 + 90 * v3 + -28 * v1 + 18 * v2 - v8 == 6308 )
    {
      return 99 * v11 + -28 * v9 + 5 * v8 + 93 * v6 + -18 * v7 + -127 * v5 + 6 * v4 + -9 * v3 + -93 * v1 + 58 * v2 == -1697;
    }
  }
  return result;
}

好多方程式。本来直接手算的(累死我了),后来看了别人的wp才知道是z3求解器。于是学了一下z3,然后用如下脚本求解:

注意:建议用VSCode写z3,用IDLE或者PyCharm都出现了莫名其妙的问题,比如报告在__init__()中未找到sat和model(),import sympy也不行。当然,如果有大佬知道怎么回事,希望赐教……

先将解出的数值按顺序转换成字符,然后注意到sub_860中交换了v1和v2,还有v6和v7,得到字符串:

F0uRTy_7w@

直接交上去发现不对,回到主函数,发现输出v5之前还有一个sub_C50的操作,点进去看不懂,但转念一想,刚刚解出的字符串是主函数需要输入的v5,只要输入的v5正确,程序就会自动进行sub_C50这个加工,并输出flag。于是运行程序并输入上面的字符串,成功得到flag:

回到顶部


6. [NPUCTF2020]EzReverse

我以前好像也发过一篇去花指令的wp,不过当时并不知道为什么这么做,这次认真写一篇。

打开IDA,直接反编译,发现:

最上面的红色部分是什么意思呢?sp是栈指针,指向内存中堆栈的顶部。每次程序调用函数,都会为这个函数在堆栈中开出一片专属空间,称为栈帧;也就是把一大块空白区域压栈,这是为局部变量以及临时变量预留的空间,等函数执行完返回时,栈帧会从堆栈中弹出,堆栈恢复原始状态。调用函数之前,为了在原函数中保护现场,也需要把可能受影响的变量压栈,等函数执行完再弹出。
压栈和弹栈都会改变sp的值,因为它始终指向栈顶。
举个栗子:

global main

yijiayi:
  push ebx      ; 把ebx的值压栈(保护现场)
  mov eax, 1
  mov ebx, 1
  add eax, ebx
  pop ebx       ; 取出栈顶元素,将其赋给ebx(恢复现场)
  ret

main:
  mov ebx, 114514
  call yijiayi
  add eax, ebx
  ret

如果程序保存为hello.asm,然后使用以下指令运行程序并获取最后一个返回值(也就是main执行过后eax的值),应该得到114516。

; nasm -f elf hello.asm -o hello.o ; gcc -m32 hello.o -o hello ; ./hello ; echo $?

程序正常结束后,由于压入栈的东西应该已经全部对称地弹出,所以sp应为0。如果在上述yijiayi过程中插入一个无条件跳转:

yijiayi:
  push ebx
  mov eax, 1
  mov ebx, 1
  add eax, ebx
  jmp label1      ; 插入的无条件跳转
  pop ebx
label1:
  ret

这样yijiayi在计算后就不会恢复ebx的值,main的返回值就变成了3。与此同时,压入栈的“114514”这个值也没有正确地弹出,导致sp不为0,也就会导致出现上面红色的 positive sp value detected。

刚才插入的jmp指令就属于花指令。由于反编译器按程序执行过程反编译,这个jmp的插入就会导致IDA生成错误的伪代码,甚至无法反编译。

怎么办呢?只要找到并去掉上述的jmp,就可以正确地反编译了。在上述界面点进main,在IDA View上下翻找,找到这个红色部分,大概率就是花指令。看标红这行左侧5个字节,第一个字节E9就是x86下jmp的机器码。右键点击这个E9,然后Patching → Change byte,把E9改为90(nop的机器码)。修改后再在这个界面上下翻找,如果还有标红部分,就再修改,直到没有标红。这是因为受 逻辑上最靠前的花指令 的影响,IDA无法判断后续指令有哪些是花指令,所以每次只能报告一个花指令。

花指令全部去除后,点击左侧函数栏的start,在start里重新进入main的IDA View,右键点击这里的main,然后Create Function。这是因为受花指令影响,IDA认为main使用了未声明的变量,导致不可反编译,就没有自动创建这个函数。

此时直接按F5就看到伪代码了,就是个简单的迷宫题,直接切掉。

回到顶部


7. findKey

很巧啊,我现在是乱序做题,但刚做完一道花指令的题,又来一个。

下载后发现图标不太一样,但后缀名是普通的.exe,于是直接运行看看这是什么东西。

发现也没什么,不过应该有些前端的函数,比如注册窗口、绘制窗口,以及消息循环、回调函数这些,类似第一部分题解里那道JustRE。(做了这道题后,我发现那道题我其实也是一知半解过掉的)。
既然之前是一知半解,那这次就再说点题外话。

消息循环是程序中的一个持续运行的循环结构,负责从系统的消息队列中获取消息(鼠标点击、键盘输入、窗口重绘等),并将其分发给对应的处理函数。
回调函数就是上述“处理函数”,它是一种通过函数指针或接口注册的函数,在特定事件(如消息到达、异步操作完成)发生时被调用。
总之,像本题这种程序,里面会有一个循环用于接受系统消息,对每类消息都有特定的函数去处理。解题关键就在于找到消息循环,再找到并分析或修改能输出flag的回调函数。

好了,开始做题。把文件拖进IDA,点进WinMain直接看到了如下的消息循环(16到23行):

循环内部没什么特殊的操作,那就往上看。发现有个函数sub_40100F参与判断是否结束程序,点进去看看,失望地发现好像只是判断窗口注册是否成功。那就只能是sub_401023了,一路点进去,看到:

我不太懂,但这应该就是初始化一个窗口,而且sub_401014应该是用来初始化显示的内容,因为其他位置都没有字符串。盲猜flag就藏在里面。但是点进去一看,

修一下吧。去反汇编窗口(IDA View)寻找花指令,看到了标红的位置:

标红的这一行,上下是两个相同的push,将同样的东西压栈两次。不过被IDA标注为花指令的地址是下面的,那就去掉下面的。然后原地创建函数(快捷键为P),按F5反编译。
反编译出来的窗口上方还是提示sp不为0,但这里毕竟是程序中间的一部分,执行过后程序并未结束,所以堆栈非空也正常(应该是吧?欢迎大佬指教……)
分析一下代码,如下:

然后看看String1是如何加工得到的:

好像前面的题解里有过类似的函数……不管了,这次认真学一下。
首先,由于这个函数肯定执行了,那暂时不理会 CryptAcquireContextA 。然后根据Windows应用开发官方教程,看看 CryptCreateHash , CryptHashDataCryptGetHashParam ,(虽然没完全看懂)但大致可以知道hash过程由 CryptCreateHash 创建,另外两个函数就是对字符串进行具体计算,并保存hash值。hash方式又由 CryptCreateHash 中间三个参数决定,它们分别是:Algid(算法标识符)、hKey(密钥)、dwFlags(好像就是0?)对于本题,Algid = 0x8003,表示MD5。那就尝试 MD5解码 ,得到“123321”。

解码结果直接交上去,发现不对。再仔细看看修复出来的回调函数,发现if之后还有个sub_401005,然后又有个MessageBox,那可能“123321”再加工一次才能得到flag?

  memcpy((a2 - 1016), &unk_423030, 0x32u);
  v9 = strlen((a2 - 1016));
  sub_401005((a2 - 492), a2 - 1016, v9);
  MessageBoxA(*(a2 + 8), (a2 - 1016), 0, 0x32u);

点进去看看,这个函数类似第一次加工的那个函数,也是和第一个参数进行异或。但a2-492是什么?
(睡了,明天再说)

(从这里开始是第二天写的)
好家伙,我昨晚关机前点了左上角的Exit → Patch program → Apply patches to → Apply patches,并且关闭时点了DON'T SAVE the database,本意是保存一下进度,没想到今天重新反编译那个修复的函数,伪代码就变成了:

LRESULT __stdcall sub_401640(HWND hWndParent, UINT Msg, WPARAM wParam, LPARAM lParam)
{
  int v5; // eax
  size_t v6; // eax
  _WORD *v7; // edx
  DWORD v8; // eax
  int v9; // eax
  int v10; // eax
  LPSTR v11; // [esp-4h] [ebp-450h] BYREF
  int v12; // [esp+4Ch] [ebp-400h]
  UINT v13; // [esp+50h] [ebp-3FCh]
  CHAR v14[256]; // [esp+54h] [ebp-3F8h] BYREF
  char v15[7]; // [esp+154h] [ebp-2F8h] BYREF
  __int16 v16; // [esp+15Bh] [ebp-2F1h]
  char v17; // [esp+15Dh] [ebp-2EFh]
  char Str[253]; // [esp+160h] [ebp-2ECh] BYREF
  __int16 v19; // [esp+25Dh] [ebp-1EFh]
  char v20; // [esp+25Fh] [ebp-1EDh]
  CHAR v21[256]; // [esp+260h] [ebp-1ECh] BYREF
  CHAR String[4]; // [esp+360h] [ebp-ECh] BYREF
  int v23; // [esp+364h] [ebp-E8h]
  __int16 v24; // [esp+368h] [ebp-E4h]
  CHAR Text[32]; // [esp+36Ch] [ebp-E0h] BYREF
  struct tagRECT Rect; // [esp+38Ch] [ebp-C0h] BYREF
  CHAR Buffer[100]; // [esp+39Ch] [ebp-B0h] BYREF
  HDC hdc; // [esp+400h] [ebp-4Ch]
  struct tagPAINTSTRUCT Paint; // [esp+404h] [ebp-48h] BYREF
  int v30; // [esp+444h] [ebp-8h]
  int v31; // [esp+448h] [ebp-4h]

  LoadStringA(hInstance, 0x6Au, Buffer, 100);
  v13 = Msg;
  if ( Msg > 0x111 )
  {
    if ( v13 == 517 )
    {
      if ( strlen((const char *)String1) > 6 )
        ExitProcess(0);
      if ( strlen((const char *)String1) )
      {
        memset(v21, 0, sizeof(v21));
        v6 = strlen((const char *)String1);
        memcpy(v21, String1, v6);
        v11 = (LPSTR)String1;
        *v7 = __ES__;
        v8 = strlen((const char *)&v11);
        sub_40101E(String1, v8, v11);
        strcpy(Str, "0kk`d1a`55k222k2a776jbfgd`06cjjb");
        memset(&Str[33], 0, 0xDCu);
        v19 = 0;
        v20 = 0;
        strcpy(v15, "SS");
        *(_DWORD *)&v15[3] = 0;
        v16 = 0;
        v17 = 0;
        v9 = strlen(Str);
        sub_401005(v15, (int)Str, v9);
        if ( _strcmpi((const char *)String1, Str) )
        {
          SetWindowTextA(hWndParent, "flag{}");
          MessageBoxA(hWndParent, "Are you kidding me?", "^_^", 0);
          ExitProcess(0);
        }
        memcpy(v14, &unk_423030, 0x32u);
        v10 = strlen(v14);
        sub_401005(v21, (int)v14, v10);
        MessageBoxA(hWndParent, v14, 0, 0x32u);
      }
      ++dword_428D54;
    }
    else
    {
      if ( v13 != 520 )
        return DefWindowProcA(hWndParent, Msg, wParam, lParam);
      if ( dword_428D54 == 16 )
      {
        strcpy(String, "ctf");
        v23 = 0;
        v24 = 0;
        SetWindowTextA(hWndParent, String);
        strcpy(Text, "Are you kidding me?");
        MessageBoxA(hWndParent, Text, Buffer, 0);
      }
      ++dword_428D54;
    }
  }
  else
  {
    switch ( v13 )
    {
      case 0x111u:
        v31 = (unsigned __int16)wParam;
        v30 = HIWORD(wParam);
        v12 = (unsigned __int16)wParam;
        if ( (unsigned __int16)wParam == 104 )
        {
          DialogBoxParamA(hInstance, (LPCSTR)0x67, hWndParent, DialogFunc, 0);
        }
        else
        {
          if ( v12 != 105 )
            return DefWindowProcA(hWndParent, Msg, wParam, lParam);
          DestroyWindow(hWndParent);
        }
        break;
      case 2u:
        PostQuitMessage(0);
        break;
      case 0xFu:
        hdc = BeginPaint(hWndParent, &Paint);
        GetClientRect(hWndParent, &Rect);
        v5 = strlen(Buffer);
        DrawTextA(hdc, Buffer, v5, &Rect, 1u);
        EndPaint(hWndParent, &Paint);
        break;
      default:
        return DefWindowProcA(hWndParent, Msg, wParam, lParam);
    }
  }
  return 0;
}

又详细又规整,而且昨天那个“Positive sp value”的提示也没了,感觉这才是它的真面目吧。为什么会这样呢?Deepseek R1说:

不知道对不对(求教),反正我信了。此时可以看到sub_401005是用之前解出的“123321”对v14异或,而v14的值是从&unk_423030赋过来的(往上两行的memcpy),unk_423030又可以直接点进去看到值。

然后用下面的exp求出异或的结果,拿下此题。

#include<bits/stdc++.h>
#define ll long long

char chs[] = {'W','^','R','T','I','_',0x1,'m','i','F',0x2,'n','_',0x2,'l','W','[','T','L'};
std::string key = "123321";

signed main(){
    for(int i = 0; i < strlen(chs); i++){
        chs[i] ^= key[i % 6];
    }
    std::cout << chs << "\n";
    return 0;
}

回到顶部


结语

这篇题解的时间跨度似乎有点长了,从 第三学期开学 写到了 第四学期即将开学;而且更难的题我似乎一时半会没法做了,需要去储备点知识了。那就先收笔吧。


posted on   C12AK  阅读(269)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5

点击右上角即可分享
微信分享提示