MTCTF 2021 Inject Writeup
美团 CTF 上一道有趣的逆向题目
主要涉及:代码解密,远程注入,函数劫持
分析题目
题目给了一个 inject.exe 和 notepad2.exe
首先在 inject.exe 中输入正确的 key1 和 key2 后,inject.exe 会使用 key1 和 key2 来解密程序中的资源文件 yyy.a,并将 yyy.a 作为动态库注入到 notepad2.exe 进程中
接着在启动后的 notepad2.exe 中输入正确的 key3,进程中的 yyy.a 会对 key3 做校验,如果结果正确则将 flag 写入到 flag.txt
解密动态库
其中 key1 和 key2 的约束条件如下
M=0x75D05803
t=dword(key1/key2)%M
p1=pow(t,8)%M
p2=pow(t,2)%M
if ((2*t-p2-42+p1)%M==1 && gcd(key1,key2)==1) goto right
考虑到这个约束条件不太好解决,用 z3 也没跑出来,所以可以考虑从后面解密 yyy.a 的部分入手
解密生成 yyy.a 的代码如下
v9 = fopen(v8, "wb");
v10 = FindResourceA(0i64, (LPCSTR)0x65, "code");
v11 = SizeofResource(0i64, v10);
v12 = LoadResource(0i64, v10);
v13 = LockResource(v12);
fwrite(v13, 1ui64, v11, v9);
v14 = FindResourceA(0i64, (LPCSTR)0x66, "code");
v15 = SizeofResource(0i64, v14);
v16 = LoadResource(0i64, v14);
v17 = LockResource(v16);
v18 = malloc((unsigned int)v15);
v19 = 0;
if ( (_DWORD)v15 )
{
v20 = v18;
v21 = v17 - v18;
do
{
*v20 = v20[v21] ^ *((_BYTE *)&dword_7FF6F30F66B8 + (v19++ & 7));
++v20;
}
while ( v19 < (unsigned int)v15 );
}
fwrite(v18, 1ui64, v15, v9);
v22 = FindResourceA(0i64, (LPCSTR)0x67, "code");
v23 = SizeofResource(0i64, v22);
v24 = LoadResource(0i64, v22);
v25 = LockResource(v24);
fwrite(v25, 1ui64, v23, v9);
fclose(v9);
生成的 yyy.a 内容由 Resource(101) + Decrypt(Resource(102), key1_2) + Resource(103) 三部分组成
其中 key1_2 是根据 key1 和 key2 生成的 8 字节密钥
Decrypt 使用 key1_2 的 8 个字节来循环异或 Resource(102) 进行解密
下断点查看第三部分开头的内容都是 0,所以可以合理猜测解密后第二部分结尾的内容也是 0
下断点看一下解密前第二部分结尾的几个 8 字节序列确实是相同的,证实了上边的猜测,这样我们就得到了 key1_2 的内容
key1_2 = [0x17, 0xe3, 0xe3, 0x37, 0x17, 0xe3, 0xe3, 0x00]
改 ip 跳过前边的约束检查,直接动态修改内存中的 key1_2,继续运行就可以得到解密后的 yyy.a 了
分析动态库
简单看一下发现 yyy.a 套了个 upx 壳,直接 upx -d 脱掉
主函数如下,可以看出是 hook 了 notepad2.exe 的 WriteFile 函数来实现对 key3 的校验操作
MessageBoxA(
0i64,
"Now input key3 in notepad like `key3:abcdef`.\nSave it.Reopen the file, you will get the flag.\n",
"Message",
0);
v1 = GetModuleHandleA("kernel32.dll");
WriteFile = GetProcAddress(v1, "WriteFile");
v3 = WriteFile + *(WriteFile + 2);
qword_180004630 = *(v3 + 6);
VirtualProtect(v3 + 6, 8ui64, 4u, &flOldProtect);
qword_180004628 = sub_1800011A0;
*(v3 + 6) = sub_1800011A0;
VirtualProtect(v3 + 6, 8ui64, flOldProtect, &flOldProtect);
新的函数入口在 sub_1800011A0,里面有校验 key3 与生成 flag 的代码
校验 key3 的代码如下
bool check()
{
unsigned __int64 v0; // r8
unsigned __int64 v1; // r9
__int64 v2; // rcx
unsigned __int64 v3; // r11
unsigned __int64 v4; // r10
__int64 v5; // rcx
__int64 v6; // rbx
v0 = 1i64;
v1 = 1i64;
v2 = 4i64;
v3 = input % 0x6440DB83ui64;
do
{
v1 = v3 * v1 % 0x6440DB83;
--v2;
}
while ( v2 );
v4 = 1i64;
v5 = 3i64;
do
{
v4 = v3 * v4 % 0x6440DB83;
--v5;
}
while ( v5 );
v6 = 2i64;
do
{
v0 = v3 * v0 % 0x6440DB83;
--v6;
}
while ( v6 );
return (2 * v0 - v4 + v1 - 32) % 0x6440DB83 == 1;
}
生成 flag 的代码如下
sub_180001010(a2, "key3:%x", &dword_18000463C);
*(float *)&input = (double)(int)dword_18000463C
* 1.818989403545856e-12
* 1.818989403545856e-12
* 0.00000002980232238769531;
if ( check() )
{
v9 = (char *)malloc(0x2Bui64);
idx = 5;
v11 = byte_180003280;
flag = v9;
len = 0;
idx_ = 5i64;
qmemcpy(v9, "flag{", 5);
v15 = 0x10842000i64;
do
{
if ( idx_ <= 28 && _bittest64(&v15, idx_) )
{
v16 = '-';
}
else
{
v17 = len % 8;
v18 = *v11;
++len;
++v11;
v16 = v18 ^ *((_BYTE *)&input + v17);
}
flag[idx_] = v16;
++idx;
++idx_;
}
while ( len < 32 );
v19 = 42i64;
v20 = flag;
*(_WORD *)&flag[idx] = '}';
}
else
{
v19 = 23i64;
v20 = "your key isn't correct.";
}
爆破密钥
爆破下面这个 key3 的中间结果,结果为 386499290
*(float *)&input = (double)(int)dword_18000463C
* 1.818989403545856e-12
* 1.818989403545856e-12
* 0.00000002980232238769531;
#include <cstdio>
bool check(unsigned input)
{
unsigned long long v0; // r8
unsigned long long v1; // r9
unsigned long long v2; // rcx
unsigned long long v3; // r11
unsigned long long v4; // r10
unsigned long long v5; // rcx
unsigned long long v6; // rbx
v0 = 1;
v1 = 1;
v2 = 4;
v3 = input % 0x6440DB83u;
do
{
v1 = v3 * v1 % 0x6440DB83;
--v2;
}
while ( v2 );
v4 = 1;
v5 = 3;
do
{
v4 = v3 * v4 % 0x6440DB83;
--v5;
}
while ( v5 );
v6 = 2;
do
{
v0 = v3 * v0 % 0x6440DB83;
--v6;
}
while ( v6 );
return (2 * v0 - v4 + v1 - 32) % 0x6440DB83 == 1;
}
int main(){
printf("%d\n",check(386499290));
for (unsigned int i=0;i<0xffffffff;i++){
if (check(i)) {
printf("%u\n",i);
getchar();
}
}
}
使用上面的中间结果爆破 key3,结果为 4505965
#include <cstdio>
unsigned int input=0;
int main(){
for (unsigned int i=0;i<0xffffffff;i++){
*(float *)&input = (double)i
* 1.818989403545856e-12
* 1.818989403545856e-12
* 0.00000002980232238769531;
if (input==386499290) printf("%u",i);
}
}
调用动态库
写一个程序直接从外部调用 yyy.a 的校验函数 sub_1800011A0 (偏移量为 0x11A0),传入 "key3:44c16d" (4505965)
#include <stdio.h>
#include <windows.h>
#include <map>
using namespace std;
typedef int (*func1)(__int64,char*, __int64, __int64, __int64);
int main() {
HMODULE hdll = NULL;
hdll = LoadLibrary(L"yyy_unpack.dll");
if (hdll != NULL) {
printf("YES\n");
func1 myfunc = ((func1)((PBYTE)hdll + 0x11A0));
for (int i = 0; i < 128; i++)
{
printf("%02x", *((unsigned char*)myfunc + i));
}
myfunc(0, (char*)"key3:44c16d", 0, 0, 0);
}
else {
printf("NO\n");
}
FreeLibrary(hdll);
return 0;
}
在 myfunc 这里下断点跟进去,在解密完成后的地方再下断点,就可以在内存中找到 flag