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: