- 前置知识
- 正文
- 一、UPX脱壳
- 二、IDA静态分析
- 三、编写逆向脚本,获取flag
前置知识
BUUCTF 新年快乐-学习笔记
正文
一、UPX脱壳
使用exeinfope检查文件是否有壳:
没有壳。
二、IDA静态分析
用IDA软件打开,shift+F12,CTRL+F搜索fl,点击进去看看:
CTRL+X查看谁调用了这个地址:
F5反编译一下,分析伪代码:
用AI 分析一下:
// 主函数 main_0// __cdecl:C 标准调用约定,参数从右往左入栈,调用者负责清理栈int__cdeclmain_0(intargc,constchar**argv,constchar**envp){size_tv3;// eax,保存字符串长度constchar*Source;// eax,保存处理后的字符串指针size_tMaxCount;// eax,保存最终比较的字符串长度charv7;// [esp+0h] [ebp-188h],临时字符变量,实际用于函数调用传参charv8;// [esp+0h] [ebp-188h],另一个临时字符变量,用于函数调用传参signedinti;// 循环变量 iintn0x64;// 循环变量 n0x64,用于初始化 Destination 数组signedintv11;// 保存 Destination 字符串长度charDestination[108];// 目标数组,用于存放变换后的用户输入charStr[28];// 用户输入缓冲区,最多读取 20 个字符_BYTE v14[8];// 8 字节临时数组,可能用于加密/编码函数的输出或上下文// ==============================// 第一部分:清空 Destination 数组// 将 Destination 的前 100 个字节置 0// ==============================for(n0x64=0;n0x64<100;++n0x64){// 边界检查:如果 n0x64 >= 100,报错if((unsignedint)n0x64>=0x64)j____report_rangecheckfailure();// Destination[n0x64] = 0// 清空数组,避免栈上垃圾数据影响后续逻辑Destination[n0x64]=0;}// ==============================// 第二部分:打印提示信息// sub_41132F("please enter the flag:", v7);// 这个函数大概率是 printf / puts 封装// 作用:输出 "please enter the flag:"// ==============================sub_41132F("please enter the flag:",v7);// ==============================// 第三部分:读取用户输入// sub_411375("%20s", (char)Str);// 这个函数大概率是 scanf / fgets 封装// %20s:最多读取 20 个字符,防止溢出// 用户输入会存入 Str 数组// ==============================sub_411375("%20s",(char)Str);/==============================// 第四部分:计算用户输入长度// ==============================v3=j_strlen(Str);// ==============================// 第五部分:调用 sub_4110BE 处理用户输入// Source = sub_4110BE(Str, v3, v14)// 参数:// Str:用户输入原始字符串// v3:字符串长度// v14:8 字节临时数组// 返回值:处理后的字符串指针// 这里 sub_4110BE 很可能是一个加密/编码/变换函数// ==============================Source=(constchar*)sub_4110BE(Str,v3,v14);// ==============================// 第六部分:将处理后的字符串拷贝到 Destination// strncpy(Destination, Source, 0x28u)// 0x28 = 40// 最多拷贝 40 个字符// ==============================strncpy(Destination,Source,0x28u);// ==============================// 第七部分:计算 Destination 的长度// ==============================v11=j_strlen(Destination)// ==============================// 第八部分:对 Destination 做逐字符变换// 第 0 个字符 += 0// 第 1 个字符 += 1// 第 2 个字符 += 2// ...// 第 i 个字符 += i// ==============================for(i=0;i<v11;++i)Destination[i]+=i;// ==============================// 第九部分:再次计算 Destination 长度// MaxCount = strlen(Destination)// ==============================MaxCount=j_strlen(Destination);// ==============================// 第十部分:与目标字符串 Str2 比较// if (!strncmp(Destination, Str2, MaxCount))// Str2 = "e3nifIH9b_C@n@dH"// 如果变换后的 Destination 等于 Str2,输出正确// ==============================if(!strncmp(Destination,Str2,MaxCount))sub_41132F("rigth flag!\n",v8);elsesub_41132F("wrong flag!\n",v8);return0;}所以这个程序的逻辑是:
用户输入 Str ->sub_4110BE 处理,传给Source ->拷贝到 Destination ->Destination[i]+=i ->与"e3nifIH9b_C@n@dH"比较所以,这道题的逆向流程就是:
目标密文"e3nifIH9b_C@n@dH"->每个字符 - i ->得到 Source ->再逆向 sub_4110BE ->得到原始 flag所以重点看sub_4110BE函数 ,双击一下,进去看看:
进去发现sub_4110BE只是个跳板函数,因为第一行注释// attributes: thunk是 IDA 自动标记,代表这是跳板中转函数:
- 本身没有任何加密运算逻辑,不处理数据;
- 功能只有一件事:把收到的所有参数原封不动转发给
sub_411AB0,并且把sub_411AB0的返回值直接向上返回。
等价通俗理解:
sub_4110BE = 传话员,自己不干活,直接把全部参数交给真正干活的
sub_411AB0
在之前的代码里,
之前我们猜测sub_4110BE是加密函数,现在证实:
✅真正的加密 / 字符串变换逻辑全部在sub_411AB0内部!
整条调用链简化:
main_0 → sub_4110BE(跳板) → sub_411AB0(真实算法)双击进入sub_411AB0函数,复行数十步,豁然开朗:
扔给AI分析,告诉你这是base64加密函数核心:
// Base64编码核心函数// p_Str:原始输入字符串指针// a2:原始字符串长度// a3:外部传入的8字节数组,函数内部把输出长度写入*a3void*__cdeclsub_411AB0(char*p_Str,size_ta2,_BYTE*a3){intv4;// [esp+D4h] [ebp-38h] 输出缓冲区下标,记录当前写到第几个字节intv5;// [esp+D4h] [ebp-38h] case1临时下标副本intv6;// [esp+D4h] [ebp-38h] case2临时下标副本intv7;// [esp+D4h] [ebp-38h] case3临时下标副本intn3;// [esp+E0h] [ebp-2Ch] 本轮循环读取到的原始字节数量(最多3字节)size_tv9;// [esp+ECh] [ebp-20h] 原始字节分组数,每3字节一组intv10;// [esp+ECh] [ebp-20h] Base64编码后的输出长度(4字节每组)intv11;// [esp+ECh] [ebp-20h] 剩余待处理原始字节数量void*v12;// [esp+F8h] [ebp-14h] malloc分配的Base64输出缓冲区char*p_Str_1;// [esp+104h] [ebp-8h] 原始字符串遍历指针// 合法性判断:空指针 或 输入长度为0,直接返回NULLif(!p_Str||!a2)return0;// ====== 计算Base64输出需要多大内存 ======v9=a2/3;// 完整3字节组数量if((int)(a2/3)%3)// 存在不足3字节的剩余数据++v9;v10=4*v9;// Base64规则:每组3字节原始数据编码为4字节*(_DWORD*)a3=v10;// 将最终编码长度写入外部传入的a3缓冲区v12=malloc(v10+1);// 分配内存,+1预留末尾'\0'字符串结束符if(!v12)// 内存分配失败,返回0return0;j_memset(v12,0,v10+1);// 将缓冲区全部清零p_Str_1=p_Str;// 遍历指针指向原始字符串起始v11=a2;// 剩余字节总数初始化为输入长度v4=0;// 输出缓冲区写入位置,从0开始// 循环处理所有原始字节,每次最多取出3字节while(v11>0){// algn_41A145、byte_41A144 是IDA全局数组// byte_41A144[0/1/2]:存放本轮取出最多3个原始字节// algn_41A145 是字节移位临时缓存(IDA反编译变量命名优化缺陷)algn_41A145[1]=0;algn_41A145[0]=0;byte_41A144[0]=0;// 循环读取最多3字节原始数据for(n3=0;n3<3&&v11>=1;++n3){byte_41A144[n3]=*p_Str_1;// 读取1字节存入缓存--v11;// 剩余字节-1++p_Str_1;// 原始指针向后移动}if(!n3)// 没有读到任何字节,退出循环break;// 根据本轮读到字节数量n3,分3种Base64编码情况switch(n3){// 场景1:只读到1个原始字节case1:// 第1个Base64字符:取原始字节高6bit*((_BYTE*)v12+v4)=aAbcdefghijklmn[(int)(unsigned__int8)byte_41A144[0]>>2];v5=v4+1;// 第2个Base64字符:原始字节低2bit + 填充0bit*((_BYTE*)v12+v5)=aAbcdefghijklmn[((algn_41A145[0]&0xF0)>>4)|(16*(byte_41A144[0]&3))];// 后两个字符填充 = (下标64)*((_BYTE*)v12+++v5)=aAbcdefghijklmn[64];*((_BYTE*)v12+++v5)=aAbcdefghijklmn[64];v4=v5+1;// 更新输出下标break;// 场景2:读到2个原始字节case2:*((_BYTE*)v12+v4)=aAbcdefghijklmn[(int)(unsigned__int8)byte_41A144[0]>>2];v6=v4+1;*((_BYTE*)v12+v6)=aAbcdefghijklmn[((algn_41A145[0]&0xF0)>>4)|(16*(byte_41A144[0]&3))];*((_BYTE*)v12+++v6)=aAbcdefghijklmn[((algn_41A145[1]&0xC0)>>6)|(4*(algn_41A145[0]&0xF))];// 末尾1个填充 =*((_BYTE*)v12+++v6)=aAbcdefghijklmn[64];v4=v6+1;break;// 场景3:完整读到3个原始字节(标准分组,无填充符)case3:*((_BYTE*)v12+v4)=aAbcdefghijklmn[(int)(unsigned__int8)byte_41A144[0]>>2];v7=v4+1;*((_BYTE*)v12+v7)=aAbcdefghijklmn[((algn_41A145[0]&0xF0)>>4)|(16*(byte_41A144[0]&3))];*((_BYTE*)v12+++v7)=aAbcdefghijklmn[((algn_41A145[1]&0xC0)>>6)|(4*(algn_41A145[0]&0xF))];*((_BYTE*)v12+++v7)=aAbcdefghijklmn[algn_41A145[1]&0x3F];v4=v7+1;break;}}*((_BYTE*)v12+v4)=0;// 在编码结果末尾写入字符串结束符'\0'returnv12;// 返回Base64编码后的字符串指针(即main中的Source)}三、编写逆向脚本,获取flag
设密文 Cipher = “e3nifIH9b_C@n@dH”
第一步逆运算:抵消 Destination[i] += i
第二步逆运算:Base64 解码 Source
解码结果就是最终 flag
importbase64 cipher=b"e3nifIH9b_C@n@dH"source=bytearray()foridx,chinenumerate(cipher):source.append(ch-idx)b64_str=source.decode("ascii")print("正确Base64字符串:",b64_str)flag=base64.b64decode(b64_str)print("flag =",flag.decode())