imgencryptWriteUp
题目链接: SOCTF - 综合实训平台
题目描述:
小明为了安全,把图片用工具加密起来了,但没想到这个工具没有解密功能,你能帮他恢复吗?
flag格式: flag{xxxxxx}
下载题目附件, 打开发现有imgencrypt和output两个文件. 使用file
命令查看imgencrypt为elf 64, output为data. 猜测imgencrypt为加密程序, output为需要解密的图片. 运行加密程序, 发现如题目描述所示, 其解密程序"未实现".
ida动态调试(远程调试)
ida在加载该elf时, 提示"idt错误", 可能需要修补idt. 但是因为elf在运行时并不需要idt, 所以决定采用动态调试的方式对程序进行辅助分析.
在ida的函数列表中, 发现未混淆的encrypt
函数和decrypt
函数. 打开decrypt
函数, 发现看不懂(
将ida64安装目录下的dbgsrv/里的Linux调试服务器上传到Linux主机, 设置好权限和防火墙并运行, 进行远程调试. 程序运行时的主菜单如下所示:
欢迎使用文件加密工具
=========================
[1] 加密文件
[2] 解密文件
[3] 退出
请输入指令:
尝试破解decrypt函数
在菜单栏的Jump命令里选择Jump to function, 跳转到decrypt
函数, 设置断点. 在Linux主机中对程序进行交互, 按照程序提示, 输入2并回车, 此时程序断在decrypt
函数中. 在ida中步进跟踪该函数, 发现其做了一系列看不懂的操作(
看起来decrypt
函数应该是如同题目描述所言的"并未实现"了...
根据encrypt函数逆向猜解解密函数
encrypt
函数运行过程如下:
请输入指令: 1
请输入要加密的文件名:
bmp
请输入加密后的文件名:
output2
文件加密成功!
同样是使用Jump to function, 跳转到encrypt
函数并设置断点. 此时使用f5可以直接查看C语言代码. 可以看到其主要逻辑如下所示: (这段代码其实是使用ida静态分析之后得到的代码, 使用动态调试后得到的代码与这个略有不同, 但大体类似. 注释为猜测的可能的函数)
__int64 __usercall encrypt@<rax>(__int64 a1@<rdx>, __int64 a2@<rbp>, __int64 a3@<rsi>)
{
// ...
v12 = sub_55D4BA9FF120(&v14, &unk_55D4BAA000D8); // fpInput = fopen(filename, 'r');
if ( v12 )
{
v13 = sub_55D4BA9FF120(&v15, &unk_55D4BAA000F3);// fpOutput = fopen(outputFilename, 'w');
if ( v13 )
{
while ( 1 )
{
v11 = sub_55D4BA9FF0E0(&v16, 1LL, 10LL, v12); // fileReadCount = fread(buffer, 1, 10, fpInput)
if ( v11 <= 0 )
break;
for ( i = 0; i < v11; ++i ) // 这里是主要的加密逻辑
*((_BYTE *)&v18 + i - 208) ^= aN0b0dykn0w[i];// buffer[i] ^= key[i];
sub_55D4BA9FF150(&v16, 1LL, v11, v13);// fwrite(buffer, 1, fileReadCount, fpOutput);
}
sub_55D4BA9FF0D0(&unk_55D4BAA00117, 1LL, v7);
sub_55D4BA9FF0F0(v12); // fclose(fpInput);
v4 = (void *)v13;
sub_55D4BA9FF0F0(v13);// fclose(fpOutput);
result = 0LL;
}
else
{
v4 = &unk_55D4BAA000F8;
sub_55D4BA9FF110();
result = 0LL;
}
}
else
{
v4 = &unk_55D4BAA000DB;
sub_55D4BA9FF110();
result = 0LL;
}
v9 = __readfsqword(0x28u);
v8 = v9 ^ v17;
if ( v9 != v17 )
result = sub_55D4BA9FF100(v4, v8, v5);
return result;
}
-
利用第一个逻辑分支判断
v12
:输入一个不存在的文件名,
v12
的值为0
, 程序打印"打开文件失败"并退出. 因此v12
为读文件的指针. -
利用第二个逻辑分支判断
v13
:同上, 输入一个不可写的文件名,
v13
的值为0
, 程序打印"写入文件失败"并退出. 因此v13
为写文件的指针. -
判断
v11
:根据函数
v11 = sub_55D4BA9FF0E0(&v16, 1LL, 10LL, v12);
,立即数1
和10
的差值为10
. 动态调试时发现, 该函数执行结束后返回值v11==0x0A
, 因此v11
可能为读取文件后返回的数据长度. -
利用断点判断
*((_BYTE *)&v18 + i - 208)
:主要加密功能的代码如下所示:
loc_5606C2F46449: ; CODE XREF: encrypt+167↓j mov eax, [rbp-1CCh] cdqe movzx ecx, byte ptr [rbp+rax-0D0h] mov eax, [rbp-1CCh] cdqe lea rdx, aN0b0dykn0w ; "n0b0dykn0w" movzx eax, byte ptr [rax+rdx] xor ecx, eax mov edx, ecx mov eax, [rbp-1CCh] cdqe mov [rbp+rax-0D0h], dl add dword ptr [rbp-1CCh], 1 loc_5606C2F46486: ; CODE XREF: encrypt+11C↑j mov eax, [rbp-1CCh] cmp eax, [rbp-1C4h] jl short loc_5606C2F46449
在异或运算(
xor ecx,eax
)处设置断点.ecx
的值来自于内存[rbp+rax-0D0h]
(0xD0=208), 对应的c语言代码应该是*((_BYTE *)&v18 + i - 208)
;eax
的值来自于内存[rax+aN0b0dykn0w]
, 对应的c语言代码应该是aN0b0dykn0w[i]
. 连续点击运行按钮多次, 每次在断点处监视寄存器ecx
和eax
的值, 发现ecx
的值即为文件的字节. 因此可以判断*((_BYTE *)&v18 - 208)
即为fread
函数的buffer. (其实直接看后面fwrite
时的buffer地址也能判断) -
判断
fwrite
:定位如下代码
mov eax, [rbp-1C4h] movsxd rdx, eax mov rcx, [rbp-1B8h] lea rax, [rbp-0D0h] mov esi, 1 mov rdi, rax call sub_5606C2F46150
参数1(rdi) 参数2(rsi) 参数3(rdx) 参数4(rcx) [rbp-0D0H]
1
[rbp-1C4h]
[rbp-1B8h]
参数1即为上述4所分析出的buffer的地址, 参数2为固定值1, 参数3为上述3分析出的
v11
, 参数4为上述2分析出的"写文件指针".
综上, 可以看出该程序的加密逻辑为:
读取文件, 将文件中每十个字节与字符串"n0b0dykn0w"
进行异或运算, 输出文件.
求解flag
按照上文分析, 既然是"异或加密", 且密钥是写死在程序中的. 则只需要用改程序对加密文件再执行一次"加密操作"即可解密. 运行程序, 对output文件进行加密操作, 打开输出的文件即为flag.
flag{y0u_f0und_the_0riginal_image}