第一次国赛。
# Asm_re
题目给了一个文本文件包含 arm 汇编 (是 ida 的片段),大部分 re 手都只会 x86 汇编,arm 汇编很少接触。
下面给出两种办法获得伪 c 片段
# 方法一 CHATGPT
#include <stdio.h> | |
#include <string.h> | |
// 假设一个类似的全局变量 | |
extern char __stack_chk_guard_ptr; | |
extern char unk_100003F10; | |
extern void __chkstk_darwin(); | |
void main() { | |
// 栈变量 | |
char stack_guard; | |
char var_18; | |
char var_B4; | |
int index=0; | |
char flag[] = "flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}"; | |
char var_C0[128]; | |
char lena; | |
char var_D0[128]; | |
char var_8[128]; | |
char var_100[128]; | |
char var_D8[128]; | |
char var_DC[128]; | |
char var_E0[128]; | |
char var_E4[128]; | |
char var_E8[128]; | |
char var_EC[128]; | |
char var_F0[128]; | |
char var_F4[128]; | |
char var_F8[128]; | |
// 初始化 | |
memcpy(var_C0, unk_100003F10, 0x98); | |
size_t len = strlen(flag); | |
lena = len + 1; | |
// 主循环 | |
while (index < lena) { | |
char c = flag[index]; | |
c = (c * 'P' + 0x14) ^ 'M'; | |
c += 0x1E; | |
var_100[index] = c; | |
index++; | |
} | |
// 打印换行 | |
printf("\n"); | |
var_F4[0] = 1; | |
var_F8[0] = 0; | |
// 另一段主循环 | |
while (var_F8[0] < lena) { | |
int comparison_result = var_100[var_F8[0]] == var_8[var_F8[0]]; | |
if (!comparison_result) { | |
var_F4[0] = 0; | |
break; | |
} | |
var_F8[0]++; | |
} | |
// 结果检查 | |
if (var_F4[0] == 1) { | |
printf("Success\n"); | |
} else { | |
printf("Failure\n"); | |
} | |
} |
用 gpt4o 可以得到比较完善的代码 (gpt3.5 的话就有点烂了)
# 方法二 提取十六进制数据
__text:0000000100003BBC FC 6F BE A9 STP X28, X27, [SP,#-0x10+var_10]! | |
__text:0000000100003BC0 FD 7B 01 A9 STP X29, X30, [SP,#0x10+var_s0] | |
__text:0000000100003BC4 FD 43 00 91 ADD X29, SP, #0x10 | |
__text:0000000100003BC8 FF 03 04 D1 SUB SP, SP, #0x100 |
可以看出字节数据都是整齐偏移的
用 vscode 打开文本文件,shif+alt 键进行矩形选择
得到这样的数据
FC 6F BE A9
FD 7B 01 A9
FD 43 00 91
FF 03 04 D1
08 00 00 B0
打开 010 编辑器
新建文件 -> 编辑 -> 粘贴自 16 进制文本 -> 保存到桌面
用 ida 打开文件,选择汇编语言类型,小端序 ARM。
可以看到汇编代码,按 p 键创建函数,F5 即可看到 c 语言代码。
# 解密
有了伪 c 代码解密就和洒洒水一样
#include <stdio.h> | |
int main(){ | |
char flag[] = "flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}"; | |
char mm1[]={0xd7,0x1f,0x00,0x00,0xb7,0x21,0x00,0x00,0x47,0x1e,0x00,0x00,0x27,0x20,0x00,0x00,0xe7,0x26,0x00,0x00,0xd7,0x10,0x00,0x00,0x27,0x11,0x00,0x00,0x07,0x20,0x00,0x00,0xc7,0x11,0x00,0x00,0x47,0x1e,0x00,0x00,0x17,0x10,0x00,0x00,0x17,0x10,0x00,0x00,0xf7,0x11,0x00,0x00,0x07,0x20,0x00,0x00,0x37,0x10,0x00,0x00,0x07,0x11,0x00,0x00,0x17,0x1f,0x00,0x00,0xd7,0x10,0x00,0x00,0x17,0x10,0x00,0x00,0x17,0x10,0x00,0x00,0x67,0x1f,0x00,0x00,0x17,0x10,0x00,0x00,0xc7,0x11,0x00,0x00,0xc7,0x11,0x00,0x00,0x17,0x10,0x00,0x00,0xd7,0x1f,0x00,0x00,0x17,0x1f,0x00,0x00,0x07,0x11,0x00,0x00,0x47,0x0f,0x00,0x00,0x27,0x11,0x00,0x00,0x37,0x10,0x00,0x00,0x47,0x1e,0x00,0x00,0x37,0x10,0x00,0x00,0xd7,0x1f,0x00,0x00,0x07,0x11,0x00,0x00,0xd7,0x1f,0x00,0x00,0x07,0x11,0x00,0x00,0x87,0x27,0x00,0x00}; | |
int *mm=mm1; | |
int index=0; | |
size_t len = strlen(flag); | |
int lena = len + 1; | |
while (index < lena){ | |
mm[index]-=0x1e; | |
mm[index]^='M'; | |
mm[index] =(mm[index]-0x14)/'P';; | |
flag[index]=mm[index]; | |
index++; | |
} | |
puts(flag); | |
} |
# Rust_baby
像这种用那些奇葩语言写的二进制文件靠静态分析是很难做出来的
动态调试寻找函数
一直 f8
步过,多窗口观察终端有没有输出什么信息
发现在 sub_7FF7D72B298A
的地方程序输出 where is your flag?:
我们由此确定主逻辑在这里。
函数有里有发现明显的长 base64 字符串
拿去解密得到
{ | |
"henhenaaa!":[1,1,4,5,1,4,1,9,1,9,8,1,0], | |
"cryforhelp":"igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc=", | |
"whatadoor":"1145141919810WTF", | |
"iwantovisit":"O0PSwantf1agnow1" | |
}{ | |
"where":"where is your flag?:", | |
"omg":"correct flag", | |
"nonono":"nope, wrong flag" | |
} |
这很明显是和加密有关的重要数据。
动调找到输出函数输入函数
从这一段到函数开头有很长一串代码,我们就不用去管它了,这种题只需要关注代码对数据的加密,不然很容易就被绕昏了。
输入函数后面下好断点,在终端输入数据 flagggggggggggg 然后再在输入函数周围寻找对这段字符串的引用,找到引用后观察对输入字符处理
do | |
{ | |
*(_QWORD *)&flagg[0] = 0i64; | |
for ( j = 0i64; j != 8; ++j ) | |
{ | |
v26 = 'E'; | |
if ( v23 + j < v20 ) | |
v26 = flagggggggg[j]; | |
*((_BYTE *)flagg + j) = v26; | |
} | |
v197[0] = (LPVOID)sub_7FF7D72B28C3( | |
*(__int64 *)&flagg[0], | |
byte_7FF7D72D2978[(unsigned __int8)(v24 - 3 * (((unsigned __int8)v24 / 3u) & 0xFE))], | |
*((_QWORD *)v188 + v24)); | |
for ( k = 0i64; k != 8; ++k ) | |
flagm1[k] = *((_BYTE *)v197 + k) ^ 0x33; | |
++v24; | |
flagggggggg += 8; | |
v23 += 8i64; | |
flagm1 += 8; | |
} | |
while ( v24 != 13 ); |
前段是把 flag 每 8 组复制一次,剩下的补 'E'
中间是个加密函数,先跳过
后面是一个异或
flagm1 存储最后的数据,可是对它的调用在 +=8
后就没有了。我们不能得知它在哪又被处理了。
这个时候需要妙用硬件断点分析,对加密后的前几个字节下断点,硬件断点可以下在 stack,监听数据的读写,读写时提示触发代码
Stack[000008D8]:000000DC9FCFEA50 db 56h ; V <-breakpoint
Stack[000008D8]:000000DC9FCFEA51 db 58h ; X
Stack[000008D8]:000000DC9FCFEA52 db 52h ; R
Stack[000008D8]:000000DC9FCFEA53 db 54h ; T
Stack[000008D8]:000000DC9FCFEA54 db 5Bh ; [
Stack[000008D8]:000000DC9FCFEA55 db 5Bh ; [
当触发断点的时候,我们已经从伪 c 代码的 369 行跳转到 634 行,这几百行看起来都极难分析,我们略过,直接观察对加密数据的处理
do | |
{ | |
v93[v97] = v96[v97] ^ *(_BYTE *)(v91 + v97); | |
++v97; | |
} | |
while ( v88 != v97 ); |
可以看出,v91 是 flag 加密后的数据,只是个简单的对表异或,这个异或表,可以经过多次调试推断,是个固定的表
debug027:0000025E67C42FA0 db 0DCh
debug027:0000025E67C42FA1 db 5Fh ; _
debug027:0000025E67C42FA2 db 20h
debug027:0000025E67C42FA3 db 22h ; "
debug027:0000025E67C42FA4 db 0C2h
debug027:0000025E67C42FA5 db 79h ; y
debug027:0000025E67C42FA6 db 19h
debug027:0000025E67C42FA7 db 56h ; V
debug027:0000025E67C42FA8 db 35h ; 5
debug027:0000025E67C42FA9 db 0DAh
debug027:0000025E67C42FAA db 8Bh
debug027:0000025E67C42FAB db 47h ; G
debug027:0000025E67C42FAC db 0D3h
debug027:0000025E67C42FAD db 19h
debug027:0000025E67C42FAE db 0FCh
debug027:0000025E67C42FAF db 55h ; U
debug027:0000025E67C42FB0 db 0ABh
debug027:0000025E67C42FB1 db 0ABh
debug027:0000025E67C42FB2 db 0ABh
debug027:0000025E67C42FB3 db 0ABh
再次对异或后的数据下硬件断点
do | |
v101[v90++] = *(_BYTE *)(v99 + v102++); | |
while ( v100 != v102 ); |
触发断点的是一个数据复制操作
继续对复制后的数据位置下断点
v112 = _byteswap_uint64(*(_QWORD *)&v101[v109]); | |
*(_BYTE *)(v106 + v108 - 32) = byte_7FF7D72D25F8[(v112 >> 58) + 3]; | |
*(_BYTE *)(v106 + v108 - 31) = byte_7FF7D72D25F8[((v112 >> 52) & 0x3F) + 3]; | |
*(_BYTE *)(v106 + v108 - 30) = byte_7FF7D72D25F8[((v112 >> 46) & 0x3F) + 3]; |
这次触发的是一大堆取表操作,数据被转化到 v106
我们对 v106 硬件断点,继续探索逻辑
v86 = v135[*((_QWORD *)&Src[0] + 1)] == v135[v106];
++v135;
if ( !v86 )
{
v136 = sub_7FF7D72C426C("nonono", 6ui64, (__int64)v174);
我们这次直接定位到一个循环的判断代码片段里,这里应该是判断密文的地方了,左边的 src 也确实藏着一个开头得到的密文数据,同时我们点击 v106,发现里面的数据全是可见字符,并且最后一个字符是 '=’, 结合代码特征可知是 base64 加密。
整体分析到此结束,我们来梳理一下:
flag分组补'E'->每组到sub_7FF7D72B28C3加密->异或0x33->异或一个固定的表->base64
现在逻辑再清晰不过了,我们只差 sub_7FF7D72B28C3 这个加密就可得出 flag
这里我引用别人的解法:调试找规律
Stack[000011E8]:000000A8A00FE5A8 offset db 66h ; f | |
Stack[000011E8]:000000A8A00FE5A9 db 66h ; f | |
Stack[000011E8]:000000A8A00FE5AA db 67h ; g | |
Stack[000011E8]:000000A8A00FE5AB db 67h ; g | |
Stack[000011E8]:000000A8A00FE5AC db 68h ; h | |
Stack[000011E8]:000000A8A00FE5AD db 68h ; h | |
Stack[000011E8]:000000A8A00FE5AE db 69h ; i | |
Stack[000011E8]:000000A8A00FE5AF db 69h ; i | |
Stack[000011E8]:000000A8A00FE5B0 flag db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B1 db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B2 db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B3 db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B4 db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B5 db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B6 db 67h ; g | |
Stack[000011E8]:000000A8A00FE5B7 db 67h ; g |
delta = -1 | |
input+= delta | |
if i % 2 =1 | |
delta -= 1 |
简单的一个加减运算,可是看起来却非常难懂,
可知我们解 ctf 题有时也需要一些取巧的技巧,
唉,感觉自己修为还不够,学长调几次就看出了这个伪加密,道阻且长。
tips: 这个函数也可以利用硬件断点来分析,可以分析出flag只在最后的循环语句中被读取,并且只执行了简单的加减操作,以前没发现硬件断点有如此妙用,如今是领教到了
以下是解密脚本
#include <stdio.h> | |
void decodeBase64(char* str,int len,char** in){ | |
char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
char ascill[129]; | |
int k = 0; | |
for(int i=0;i<64;i++){ | |
ascill[base64[i]] = k++; | |
} | |
int decodeStrlen = len / 4 * 3 + 1; | |
char* decodeStr = (char*)malloc(sizeof(char)*decodeStrlen); | |
k = 0; | |
for(int i=0;i<len;i++){ | |
decodeStr[k++] = (ascill[str[i]] << 2) | (ascill[str[++i]] >> 4); | |
if(str[i+1] == '='){ | |
break; | |
} | |
decodeStr[k++] = (ascill[str[i]] << 4) | (ascill[str[++i]] >> 2); | |
if(str[i+1] == '='){ | |
break; | |
} | |
decodeStr[k++] = (ascill[str[i]] << 6) | (ascill[str[++i]]); | |
} | |
decodeStr[k] = '\0'; | |
*in = decodeStr; | |
} | |
int main(){ | |
unsigned char xortable[]={0xdc,0x5f,0x20,0x22,0xc2,0x79,0x19,0x56,0x35,0xda,0x8b,0x47,0xd3,0x19,0xfc,0x55,0x14,0xcd,0xd2,0x7b,0x58,0x59,0x09,0x42,0xde,0x2c,0xb4,0x48,0xd9,0xf2,0x1b,0xa9,0x40,0xe1,0xa6,0xfb,0xff,0x38,0xc1,0xd5,0xe2,0xe8,0x77,0x78,0x6f,0x22,0x04,0xe6,0x16,0x3e,0x0c,0x35,0x27,0x29,0x89,0xb5,0x92,0x2e,0x6a,0xa6,0xdb,0x2f,0xc6,0xa9,0x6e,0x8f,0x34,0x90,0x59,0xfc,0x2d,0x91,0x66,0xeb,0xbe,0x0d,0xf4,0x05,0x0b,0x1b,0xcb,0x18,0x74,0xf9,0x82,0x81,0xbc,0x04,0xd9,0x75,0x8e,0x2d,0x97,0x07,0x7c,0x7d,0x18,0xc8,0x3d,0x4f,0xc0,0xa5,0x6a,0xd7}; | |
unsigned char *mm2; | |
decodeBase64(mm1,strlen(mm1),&mm2); | |
puts(mm2); | |
for(int i=0;i<strlen(mm1) / 4 * 3 + 1;i++){ | |
mm2[i]^=(unsigned char)xortable[i]; | |
mm2[i]^=0x33; | |
} | |
puts(mm2); | |
for(int i=0;i<strlen(mm2);i+=8){ | |
mm2[i]+=1; | |
mm2[i+1]+=1; | |
mm2[i+4]-=1; | |
mm2[i+5]-=1; | |
mm2[i+6]-=2; | |
mm2[i+7]-=2; | |
} | |
puts(mm2); | |
} |