这道题真的是很难的,....,做完之后上网看了看别的师傅的题解,虽然整体是这个流程,但是细节处理一个人一个方法,有的方法就会简便很多.
不过我这种各种瞎猜的果然还是比较简单(着实是沾了非实战是个做题的光)
做题和实战最大的区别就是做题往往整个程序控制流都和题有关,不会有大量的无关的东西.
看上去又是一道双线程"双手互搏"题.
运行一下发现全程不需要输入,后来发现是第一次我的terminal抽风.
PS E:\ctf\work\SUCTF2019 Akira Homework> .\WinRev.exe
[+]======================[+]
[+] Akira's Homework 2nd [+]
[+]======================[+]
[=] My passwords is:
调用很多WinAPI,还是动调一下. 一动调直接谈了个对话框"Stop debugging program"
找到对应位置, 扬了就行
对应位置后面是这样的,把四个数组分别异或一个table中的值,我们直接打个断点看看
荧光笔打上的地方就是四个字符串
GetProcAddress的MSDN文档
可以读到就是从ntdll提取这三个模块的地址, 我们继续检查这个函数8850的交叉引用,看看这个地址存在哪里要用来干嘛
放到了一个堆内存里.这个函数是main的第一个函数.没太能看的出来它想干啥, 但是这三个函数分别是获取进程\线程信息和Apc队列. 没见过这东西,查一下.
大概就是在多线程运行条件下,当某个程序执行到特殊函数造成的等待状态时,系统中断, 本线程优先去APC队列中调用一个Callback.
但是现在没有APC到底注入了什么函数的信息, 翻了翻也没找到, 我们回到main, 去看看第二个线程执行了什么
第二个线程从StartAddress开始,注意到main的结尾调用了一个SleepEx,这个函数是可以触发APC的
StartAddress里也同样有一个Sleep, 我们看看都干了啥
__int64 sub_7FF7B0ED8B20()
{
size_t v1; // rax
int i; // [rsp+24h] [rbp-304h]
int j; // [rsp+28h] [rbp-300h]
BOOL v4; // [rsp+2Ch] [rbp-2FCh]
int v5; // [rsp+30h] [rbp-2F8h] BYREF
HANDLE hSnapshot; // [rsp+38h] [rbp-2F0h]
__int64 v7; // [rsp+40h] [rbp-2E8h] BYREF
PROCESSENTRY32W pe; // [rsp+50h] [rbp-2D8h] BYREF
char v9[48]; // [rsp+290h] [rbp-98h] BYREF
wchar_t String1[40]; // [rsp+2C0h] [rbp-68h] BYREF
sub_7FF7B0ED8300(&v7, 1i64, 1048577i64);
memset(&pe, 0, sizeof(pe));
pe.dwSize = 568;
hSnapshot = CreateToolhelp32Snapshot(2u, 0);
if ( hSnapshot == (HANDLE)-1i64 )
return 0i64;
v4 = Process32FirstW(hSnapshot, &pe);
if ( !v4 )
return 0i64;
while ( v4 )
{
memset(v9, 0, 0x21ui64);
memset(String1, 0, 0x42ui64);
v1 = wcslen(pe.szExeFile);
sub_7FF7B0ED7DD0(pe.szExeFile, v1, v9);
for ( i = 0; i < 16; ++i )
sprintf_s((char *const)&String1[2 * i], 3ui64, L"%02x", (unsigned __int8)v9[i]);
for ( j = 0; (unsigned __int64)j < 0x31; ++j )
{
if ( !wcscmp(String1, &a438078d884693c[33 * j]) )
exit(-1);
}
v4 = Process32NextW(hSnapshot, &pe);
}
v5 = 106;
if ( !byte_7FF7B0EE6158 )
{
sub_7FF7B0ED6C10(qword_7FF7B0EE6178, v7, &v5);
byte_7FF7B0EE6158 = 1;
}
return 1i64;
}
8300这个函数不明所以,但是后面6C10用到了它的返回值.我们先从可读的地方探一探
这个CreateToolhelp32Snapshot是为了获取一个进程快照,第一个参数代表快照的系统部分,为2代表要包括系统中的所有进程.第二个参数为0代表当前进程
之后Process32First/Next就是遍历这个Snapshot中打下的系统的所有进程. szExeFile是这个遍历到的进程的名称.其实我目前还没有理解这到底是是打下了所有进程还是我当前进程, 我猜测是因为不知道有没有其他进程创建当前进程,所以dump下来系统其他进程的信息, 和当前进程内存信息
7DD0这个函数可以通过magic number判断是一个md5函数, 没有什么好说的, 最后结果存到a3指向地址里
结合一起猜测这应该也是个反调, 要求不能有进程的md5和固定值符合, 虽然是瞎猜的,但是调试时确实会进到这里, 扬掉
后半段是一些和线程锁相关的东西,感觉是切换线程前的准备
继续看StartAddress的第二个函数
晕, 这反调多的眼花缭乱的, 这个a1通过交叉引用也找到了位置:正是我们一开始解密的ntdll里的三个函数, 注意到有一个qword_7FF7B0EE6188 = (__int64)v6; 这里用到了这个指针qword
fastcall func2(void (__fastcall **callback)(HANDLE, __int64, _QWORD, _QWORD))
这里的函数声明也可以对应的改成callback了,方便看
这整个函数都不好,看了也没用,直接干掉它的调用了(我在想是不是整个子进程都可以干掉). 再测一下发现main能正常进去了, 我们调一下
这个exit-1看着就不像好事,猜测9710这个函数要返回true, table[3] 是 0x49, 我们直接看看它会puts啥吧.
擦, 这个进程进去之后会等一会-1退出, 正好我拖IDA的窗口,把IDA干崩了,
unsigned char ida_chars[] =
{
0x4A, 0x3A, 0x4C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C,
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C,
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x4A, 0x3A, 0x4C, 0x1B, 0x4A,
0x3A, 0x4C, 0x31, 0x50, 0x7A, 0x78, 0x63, 0x70, 0x36, 0x62,
0x31, 0x59, 0x7E, 0x7C, 0x74, 0x66, 0x7E, 0x63, 0x7A, 0x31,
0x23, 0x7F, 0x75, 0x31, 0x4A, 0x3A, 0x4C, 0x1B, 0x4A, 0x3A,
0x4C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C,
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C,
0x2C, 0x2C, 0x2C, 0x4A, 0x3A, 0x4C, 0x1B, 0x4A, 0x2C, 0x4C,
0x31, 0x5C, 0x68, 0x31, 0x61, 0x70, 0x62, 0x62, 0x66, 0x7E,
0x63, 0x75, 0x62, 0x31, 0x78, 0x62, 0x2B, 0x11
};
#include <cstdio>
int main() {
for(int i = 0; i < 108; i ++ ) {
putchar(ida_chars[i] ^ 0x11);
}
}
---
/out:test1.exe
test1.obj
[+]======================[+]
[+] Akira's Homework 2nd [+]
[+]======================[+]
[=] My passwords is:
9710干了这件事, 然后应该是读入一个18s, 并扔到9200里判断要不要返回1
如果判断错误会打印WHO ARE YOU????
然后退出, 先把9200的结果逆了
memset(v6, 0, 0x13ui64);
memcpy(v6, &target1, 0x12ui64);
for ( i = 0; i < 0x13; ++i )
v6[i] ^= xor_table[1];
memset(v7, 0, 0x13ui64);
for ( j = 0; j < 0x12; ++j )
v7[j] = j ^ a1[j];
v5[0] = 1;
v5[1] = 5;
v5[2] = 4;
v5[3] = 2;
v5[4] = 3;
v5[5] = 0;
for ( k = 0; k < 0x12; ++k )
{
if ( v6[k] != v7[6 * (k / 6) + v5[k % 6]] )
return 0;
}
核心直接摆这儿了,就是一个Xor+轮换,很好逆
#include <cstdint>
#include <cstdio>
uint8_t target1[] =
{
0x2F, 0x1F, 0x20, 0x2E, 0x34, 0x04, 0x37, 0x2D, 0x10, 0x39,
0x7C, 0x22, 0x7B, 0x75, 0x0A, 0x38, 0x39, 0x21
};
uint8_t ans[20];
uint8_t shift[6] = {1,5,4,2,3,0};
int main() {
for(int i = 0; i < 18; i ++ ) target1[i] ^= 0x45;
for(int i = 0; i < 18; i ++ ) {
ans[6*(i/6)+shift[i%6]] = target1[i];
}
for(int i = 0; i < 18; i ++ ) {
ans[i] ^= i;
}
printf("%s", ans);
}
// Akira_aut0_ch3ss_!
Akira自走棋是吧
下面还有个6C10用了我们的输入, 走进去看看, 又用了个虚函数, 懒得找表,动调看看在哪
char __fastcall sub_7FF6B9348910(__int64 a1, const char *a2)
{
int i; // [rsp+20h] [rbp-28h]
int v4; // [rsp+24h] [rbp-24h]
v4 = strlen(a2);
for ( i = 0; i < dword_7FF6B9351194; ++i )
{
if ( !(i % 3) )
byte_7FF6B93511A0[i] ^= a2[i / 3 % v4];
}
SetEvent(Handles[0]);
return 1;
}
把一段长度4C00的内存和我们的输入异或了, 而且这段内存后面还有几次调用,暂时不知道在哪,我们给它改个名叫secret
[=] My passwords is:
Akira_aut0_ch3ss_!
Have no sign!
Time out!
接下来要在有限的时间内sign,不然就exit
我想找找在哪,首先关注到了main函数最后调用的一个函数,的最末尾,但是这里是另一个failed
Failed to check sign!
我们回到这个函数的开头,关注另外两个异或解密,
Source的结果是:signature
Buffer是Have no sign!
就是要创建或打开一个$FileName:signature的文件,如果创建或打开不了就no sign, 我猜后面的参数是不创建只打开, 看看MSDN
OPEN_EXISTING3 |
仅当文件或设备存在时才打开它。如果指定的文件或设备不存在,则函数失败并且最后一个错误代码设置为 ERROR_FILE_NOT_FOUND (2) |
|
|
正是如此, 所以我们要创建一个这个文件然后过验证
诶人傻了怎么不能带冒号啊...
查了一下这个冒号是CreateFIleW里指定的文件名:文件流, 也就是我们实际上是要创建一个叫signature的流写到源文件里
这里要求流的md5和给定的md5值相同, 然后有一个类似之前的结构调用了第二个虚函数
先给这个MD5解密
我尝试直接向WinRev.exe:signature写\用Linux写,都失败了
先不管了, 先找到了这三个虚函数, 8910是和自走棋异或, 8A80没有参数, 是把它的高低位交换
89e0则是把它和一个参数^0x33再异或,结合这部分调用的虚函数传进去的参数为0, 而且调用在8910的后面,我优先猜测它应该是高低位交换,现在不管文件流,我们要找找89e0在哪里调用的
其实就是找到这样的结构, 诶诶诶, 其实要是点进去6C10, 会发现这里也有很多和mutex相关的东西,还记得好久以前我说过一个函数和信号量相关,应该没用么?我猜当时是虚函数调用被我忽略了.我们回去找找8B20, 没错,就是这儿,而且当时调用的也是6C10这个函数
其实三次调用的都是这个,不过第二个参数影响了调用虚函数具体是哪个,结合这个函数出现的位置,我们可以猜测它就是第一个异或固定值的虚函数, 传进去的参数是106, 这样我们应该就把那段长度4C00的内存解密了, 虽然那么长应该不是Flag还没完...
uint s[] = {
略
}
#include <cstdio>
#include <cstdint>
int main() {
int len = 19456;
char s1[] = "Akira_aut0_ch3ss_!";
for(int i = 0; i < len; i ++ ) if(i % 3 == 1) {
s[i] ^= 106 ^ 0x33;
}
for(int i = 0; i < len; i ++ ) if(i % 3 == 0) {
s[i] ^= s1[i/3%18];
}
for(int i = 0; i < len; i ++ ) if(i % 3 == 2) {
s[i] = (s[i] >> 4) | ((s[i] << 4)&0xFF);
}
FILE *fp = fopen("tt.bin", "wb");
fwrite(s, 1, len, fp);
fclose(fp);
}
这东西打开之后清晰可见的一个MZ头,不是dll就是一PE, 直接扔IDA里看好了
这下怎么都该完了吧..看看这个解一下
跟到2800里一看,跟了好几层, 终于有了一个变换了
内存是这样的, 开头0x63,0x7C,0x77,0x7B..这..一眼AES, 我现在只期望它是个朴素的AES-128,不要再玩花了, 我们输入要和AES的结果一样, AES的明文由原程序给出,看来原来的程序最后应该是装载了这个DLL, 我猜是最后一个sleep之后, 前面那个A什么队列里塞了装载.. 当然,这是本着做题的角度,我觉得那个不可能没用.. 现在回头看看合格到底怎么调用的..
而且我觉得这里真的好怪啊, 传过来的是AES明文, 输入的是加密后的结果, 那加密后的结果也够呛能打印啊.一般应该是我输入加密之后和传过来的密文对比吧..不管怎么样先弄到共享内存的值再说, 因为这后面有个0x52, 0x9, 没准下标+256这不是加密是解密呢((((
共享内存的MSDN文档
直接搜ShareMemory字符串
找到Target,
呜..这题真麻烦啊...我做了3个小时..