[GFCTF] 2021wordy

2023.9.23

本题关键是用脚本解决一系列的花指令(数量过多需要使用IDA Python来写脚本)

发现程序中有花指令jmp,将其patch为nop(0x90):

1
2
3
4
5
6
7
start_adr = 0x1151
end_adr = 0x3100
# 因为尝试patch一个后发现后面有大批量的花指令,所以编写脚本
for i in range(start_adr,end_adr):
if get_wide_byte(i) == 0xeb:
if get_wide_byte(i+1) == 0xff:
patch_byte(i,0x90)

然后发现是一系列putchar(),同样写脚本将输出的字符提取出来:

1
2
3
4
5
start_adr = 0x1151
end_adr = 0x3100
for i in range(start_adr,end_adr):
if get_wide_byte(i) == 0xc0:
print(chr(get_wide_byte(i+2)),end='')

运行结果:

1
2
3
4
5
6
7
ello world!
There are moments in life when you miss someone so much that you just want to pick them from your dreams and hug them for real! Dream what you want to dream;go where you want to go;be what you want to be,becÿause you have only one life and one chance to do all the things you want to do.
May you have enough happiness to make you sweet,enough trials to make you strong,enough sorrow to keep you human,enough hope to make you happy? Always put yourself in others'shÿoes.If you feel that it hurts you,it probably hurts the other person, too.

GFÿCTF{u_are2wordy}
You find Flag, Congratulation!
You didn't find Flag

[HZNUCTF 2023 final]虽然他送了我玫瑰花

2023.9.28

花指令patch

本题的main函数有一处(还是两处;来着)花指令,同样把jz+jnz后面的脏字节patch为0x90,即nop指令;

然后在nop处按快捷键c识别为代码;

最后光标指示到main函数的开头,按快捷键p重新生成函数,看到(可能?)生成了一些变量的初始化代码后说明成功,此时就可以正常生成main函数的C伪代码了.

image-20230928142524335

image-20230928142642131

image-20230928142712825

image-20230928142801458

最后记得patch后要同步保存一下,这里不知道IDA怎么搞的,反正我为了防止出问题,先从patch菜单中点击保存到输入文件(原来的文件):

image-20230928142930135

然后保存,最后重启IDA:

image-20230928143027704

这样就完成了.

最终的main伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // esi
int v4; // eax
char v6; // [esp+0h] [ebp-F8h]
char v7; // [esp+0h] [ebp-F8h]
char v8[100]; // [esp+Ch] [ebp-ECh]
__int128 v9; // [esp+70h] [ebp-88h]
int v10; // [esp+80h] [ebp-78h]
int v11; // [esp+84h] [ebp-74h]
int v12; // [esp+88h] [ebp-70h]
char v13; // [esp+8Ch] [ebp-6Ch]
char Arglist[100]; // [esp+90h] [ebp-68h] BYREF

sub_401020((char *)&Format, v6);
sub_401050("%s", (char)Arglist);
v10 = -171171450;
v11 = -669748952;
v12 = 1651994351;
v13 = -6;
v9 = xmmword_402170;
if ( strlen(Arglist) == 29 )
{
for ( i = 0; i < 29; ++i )
v8[i] = funcs_40117E[i % 5u](Arglist[i]);
v4 = 0;
while ( v8[v4] == *((_BYTE *)&v9 + v4) )
{
if ( ++v4 >= 29 )
{
sub_401020("Congratulations!!\n", v7);
return 0;
}
}
sub_401020("try again\n", v7);
}
else
{
sub_401020("wwwhhhaaattt???\n", v7);
}
return 0;
}

分析代码

很容易知道flag长度为29,我们构造一个长29的字符串备用:

1
2
3
4
5
6
7
#include <stdio.h>
int main() {
for(int i=0;i<29;++i)
putchar('a');
//aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
return 0;
}

我们从伪代码中发现这段关键代码,使用了一个函数指针数组对每一位进行不同的加密变换:

1
2
for ( i = 0; i < 29; ++i )
v8[i] = funcs_40117E[i % 5u](Arglist[i]);

由于只有5个函数,我们直接把他们提取出来—点击funcs_40117E到内存,跳转到对应的五个函数,稍加修改备用:

image-20230928143211657

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int __cdecl f0(int a1) {
//printf("call f0\n");
return a1 ^ 0x19;
}
int __cdecl f1(int a1) {
//printf("call f1\n");
return a1 + 18;
}
int __cdecl f2(int a1) {
//printf("call f2\n");
return a1 - 16;
}
int __cdecl f3(int a1) {
//printf("call f3\n");
return 2 * (a1 & 0x7F);
}
int __cdecl f4(int a1) {
//printf("call f4\n");
return a1 ^ ((unsigned char)a1 ^ (unsigned char)~(unsigned char)a1) & 0x80;
}
int main(){
int (*func_p[5])(int a1) = {
f0, f1, f2, f3, f4
};
//...
return 0;
}

这时候就只剩下提取出v9对应的数组(IDA分析将其错误分析成一个__int128,三个int和一个char的组合了)中的加密后的flag了,我们动态调试,然后用IDA Python提取29个字节,这里多提取了几次,最后提取出来的没有0x0的才是正确的(虽然我不知道怎么回事).

貌似要在正式开始和v9比较时数据才写回内存(?)总之把断点位置从22行if ( strlen(Arglist) == 29 )放到后面27行的while ( v8[v4] == *((_BYTE *)&v9 + v4) )才最后成功.

这也意味着我们动调是需要输入正确的长为29的字符串,我们已经生成好了.

image-20230928143705669 image-20230928143717471

我们双击v9,找到其地址,写脚本导出即可:

image-20230928143825128

脚本:

1
2
3
4
5
adr=0x0036FD68
for i in range(29):
temp=get_bytes(adr+i,1)
print(hex(int.from_bytes(temp,byteorder='little')),end=',')
#print(temp,end=',')*/

最后把提取出来的数据进行爆破解密即可:

爆破脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
int __cdecl f0(int a1) {
return a1 ^ 0x19;
}
int __cdecl f1(int a1) {
return a1 + 18;
}
int __cdecl f2(int a1) {
return a1 - 16;
}
int __cdecl f3(int a1) {
return 2 * (a1 & 0x7F);
}
int __cdecl f4(int a1) {
return a1 ^ ((unsigned char)a1 ^ (unsigned char)~(unsigned char)a1) & 0x80;
}



int main() {
unsigned char str1[] = {
0x7f, 0x7e, 0x51, 0xce, 0xfb, 0x4e, 0x7a, 0x24, 0xe8, 0xdf, 0x59, 0x71, 0x26, 0xca, 0xe1, 0x6c, 0x86, 0x21, 0xcc, 0xf5, 0x28, 0x71, 0x14, 0xd8, 0xef, 0x6e, 0x77, 0x62, 0xfa
};
// printf("%d",sizeof(str1) / sizeof(str1[0]));

int (*func_p[5])(int a1) = {
f0, f1, f2, f3, f4
};

for (int i = 0; i < 29; ++i) {
for (int c = 32; c <= 126; ++c) {
if (func_p[i % 5](c) == str1[i]) {
putchar(c);
break;
}
}
}
return 0;
}
/*
adr=0x00EFFA1C
for i in range(29):
temp=get_bytes(adr+i,1)
print(hex(int.from_bytes(temp,byteorder='little')),end=',')
#print(temp,end=',')*/

最终结果:

image-20230928144001030

结束…

NSSRound#3 Team]jump_by_jump

直接将jz和jnz下面识别出的call指令的首字节E8修改成90(nop指令),然后将后面转换出来的数据重新分析成指令,然后最后在main的开头重新生成函数即可还原函数伪代码.

问题:我的IDA莫名其妙patch为nop后,新生成的数据无法分析为数据,来回辗转好机会才成功.猜测可能是重新分析成代码时,选中的数据要包含有patch出来的nop.

patch掉脏字节↓

image-20230925014252908

选中包含nop在内的那堆数据,进行analyze,然后就会生成正确的汇编代码,此时在_main_0的开头重新生成函数即可↓

image-20230925014609287

修复完成,此时TAB键即可生成伪代码↓

image-20230925014215010 image-20230925014904236