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 , CryptHashData 和 CryptGetHashParam ,(虽然没完全看懂)但大致可以知道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;
}
结语
这篇题解的时间跨度似乎有点长了,从 第三学期开学 写到了 第四学期即将开学;而且更难的题我似乎一时半会没法做了,需要去储备点知识了。那就先收笔吧。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】