2022 *CTF REVERSE 的 NaCl
下载附件:
.
.
照例扔入 exeinfope 中查看信息:
.
.
照例扔入虚拟机中运行一下,查看主要回显信息:
.
.
照例扔入 IDA64 中查看伪代码,没有 main 函数,根据关键字符串交叉引用定位到函数 sub_8001774 处:
.
.
因为是当时我不会做,所以赛后我尽力梳理逻辑,我的做法就是每一条指令都尽力理解它的作用,所以基本每条都有注释。
.
.
.
然后。。。。我误解了别人的博客,或者说是大佬们写的实在是太简略了啊!!!
我硬啃了这段 43 次循环的加密逻辑,怎么都得不到下面的加密逻辑,设置连 (ROL(x, 1)&ROL(x, 8)) ^ ROL(x, 2) ^ y ^ data[i]
这段代码都分析不出来,然后我去找这段 43 次循环中与输入相关的部分,一直找一直找都找不到,然后就怀疑人生了(༎ຶ-༎ຶ)
.
.
卡了好久好久,又不肯放弃:
因为分析不出来,以为是自己汇编底子不好(事实上的确不好),但是又不甘心,每天都看一遍,希望能有新的突破。
直到一天我发现了 FW_ltlly 的博客,他从伪代码切入,把 r15 nop
且修补为 rsp
等价形式的方法让我欣喜若狂,因为修补后能让 IDA
分析出伪代码,这不仅对汇编底子不够的人极为友好,也是官方提到的正确的做法。
.
.
跟着他的思路,我们从头开始看起:
.
大量 jmp 的反跳代表 IDA 错误的将上方的函数识别为函数块:(下面文字出自 Sch01aR# 的博客)
函数块
在由 Microsoft Visual C++ 编译器生成的代码中,经常可以找到函数块
.
编译器移动不常执行的代码段,用以将经常执行的代码段“挤入”不大可能被换出的内存页,由此便产生了函数块
.
如果一个函数以这种方式被分割,IDA 会通过跟踪指向每个块的跳转,尝试定位所有相关的块
.
多数情况下,IDA 都能找到所有这些块,并在函数的头部列出每一个块
.
有时候,IDA 可能无法确定与函数关联的每一个块,或者函数可能被错误地识别成函数块,而非函数本身
.
在这种情况下,需要创建自己的函数块,或删除现有的函数块
.
在反汇编代码清单中,函数块就叫做函数块;在 IDA 的菜单系统中,函数块叫做函数尾(function tail)
.
要删除现有的函数块,将光标放在要删除的块中的任何一行上,然后选择 Edit -> Functions -> Remove Function Tail
.
在初次将文件加载到 IDA 时,取消选择 Create function tails 加载器选项,IDA 将不创建函数块
.
如果禁用了函数尾,已经包含函数尾的函数将包含指向函数边界以外区域的跳转,IDA 会在反汇编代码清单左侧的箭头窗口中用红线和箭头突出显示这些跳转
.
.
前面分析中我们说过在 sub_8080900 函数中 r15 当成 rsp 来使用了。
.
.
因为函数块是一个函数被分割成各个部分,中间用 jmp 连接。
所以我们修补函数块的方法就是:
1:对于回跳式的代码,如上面说得 jmp rdi,nop 修补后末尾补 retn。
2:对于调用函数式的代码,如上面的 jmp sub_8080720,nop 后把 jmp 改为 call。注意末尾不能填 retn,而是用 nop 直接打通与下一个代码块的中间部分,因为 r15 压入的下一条指令就是下一个代码块的地址,如果直接用 retn 来结尾的话就会照成函数被强行分割,且无法连接,这样 F5 反汇编时就会缺斤少两。
3:在摸索清楚修补规律后甚至可以直接使用一键式脚本:(代码出自 水番正文 师傅)
start = 0x807FEC0
end = 0x8080AD1
callopera=["lea","lea","mov","jmp"]
retnopera=["lea","mov","and","lea","jmp"]
def nop(s,e):
while(s<e): #注意这里并没有到e,以指令行为单位,只是到e的上一行而已
patch_byte(s,0x90)
s+=1
def callpatch(s,e,h):#这里的 h 是 call 后 retn 返回的地址,中间是冗余指令
nop(s,e) #一直 nop 到 jmp 地址处
patch_byte(e,0xE8) #然后把 jmp 的操作码改为 call
start=next_head(e) #然后再跳过 call 的那条指令,总不能覆盖吧。
end=h #h 是要返回的地址,是 mov 的操作数,中间的冗余指令要 nop 掉
nop(start,end)
def retnpatch(s,e):#这里直接全部填补后,末尾改个 c3 即可
nop(s,e) #先把到 e 前的都填补了,只剩最后一条指令行 jmp rdi
patch_byte(e,0x90) #jmp 和 rdi 都是单操作数,先填补 jmp
patch_byte(e+1,0xc3) #剩下的 rdi 改为 retn 操作码即可
tmp=[i for i in range(5)] #先设立一个数组列表,成员是数字型,不要 str 型,不然不好比较
while start<end: #从 start 到 end 慢慢逼近
tmp[0]=start
tmp[1]=next_head(tmp[0])
tmp[2]=next_head(tmp[1])
tmp[3]=next_head(tmp[2])
tmp[4]=next_head(tmp[3]) #都是获取一整行指令存储起来
for i in range(4):#先比较是否满足 call 的指令助记符
if print_insn_mnem(tmp[i]) != callopera[i]:
break #不满足就直接退出啦
else: #。。。。。。。这个原来是 for else 语句,排错了半天
callpatch(tmp[0],tmp[3],get_operand_value(tmp[1],1)) #call 的助记符只有四个,所以是 0~3,然后 retn 返回的操作数在第二个助记符的第二个操作数中,我们用 get_operand_value(tmp[1],1) 获取
start=next_head(tmp[3]) #如果执行到了 call 替换,那就直接跨过 call 替换后的指令行数,缩减空间。
continue #循环必用,跳过后面,提前进入下一个循环
for j in range(5): #retn 的替换指令占了 5 个助记符,5 行指令。
if print_insn_mnem(tmp[j]) != retnopera[j]:
break #没有就退出,没什么好说的
else: #。。。。。。。这个原来是 for else 语句,排错了半天
retnpatch(tmp[0],tmp[4]) #填补 5 行指令地址
start=next_head(tmp[4]) #如果执行到了 retn 替换,那就直接跨过 retn 替换后的指令行数,缩减空间。
continue #循环必用,跳过后面,提前进入下一个循环
start=next_head(start) #如果前面都没指令,那就跳到下一行
.
.
全都修补完后按 F5 键重新分析即可得到一个崭新的伪代码视图:
.
.
首先跟进 sub_8080720 函数中:
.
.
所以这一部分对应的代码逻辑可以写成如下:
(底子不好一定要照着 IDA 上的顺序仿写,不要顺便给代码化简,我一化简就调了一整天逻辑!!!)
#include<stdio.h>
#include<stdint.h>
#define ROL(x,y) ((x<<y) | (x>>(32-y))) //这是以 32 位为单位的循环左移
#define LeToBig(x) (((x & 0xff) << 8 *3) | (((x >> 8) & 0xff) << 2*8) | (((x >> 2*8) & 0xff ) << 8) | (x >> (8 * 3))) //小端转大端,即逆序,注意截取是已经移到最左边了啊!!!
uint32_t data[]={0x04050607, 0x00010203, 0x0C0D0E0F, 0x08090A0B, 0xCD3FE81B, 0xD7C45477, 0x9F3E9236, 0x0107F187, 0xF993CB81, 0xBF74166C, 0xDA198427, 0x1A05ABFF, 0x9307E5E4, 0xCB8B0E45, 0x306DF7F5, 0xAD300197, 0xAA86B056, 0x449263BA, 0x3FA4401B, 0x1E41F917, 0xC6CB1E7D, 0x18EB0D7A, 0xD4EC4800, 0xB486F92B, 0x8737F9F3, 0x765E3D25, 0xDB3D3537, 0xEE44552B, 0x11D0C94C, 0x9B605BCB, 0x903B98B3, 0x24C2EEA3, 0x896E10A2, 0x2247F0C0, 0xB84E5CAA, 0x8D2C04F0, 0x3BC7842C, 0x1A50D606, 0x49A1917C, 0x7E1CB50C, 0xFC27B826, 0x5FDDDFBC, 0xDE0FC404, 0xB2B30907};
void enc1(uint32_t* v)
{
unsigned int x = LeToBig(v[1]),y=LeToBig(v[0]),tmp; //x是输入8位中后四位,且倒序。y是输入8位中前四位,且倒序。
for(int i=0;i<=43;i++) //照着IDA的顺序来写啊!!!
{
tmp = y;
y = x ^ (ROL(y,1) & ROL(y,8)) ^ ROL(y,2) ^ data[i];
x = tmp;
}
v[1] = y;
v[0] = x; //末尾交换回顺序
}
.
.
接着分析下一个 sub_8080100 函数:
.
.
紧接着的 sub_807FEC0 函数是一个赋值函数:
.
.
最后的 sub_807FF20 是一个比较函数,比较 32 位:
.
.
识别完后我们把 sub_8080900 函数总的加密流程仿写出来:
仿写的过程中发现结果怎么都对不上,调试着发现是输入的转换错了,这是 “小端转大端存储算法” 与 “地址小端存放与正向” 的一个结合。
举例:
用户输入字符串数字如:‘1234’,被程序转成数字并转换类型成 DWORD 大小后就变成了 0x34333231。也就是说我们的字符串数字写入程序后被逆序了,通常程序会通过小端转大端存储算法把其变回正序的 0x31323334(sub_80802A0函数)。关键就是如果我们仿写加密逻辑传入 32 位数字数组时,要考虑其中的程序逆序,我们应该传入单个的 0x31 0x32 x033 0x34,或者整体但逆序的 0x34333231。
代码形式如下:
#include<stdio.h>
#include<stdint.h>
#define ROL(x,y) ((x<<y) | (x>>(32-y))) //这是以 32 位为单位的循环左移
#define LeToBig(x) (((x & 0xff) << 8 *3) | (((x >> 8) & 0xff) << 2*8) | (((x >> 2*8) & 0xff ) << 8) | (x >> (8 * 3))) //小端转大端,即逆序,注意截取是已经移到最左边了啊!!!
uint32_t data[]={0x04050607, 0x00010203, 0x0C0D0E0F, 0x08090A0B, 0xCD3FE81B, 0xD7C45477, 0x9F3E9236, 0x0107F187, 0xF993CB81, 0xBF74166C, 0xDA198427, 0x1A05ABFF, 0x9307E5E4, 0xCB8B0E45, 0x306DF7F5, 0xAD300197, 0xAA86B056, 0x449263BA, 0x3FA4401B, 0x1E41F917, 0xC6CB1E7D, 0x18EB0D7A, 0xD4EC4800, 0xB486F92B, 0x8737F9F3, 0x765E3D25, 0xDB3D3537, 0xEE44552B, 0x11D0C94C, 0x9B605BCB, 0x903B98B3, 0x24C2EEA3, 0x896E10A2, 0x2247F0C0, 0xB84E5CAA, 0x8D2C04F0, 0x3BC7842C, 0x1A50D606, 0x49A1917C, 0x7E1CB50C, 0xFC27B826, 0x5FDDDFBC, 0xDE0FC404, 0xB2B30907};
void enc1(uint32_t* v) //自己的
{
unsigned int x = LeToBig(v[1]),y=LeToBig(v[0]),tmp; //x是输入8位中后四位,且倒序。y是输入8位中前四位,且倒序。
for(int i=0;i<=43;i++)
{
tmp = y;
y = x ^ (ROL(y,1) & ROL(y,8)) ^ ROL(y,2) ^ data[i];
x = tmp;
}
v[1] = y;
v[0] = x; //末尾交换回顺序
}
void xteaenc(unsigned int num_rounds, uint32_t *v)
{
unsigned int i;
uint32_t const key[4]={0x3020100,0x7060504,0x0B0A0908,0x0F0E0D0C};
uint32_t v0=v[0],v1=v[1],sum=0,delta=0x10325476;
for(i=0;i<num_rounds;i++){
v0+=(((v1<<4)^(v1>>5))+v1)^(sum+key[sum&3]);
sum+=delta;
v1+=(((v0<<4)^(v0>>5))+v0)^(sum+key[(sum>>11)&3]);
}
v[0]=v0;v[1]=v1;
}
int main()
{
//input = "12345678123456781234567812345678"
//uint32_t v[]={0x31323334,0x35363738,0x31323334,0x35363738,0x31323334,0x35363738,0x31323334,0x35363738}; //这是错误的
uint32_t v[]={0x34333231,0x38373635,0x34333231,0x38373635,0x34333231,0x38373635,0x34333231,0x38373635}; //这里输入要逆序,理由上上面讲了
for(int i=0; i<=3;i++)
{
enc1(&v[2*i]);
xteaenc((1<<(i+1)),&v[2*i]);
printf("%x,%x\n",v[2*i],v[2*i+1]);
}
return 0;
}
.
.
然后编写解密代码:
恕我无能和菜,对于 enc1 这种交替赋值的逻辑逆向没有经验,我调了好久才成功跳出来其逆向逻辑 dec1,这种交替赋值的逻辑逆向以后一定要整理成一个专题出来才行,菜就多学习!!!
#include<stdio.h>
#include<stdint.h>
#define ROL(x,y) ((x<<y) | (x>>(32-y))) //这是以 32 位为单位的循环左移
#define LeToBig(x) (((x & 0xff) << 8 *3) | (((x >> 8) & 0xff) << 2*8) | (((x >> 2*8) & 0xff ) << 8) | (x >> (8 * 3))) //小端转大端,即逆序,注意截取是已经移到最左边了啊!!!
uint32_t data[]={0x04050607, 0x00010203, 0x0C0D0E0F, 0x08090A0B, 0xCD3FE81B, 0xD7C45477, 0x9F3E9236, 0x0107F187, 0xF993CB81, 0xBF74166C, 0xDA198427, 0x1A05ABFF, 0x9307E5E4, 0xCB8B0E45, 0x306DF7F5, 0xAD300197, 0xAA86B056, 0x449263BA, 0x3FA4401B, 0x1E41F917, 0xC6CB1E7D, 0x18EB0D7A, 0xD4EC4800, 0xB486F92B, 0x8737F9F3, 0x765E3D25, 0xDB3D3537, 0xEE44552B, 0x11D0C94C, 0x9B605BCB, 0x903B98B3, 0x24C2EEA3, 0x896E10A2, 0x2247F0C0, 0xB84E5CAA, 0x8D2C04F0, 0x3BC7842C, 0x1A50D606, 0x49A1917C, 0x7E1CB50C, 0xFC27B826, 0x5FDDDFBC, 0xDE0FC404, 0xB2B30907};
void dec1(uint32_t* v)
{
unsigned int x = v[0],y=v[1],tmp; //x是输入8位中后四位,且倒序。y是输入8位中前四位,且倒序。
for(int i=0;i<=43;i++) //交替赋值逻辑的交替逆向笔记注意
{
tmp = x;
x = y ^ (ROL(x,1) & ROL(x,8)) ^ ROL(x,2) ^ data[43-i];
y = tmp;
}
v[0] = LeToBig(y);
v[1] = LeToBig(x); //末尾交换回顺序
}
void xteadec(unsigned int num_rounds, uint32_t *v)
{
unsigned int i;
uint32_t v0=v[0],v1=v[1],delta=0x10325476,sum=delta*num_rounds;
uint32_t const key[4]={0x3020100,0x07060504,0x0B0A0908,0x0F0E0D0C};
for(i=0;i<num_rounds;i++){
v1-=(((v0<<4)^(v0>>5))+v0)^(sum+key[(sum>>11)&3]);
sum-=delta;
v0-=(((v1<<4)^(v1>>5))+v1)^(sum+key[sum&3]);
}
v[0]=v0;v[1]=v1;
}
int main()
{
uint32_t v[]={ 0xFDF5C266, 0x7A328286, 0xCE944004, 0x5DE08ADC, 0xA6E4BD0A, 0x16CAADDC, 0x13CD6F0C, 0x1A75D936};
for(int i=0; i<=3;i++)
{
xteadec((1<<(i+1)),&v[2*i]);
dec1(&v[2*i]);
}
char* flag = (char*)v; //有意思的 xtea 数字数组输出成字符类型,值得学习!!!
for (int i = 0; i < 32; i += 4)
{
printf("%c", flag[i]);
printf("%c", flag[i + 1]);
printf("%c", flag[i + 2]);
printf("%c", flag[i + 3]);
}
return 0;
}
.
.
.
最后的最后,我们回到最开始,试着从汇编语言的角度去梳理逻辑,学习那些硬刚汇编的大佬们的思路:
首先从 sub_8080900 函数已经 F5 出来的伪代码看起:
.
.
接下来跟进到 sub_8080720 函数的汇编代码中去:
.
.
接着我们进入 sub_8080100 函数的汇编代码:
.
.
sub_8080100 执行完后就跳转到 sub_807FEC0 函数中:
.
.
由此 sub_8080900 的循环体就结束了,最后跳转到 loc_807FF20 代码块中进行最后的比较:
.
.
最后 sub_8080900 函数就结束了,总的流程就是:
用户输入------>4次循环加密------>循环体内是 sub_8080720加密和sub_8080100加密------>最终比较------>得到 flag
.
.
.
.
写在最后:
这道题我复现了相当长的时间,从汇编梳理不出逻辑到巧得师傅指导用上了修补得到伪代码的方法,再到艰难啃下伪代码和艰难地仿写出加密解密 exp,最后反过来看汇编代码,真的一步一个脚印啊,被自己菜哭了。
哪怕最后照着伪代码梳理汇编逻辑也深感不易,如果没有伪代码的对照自己可能还是梳理不出来,直接被跳转绕晕了。。。
所以真的由衷佩服那些直接调汇编就能解出题来的师傅,真的强!!!!
最后写下这篇博客也算是自己这些天来硬刚的一个见证吧,菜就多学习。^ ~ ^
.
.
.
解毕!
敬礼!