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

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

一如既往地把文件拖进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。

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

回到顶部


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:

回到顶部


posted on 2024-09-02 14:34  C12AK  阅读(32)  评论(0编辑  收藏  举报