September 18, 2022

强网杯2022 初赛Reverse 复现

终于开始缓慢复现强pwn杯了

easyre

这题我真的是气死了啊啊啊啊啊啊浪费我一天时间!!!!!!!

初步分析

先finger一把梭了恢复符号表。

主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // r8
__int64 v4; // r9
int v5; // er8
int v6; // er9
__int64 v8; // [rsp+0h] [rbp-20h]
unsigned int v9; // [rsp+1Ch] [rbp-4h]

sub_402150();
v9 = fork();
if ( v9 )
{
sub_401F2F(v9);
remove("re3");
}
else
{
sys_ptrace(0LL, 0LL, 0LL, 0LL, v3, v4, (char)argv);
sub_44B680((unsigned int)"re3", (unsigned int)"re3", *(_QWORD *)(v8 + 8), 0, v5, v6, v8);
}
return 0;
}

sub_402150写了一个叫re3的文件,然后fork一个线程。父进程进入sub_401F2F运行re3,结束后remove掉re3,子进程接受父进程的ptrace

sub_401F2F:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
__int64 __fastcall sub_401F2F(unsigned int a1)
{
__int64 v1; // r8
__int64 v2; // r9
unsigned int v3; // ecx
__int64 v4; // rdx
__int64 result; // rax
__int64 v6; // r8
__int64 v7; // r9
__int64 v8; // r8
__int64 v9; // r9
__int64 v10; // r8
__int64 v11; // r9
__int64 v12; // r8
__int64 v13; // r9
__int64 v14; // r8
__int64 v15; // r9
char v16; // [rsp+0h] [rbp-130h]
char v17; // [rsp+0h] [rbp-130h]
char v18; // [rsp+0h] [rbp-130h]
char v19; // [rsp+0h] [rbp-130h]
unsigned int v20; // [rsp+10h] [rbp-120h]
__int64 v21; // [rsp+18h] [rbp-118h]
int v22; // [rsp+2Ch] [rbp-104h] BYREF
struct user_regs_struct v23; // [rsp+30h] [rbp-100h] BYREF
unsigned __int64 v24; // [rsp+108h] [rbp-28h]
__int64 v25; // [rsp+110h] [rbp-20h]
__int64 v26; // [rsp+118h] [rbp-18h]
int i; // [rsp+124h] [rbp-Ch]
__int64 v28; // [rsp+128h] [rbp-8h]

sys_wait(a1, 0LL, 0);
sys_ptrace(PTRACE_CONT, a1, 0LL, 0LL, v1, v2, v16);
v26 = sub_401A30(a1);
v3 = sub_4017E5();
result = v4;
v20 = v3;
v21 = v4;
for ( i = 0; i <= 2; ++i )
{
v22 = 0;
sys_wait(a1, &v22, 0);
result = v22 & 0x7F;
if ( (v22 & 0x7F) == 0 )
break;
sys_ptrace(PTRACE_GETREGS, a1, 0LL, (__int64)&v23, v6, v7, v17);
v25 = v23.rip - 1;
v24 = sys_ptrace(PTRACE_PEEKTEXT, a1, v23.rip - 1, 0LL, v8, v9, v18);
if ( v24 == 0xCAFEB055BFCCLL )
{
v28 = v25;
SMC(a1, v25, v26, v20, v21);
v23.rip = v25 + 10;
sys_ptrace(PTRACE_SETREGS, a1, 0LL, (__int64)&v23, v12, v13, v19);
}
else if ( v24 == 0xCAFE1055BFCCLL )
{
lizaiganshenmo(a1, v28, v25, v26, v20, v21);
v23.rip = v25 + 10;
sys_ptrace(PTRACE_SETREGS, a1, 0LL, (__int64)&v23, v14, v15, v19);
}
result = sys_ptrace(PTRACE_CONT, a1, 0LL, 0LL, v10, v11, v19);
}
return result;
}

sub_4017E5取了一堆抽象的数据。起子进程后等待子进程运行,wait子进程发送信号,为0就退出for循环。不为0的话就保存子进程的寄存器,然后去判断if-else(就是一个子进程命中异常后把控制权交给父进程,父进程保存子进程上下文,处理完异常后恢复子进程上下文,此后继续运行子进程的过程)。

依据从RIP-1处取到的数据来判断进入if还是else,if会对子进程进行一个SMC的操作,else则会将子进程SMC回去。
其实findcrypto可以找到MD5常数,然后推测SMC使用了对前面取的数据的MD5值来异或。但是这样毕竟太玄学了,想要恢复re3还是应该采取一些正常人能想到的方法。

re3分析

获取re3很简单,手动dumpunk_4BC380处的数据或者把remove扬了然后跑一遍程序就能获取到re3。

对re3分析主函数(分析不出来的函数一路C然后P就彳亍):

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
46
47
48
__int64 __fastcall main(int a1, char **a2, char **a3)
{
struct __jmp_buf_tag env[1]; // [rsp+10h] [rbp-850h] BYREF
struct __jmp_buf_tag v5[1]; // [rsp+E0h] [rbp-780h] BYREF
struct __jmp_buf_tag v6[1]; // [rsp+1B0h] [rbp-6B0h] BYREF
struct __jmp_buf_tag v7[1]; // [rsp+280h] [rbp-5E0h] BYREF
char v8[640]; // [rsp+350h] [rbp-510h] BYREF
char v9[636]; // [rsp+5D0h] [rbp-290h] BYREF
int v10; // [rsp+84Ch] [rbp-14h]
int v11; // [rsp+850h] [rbp-10h]
int v12; // [rsp+854h] [rbp-Ch]
int v13; // [rsp+858h] [rbp-8h]
int v14; // [rsp+85Ch] [rbp-4h]
__int64 savedregs; // [rsp+860h] [rbp+0h] BYREF

v14 = 1;
if ( a1 != 2 )
return 0LL;
((void (__fastcall *)(char *, char **, char **))sub_2490)(a2[1], a2, a3);
((void (__fastcall *)(void *, char *, char *))loc_21F9)(&unk_55C0, v9, v8);
v13 = _setjmp(env);
if ( v13 <= 24 )
{
v11 = _setjmp(v5);
if ( v11 < byte_50A0[25 * v13] )
{
if ( byte_50A0[25 * v13 + 1 + v11] != *((char *)&savedregs + 25 * v13 + v11 - 655) )// v9
v14 = 0;
longjmp(v5, v11 + 1);
}
longjmp(env, v13 + 1);
}
v12 = _setjmp(v6);
if ( v12 <= 24 )
{
v10 = _setjmp(v7);
if ( v10 < byte_5320[25 * v12] )
{
if ( byte_5320[25 * v12 + 1 + v10] != *((char *)&savedregs + 25 * v12 + v10 - 1295) )// v8
v14 = 0;
longjmp(v7, v10 + 1);
}
longjmp(v6, v12 + 1);
}
if ( v14 == 1 )
puts("GOOD JOB");
return 0LL;
}

sub_2490接受输入,控制输入只能为0或1,21F9进行了一些处理,但由于此函数被SMC了所以我们不知道它干了什么,然后把savedregs(做了一些混淆)和byte_50A0处的数据对比,能看出来数据是两组25*25的表。想要看到验证逻辑还是要恢复21F9处的逻辑。

恢复re3

对于SMC,要么是静态分析自己写脚本恢复,要么是动态调试去解密程序。但是问题在于,子进程本身就处于一个被父进程调试的过程,所以我们没法再向子进程附加调试器。而当父进程结束时,子进程也会结束。不过正是因为父子进程的通信过程,我们又会多一种思路。对于本题而言,恢复re3有以下4种思路。

通过patch程序让子进程成为僵尸进程

思路来自FallW1nd师傅,直接看原文8 我就不抄一遍了

整体的patch思路:

但是我WSL调试根本起不来子进程,怎么会是捏?
准备之后配个ubuntu虚拟机了🤗fork调的血压高

硬刚加密函数

findcrypto能看到MD5的常数,可以猜测是用了MD5。
可能调试看的更快吧,但是我起不来子进程,看看PZ师傅的WP

通过trace程序获取真实数据

思路来自track神(track神TTTTQQQQLLLL!!!

本题父进程对子进程的操作都需要从子进程读数据、在父进程中修改、写回子进程这3个过程。其中第1、3步会用到ptrace这个系统调用。
而对于系统调用,可以使用trace来leak出程序运行过程中的各种参数。

使用命令strace -e trace=ptrace -o ./easyre.log ./easyre flag{hhhhhhhhhh},然后去看ptrace的log,只要把这些参数写回就好了。

1
2
3
4
5
6
7
8
9
10
11
ptrace(PTRACE_POKETEXT, 149, 0x561dac238213, 0xe900000000fc45c7) = 0
ptrace(PTRACE_PEEKTEXT, 149, 0x561dac238223, [0x4d94f7e68575494c]) = 0
ptrace(PTRACE_POKETEXT, 149, 0x561dac238223, 0xf445c7000000) = 0
ptrace(PTRACE_PEEKTEXT, 149, 0x561dac238233, [0xf1ac2feb8f8c89a9]) = 0
ptrace(PTRACE_POKETEXT, 149, 0x561dac238233, 0x1ec45c700) = 0
ptrace(PTRACE_PEEKTEXT, 149, 0x561dac238243, [0x7ef555b84fe2ed45]) = 0
ptrace(PTRACE_POKETEXT, 149, 0x561dac238243, 0x8dd00102e0c1d089) = 0
ptrace(PTRACE_PEEKTEXT, 149, 0x561dac238253, [0x7d3de98de644b0e3]) = 0
ptrace(PTRACE_POKETEXT, 149, 0x561dac238253, 0xd06348d001f8458b) = 0
ptrace(PTRACE_PEEKTEXT, 149, 0x561dac238263, [0xee5cc92072689f66]) = 0
(后面太长不贴了

PTRACE_POKETEXT是向子进程写回数据,取这个数据就好。

IDAPython脚本:

1
2
3
4
5
6
7
8
key = [0xe900000000fc45c7,0xf445c7000000,0x1ec45c700,0x8dd00102e0c1d089,0xd06348d001f8458b,0x45830d74c08400b6,0x7400f07d8347eb00,0x14802e0c148d089,0xc0458b48c2014800,0x20c889848ec458b,0xf045c701ec45,0xf07d83407519f8,0x4802e0c148d08948,0x458b48c201480000,0xc889848ec458bc1,0x18f87d8301ec4583,0x6348fc458bc189ec,0x85148d48d00148,0xff518dd00148c045,0xfffffed48e0f18fc,0xe445c700000122,0xdc45c70000,0xe4558b000000c7e9,0xc201000000008514,0xfd00148c8458b48,0x1dc45c701e0,0x48d06348e8458b3a,0x85148d48d0,0xc189e0458bc20148,0x8300000000e045c7,0x7d8301e445830000,0xd06348e8458b3a74,0x85148d48d001,0x89e0458bc20148b8,0xe045c702,0x458bffffff2f8e0f,0x2e0c148d08948d0,0x8b48c20148000000,0x7d8301e845831088]

addr = 0x2213

for i in key:
idc.patch_qword(addr, i)
addr += 16
print("ok")

patch完慢慢分析伪代码就彳亍。

然后就是注意一下,日常翻翻init段 = =
里面调用了一个函数sub_257D,里面对验证数据做了一些修改:

1
2
3
4
5
6
7
8
9
10
11
12
__int64 sub_257D()
{
char v1[640]; // [rsp+0h] [rbp-290h] BYREF
__int64 v2; // [rsp+280h] [rbp-10h]
int i; // [rsp+28Ch] [rbp-4h]

qmemcpy(v1, &unk_3020, 0x271uLL);
v2 = (__int64)&unk_5838 - 1304; //0x5320
for ( i = 0; i <= 24; ++i )
memcpy((void *)(25 * i + v2), &v1[25 * i], (unsigned __int8)v1[25 * i] + 1);
return nullsub_2();
}

在unk_3020附近有unk_32A0,交叉引用可以看到用它对0x50A0做了修改:

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
46
47
48
49
50
51
52
53
54
__int64 __fastcall sigabbrev_np(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
char v4; // zf
__int64 v5; // rbp
__int64 v6; // rcx
unsigned int v7; // eax
__int64 result; // rax

v6 = a4 - 1;
if ( v4 && v6 )
return nullsub_2(a1, a2, a3, v6);
_enable();
*(_BYTE *)(v5 - 97) = -3;
*(_WORD *)(v5 - 114) = -5399;
*(_BYTE *)(v5 - 112) = -20;
*(_DWORD *)(v5 - 120) = -268633374;
*(_WORD *)(v5 - 116) = -5139;
*(_DWORD *)(v5 - 126) = -268633347;
*(_WORD *)(v5 - 122) = -5139;
*(_WORD *)(v5 - 129) = -5401;
*(_BYTE *)(v5 - 127) = -17;
for ( *(_DWORD *)(v5 - 4) = 0; *(int *)(v5 - 4) <= 14; ++*(_DWORD *)(v5 - 4) )
*(_BYTE *)(v5 + *(int *)(v5 - 4) - 111) ^= 0x8Eu;
for ( *(_DWORD *)(v5 - 8) = 0; *(int *)(v5 - 8) <= 2; ++*(_DWORD *)(v5 - 8) )
*(_BYTE *)(v5 + *(int *)(v5 - 8) - 114) ^= 0x8Eu;
for ( *(_DWORD *)(v5 - 12) = 0; *(int *)(v5 - 12) <= 5; ++*(_DWORD *)(v5 - 12) )
*(_BYTE *)(v5 + *(int *)(v5 - 12) - 120) ^= 0x8Eu;
for ( *(_DWORD *)(v5 - 16) = 0; *(int *)(v5 - 16) <= 5; ++*(_DWORD *)(v5 - 16) )
*(_BYTE *)(v5 + *(int *)(v5 - 16) - 126) ^= 0x8Eu;
for ( *(_DWORD *)(v5 - 20) = 0; *(int *)(v5 - 20) <= 5; ++*(_DWORD *)(v5 - 20) )
*(_BYTE *)(v5 + *(int *)(v5 - 20) - 129) ^= 0x8Eu;
v7 = getppid();
snprintf((char *)(v5 - 80), 0x1EuLL, (const char *)(v5 - 111), v7);
*(_QWORD *)(v5 - 32) = fopen((const char *)(v5 - 80), "r");
fgets((char *)(v5 - 96), 16, *(FILE **)(v5 - 32));
fclose(*(FILE **)(v5 - 32));
result = strstr((const char *)(v5 - 96), (const char *)(v5 - 129))
|| strstr((const char *)(v5 - 96), (const char *)(v5 - 114))
|| strstr((const char *)(v5 - 96), (const char *)(v5 - 120))
|| strstr((const char *)(v5 - 96), (const char *)(v5 - 126));
*(_DWORD *)(v5 - 36) = result;
if ( !*(_DWORD *)(v5 - 36) )
{
qmemcpy((void *)(v5 - 768), byte_32A0, 0x271uLL);
result = (__int64)&unk_5834 - 1940; //0x50A0
*(_QWORD *)(v5 - 48) = (char *)&unk_5834 - 1940;
for ( *(_DWORD *)(v5 - 24) = 0; *(int *)(v5 - 24) <= 24; ++*(_DWORD *)(v5 - 24) )
result = (__int64)memcpy(
(void *)(25 * *(_DWORD *)(v5 - 24) + *(_QWORD *)(v5 - 48)),
(const void *)(v5 - 768 + 25LL * *(int *)(v5 - 24)),
*(unsigned __int8 *)(v5 + 25LL * *(int *)(v5 - 24) - 768) + 1);
}
return result;
}

正好和主函数里进行验证的数据对上了,所以这两组数据才是真实数据。分析出来是数织游戏,去在线网站解一下就好。

GameMaster

双击游戏,拿20块赢到3700w

对gamemessage的两处解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1
for (int i = 0; i < num; i++)
{
byte[] array = Program.memory;
int num = i;
array[num] ^= 34;
}
//2
byte[] key = new byte[]{66,114,97,105,110,115,116,111,114,109,105,110,103,33,33,33};
ICryptoTransform cryptoTransform = new RijndaelManaged
{
Key = key,
Mode = CipherMode.ECB,
Padding = PaddingMode.Zeros
}.CreateDecryptor();
Program.m = cryptoTransform.TransformFinalBlock(Program.memory, 0, num);

python解密得到二进制文件,找PE头,前面的全部删掉。
得到解密后的文件拖进dnspy读代码,z3解方程照抄逻辑即可。

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
from z3 import *

enc = [101,5,80,213,163,26,59,38,19,6,173,189,198,166,140,183,42,247,223,24,106,20,145,37,24,7,22,191,110,179,227,5,62,9,13,17,65,22,37,5]
num = -1
chk = [0]*50

s = Solver()
x = BitVec('x',64)
y = BitVec('y',64)
z = BitVec('z',64)

for i in range(320):
x = (((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1) | x << 1)
y = (((y >> 30 ^ y >> 27) & 1) | y << 1)
z = (((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1) | z << 1)
flag = i % 8 == 0
if flag:
num+=1
chk[num] = ((chk[num] << 1) | ((((z >> 32 & 1 & (x >> 30 & 1)) ^ (((z >> 32 & 1) ^ 1) & (y >> 31 & 1))))))

for i in range(len(enc)):
s.add(chk[i]==enc[i])

if(s.check() == sat):
print(s.model())
# [y = 868387187, x = 156324965, z = 3131229747]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xor_key = [60,100,36,86,51,251,167,108,116,245,207,223,40,103,34,62,22,251,227]

y = 868387187
x = 156324965
z = 3131229747

key = []

for i in range(4):
key.append(x & 0xff)
x >>= 8
for i in range(4):
key.append(y & 0xff)
y >>= 8
for i in range(4):
key.append(z & 0xff)
z >>= 8

for i in range(len(xor_key)):
print(chr(key[i%len(key)]^xor_key[i]),end='')

deeprev

题目思路跟googleCTF的eldar一样的,链接是我写的翻译,但是建议读原文。

运行一下,然后去so库里面搜flag,可以搜到一个出题人写好的flag,提示我们应该patch这里。当我们把flag patch成正确的flag,这个程序就会提示win。

用IDA打开deeprev可以看到巨大无比的重定位表,并且这个表是可读可写可执行的。
注意一堆0xc3,这是ret的机器码,而0xc3类型都是REL,这就是r.r_offset = r.r_addend(见eldar那篇wp)。

找一个连着REL类型的0xc3的机器码反编译一下

直接双击stru_8040C4进去按U看看内部结构

注意IDA已经注释出来,0x8040cc地址处的数据是Copy of shared data,也就是从so文件里copy过来的flag值,所以这里肯定跟flag关系密切。
如果不嫌麻烦愿意直接调试硬啃的话就可以直接在这里下内存断点然后跑程序了,调试就可以发现它不断地copy so库里的flag到这个地址,然后经过一个异或和加的运算,再把密文存到另一处(0x8042BA处),然后再不断copy此处存的密文到0x8040FC处,最后再对这个地址异或一个值。
我们当然可以慢慢调试去一个一个记录这些值,不过这样还是 太印度人了 太麻烦了,所以可以思考选择一些工具。

首先经过调试可以发现,关键代码都在ret前面(也就是0xc3),那么就可以以这一条为依据来筛选我们需要的r.r_addend值,然后输出这些值的反汇编代码。
可以先readelf -r deeprev > rel.txt大概看一下重定位表,只有1.2k+行是有意义的,后面都是0,所以写代码的时候也不用遍历整个重定位表,这样会很慢,遍历到1300的时候就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
import lief

b = lief.ELF.parse('./deeprev')

def parse(b) -> list:
relocs = list(b.relocations)

for i in range(3, 1300):
r = relocs[i]
if r.type == 8: #筛选REL类型的重定位信息
print(hex(r.addend), end=',')

parse(b)

打印出来的值可以直接反汇编,我这里手动去掉了一些0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from capstone import *
from struct import *

# r.r_addend被lief解析为signed,所以pack可能会报错out of range(因为变成了负数)
# 写一个函数转换一下负数即可
def yun(i):
if i < 0:
i += 0x10000000000000000
return i

lst = [0x404040,0x1,0x16008040cc253480,0xc3,0x804332,0x1000a0000001a,0xa53f62,0x18,0x8040cc250480,0xc3,0x80440a,0x1000a0000001a,0x8040cc,0x1,0x404041,0x17008040cc253480,0xc3,0x80452a,0x1000a0000001a,0xa53f62,0x18,0x1008040cc250480,0xc3,0x804602,0x1000a0000001a,0x8040cc,0x1,0x404042,0x10008040cc253480,0xc3,0x804722,0x1000a0000001a,0xa53f62,0x18,0x2008040cc250480,0xc3,0x8047fa,0x1000a0000001a,0x8040cc,0x1,0x404043,0x12008040cc253480,0xc3,0x80491a,0x1000a0000001a,0xa53f62,0x18,0x3008040cc250480,0xc3,0x8049f2,0x1000a0000001a,0x8040cc,0x1,0x404044,0x10008040cc253480,0xc3,0x804b12,0x1000a0000001a,0xa53f62,0x18,0x4008040cc250480,0xc3,0x804bea,0x1000a0000001a,0x8040cc,0x1,0x404045,0x11008040cc253480,0xc3,0x804d0a,0x1000a0000001a,0xa53f62,0x18,0x5008040cc250480,0xc3,0x804de2,0x1000a0000001a,0x8040cc,0x1,0x404046,0x12008040cc253480,0xc3,0x804f02,0x1000a0000001a,0xa53f62,0x18,0x6008040cc250480,0xc3,0x804fda,0x1000a0000001a,0x8040cc,0x1,0x404047,0x13008040cc253480,0xc3,0x8050fa,0x1000a0000001a,0xa53f62,0x18,0x7008040cc250480,0xc3,0x8051d2,0x1000a0000001a,0x8040cc,0x1,0x404048,0x14008040cc253480,0xc3,0x8052f2,0x1000a0000001a,0xa53f62,0x18,0x8008040cc250480,0xc3,0x8053ca,0x1000a0000001a,0x8040cc,0x1,0x404049,0x15008040cc253480,0xc3,0x8054ea,0x1000a0000001a,0xa53f62,0x18,0x9008040cc250480,0xc3,0x8055c2,0x1000a0000001a,0x8040cc,0x1,0x40404a,0x16008040cc253480,0xc3,0x8056e2,0x1000a0000001a,0xa53f62,0x18,0xa008040cc250480,0xc3,0x8057ba,0x1000a0000001a,0x8040cc,0x1,0x40404b,0x17008040cc253480,0xc3,0x8058da,0x1000a0000001a,0xa53f62,0x18,0xb008040cc250480,0xc3,0x8059b2,0x1000a0000001a,0x8040cc,0x1,0x40404c,0x18008040cc253480,0xc3,0x805ad2,0x1000a0000001a,0xa53f62,0x18,0xc008040cc250480,0xc3,0x805baa,0x1000a0000001a,0x8040cc,0x1,0x40404d,0x19008040cc253480,0xc3,0x805cca,0x1000a0000001a,0xa53f62,0x18,0xd008040cc250480,0xc3,0x805da2,0x1000a0000001a,0x8040cc,0x1,0x40404e,0x24008040cc253480,0xc3,0x805ec2,0x1000a0000001a,0xa53f62,0x18,0xe008040cc250480,0xc3,0x805f9a,0x1000a0000001a,0x8040cc,0x1,0x40404f,0x2c008040cc253480,0xc3,0x8060ba,0x1000a0000001a,0xa53f62,0x18,0xf008040cc250480,0xc3,0x806192,0x1000a0000001a,0x8040cc,0x1,0x404050,0x26008040cc253480,0xc3,0x8062b2,0x1000a0000001a,0xa53f62,0x18,0x10008040cc250480,0xc3,0x80638a,0x1000a0000001a,0x8040cc,0x1,0x404051,0x1e008040cc253480,0xc3,0x8064aa,0x1000a0000001a,0xa53f62,0x18,0x11008040cc250480,0xc3,0x806582,0x1000a0000001a,0x8040cc,0x1,0x404052,0x1f008040cc253480,0xc3,0x8066a2,0x1000a0000001a,0xa53f62,0x18,0x12008040cc250480,0xc3,0x80677a,0x1000a0000001a,0x8040cc,0x1,0x404053,0x20008040cc253480,0xc3,0x80689a,0x1000a0000001a,0xa53f62,0x18,0x13008040cc250480,0xc3,0x806972,0x1000a0000001a,0x8040cc,0x1,0x404054,0x20008040cc253480,0xc3,0x806a92,0x1000a0000001a,0xa53f62,0x18,0x14008040cc250480,0xc3,0x806b6a,0x1000a0000001a,0x8040cc,0x1,0x404055,0x21008040cc253480,0xc3,0x806c8a,0x1000a0000001a,0xa53f62,0x18,0x15008040cc250480,0xc3,0x806d62,0x1000a0000001a,0x8040cc,0x1,0x404056,0x23008040cc253480,0xc3,0x806e82,0x1000a0000001a,0xa53f62,0x18,0x16008040cc250480,0xc3,0x806f5a,0x1000a0000001a,0x8040cc,0x1,0x404057,0x27008040cc253480,0xc3,0x80707a,0x1000a0000001a,0xa53f62,0x18,0x17008040cc250480,0xc3,0x807152,0x1000a0000001a,0x8040cc,0x1,0x404058,0x24008040cc253480,0xc3,0x807272,0x1000a0000001a,0xa53f62,0x18,0x18008040cc250480,0xc3,0x80734a,0x1000a0000001a,0x8040cc,0x1,0x404059,0x25008040cc253480,0xc3,0x80746a,0x1000a0000001a,0xa53f62,0x18,0x19008040cc250480,0xc3,0x807542,0x1000a0000001a,0x8040cc,0x1,0x40405a,0x26008040cc253480,0xc3,0x807662,0x1000a0000001a,0xa53f62,0x18,0x1a008040cc250480,0xc3,0x80773a,0x1000a0000001a,0x8040cc,0x1,0x40405b,0x27008040cc253480,0xc3,0x80785a,0x1000a0000001a,0xa53f62,0x18,0x1b008040cc250480,0xc3,0x807932,0x1000a0000001a,0x8040cc,0x1,0x8042ba,0x8,0x70008040fc253480,0xc3,0x807ab2,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042bb,0x7c008040fc253480,0xc3,0x807c4a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042bc,0x73008040fc253480,0xc3,0x807de2,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042bd,0x78008040fc253480,0xc3,0x807f7a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042be,0x6f008040fc253480,0xc3,0x808112,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042bf,0x27008040fc253480,0xc3,0x8082aa,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c0,0x2a008040fc253480,0xc3,0x808442,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c1,0x2c008040fc253480,0xc3,0x8085da,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c2,0x7f008040fc253480,0xc3,0x808772,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c3,0x35008040fc253480,0xc3,0x80890a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c4,0x2d008040fc253480,0xc3,0x808aa2,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c5,0x32008040fc253480,0xc3,0x808c3a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c6,0x37008040fc253480,0xc3,0x808dd2,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c7,0x3b008040fc253480,0xc3,0x808f6a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c8,0x22008040fc253480,0xc3,0x809102,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042c9,0x59008040fc253480,0xc3,0x80929a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042ca,0x53008040fc253480,0xc3,0x809432,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042cb,-0x71ff7fbf03dacb80,0xc3,0x8095ca,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042cc,0x3d008040fc253480,0xc3,0x809762,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042cd,0x2a008040fc253480,0xc3,0x8098fa,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042ce,0x59008040fc253480,0xc3,0x809a92,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042cf,0x27008040fc253480,0xc3,0x809c2a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042d0,0x2d008040fc253480,0xc3,0x809dc2,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042d1,0x29008040fc253480,0xc3,0x809f5a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042d2,0x34008040fc253480,0xc3,0x80a0f2,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042d3,0x2d008040fc253480,0xc3,0x80a28a,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042d4,0x61008040fc253480,0xc3,0x80a422,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x8042d5,0x32008040fc253480,0xc3,0x80a5ba,0x1000a0000001a,0xa53f62,0x18,0x8040fc,0x8,0x40405c,0x40405d,0x80412c,0x8040fc,0x804144,0x804114,0x804144,0x6c0080415c253480,0xc3,0x80a992,0x1000a0000001a,0xa53f62,0x18,0x80415c,0x8,0x40405c,0x40405d,0x80412c,0x8040fc,0x80412c,0x804144,0x804114,0x804144,-0x5eff7fbea3dacb80,0xc3,0x80ae2a,0x1000a0000001a,0xa53f62,0x18,0x80415c,0x8,0x40405e,0x40405f,0x80412c,0x8040fc,0x804144,0x804114,0x804144,-0x4eff7fbea3dacb80,0xc3,0x80b262,0x1000a0000001a,0xa53f62,0x18,0x80415c,0x8,0x40405e,0x40405f,0x80412c,0x8040fc,0x80412c,0x804144,0x804114,0x804144,-0x1aff7fbea3dacb80,0xc3,0x80b6fa,0x1000a0000001a,0xa53f62,0x18,0x80415c,0x8,0x8040e4,0x1]
md = Cs(CS_ARCH_X86, CS_MODE_64)
for j in range(len(lst)):
if lst[j] == 0xc3:
try:
CODE = pack('<Q',lst[j-1])
except:
t = yun(lst[j-1])
CODE = pack('<Q', t)
for i in md.disasm(CODE, 0):
print("%s\t%s" %(i.mnemonic, i.op_str))

输出:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
xor	byte ptr [0x8040cc], 0x16
add byte ptr [0x8040cc], 0
xor byte ptr [0x8040cc], 0x17
add byte ptr [0x8040cc], 1
xor byte ptr [0x8040cc], 0x10
add byte ptr [0x8040cc], 2
xor byte ptr [0x8040cc], 0x12
add byte ptr [0x8040cc], 3
xor byte ptr [0x8040cc], 0x10
add byte ptr [0x8040cc], 4
xor byte ptr [0x8040cc], 0x11
add byte ptr [0x8040cc], 5
xor byte ptr [0x8040cc], 0x12
add byte ptr [0x8040cc], 6
xor byte ptr [0x8040cc], 0x13
add byte ptr [0x8040cc], 7
xor byte ptr [0x8040cc], 0x14
add byte ptr [0x8040cc], 8
xor byte ptr [0x8040cc], 0x15
add byte ptr [0x8040cc], 9
xor byte ptr [0x8040cc], 0x16
add byte ptr [0x8040cc], 0xa
xor byte ptr [0x8040cc], 0x17
add byte ptr [0x8040cc], 0xb
xor byte ptr [0x8040cc], 0x18
add byte ptr [0x8040cc], 0xc
xor byte ptr [0x8040cc], 0x19
add byte ptr [0x8040cc], 0xd
xor byte ptr [0x8040cc], 0x24
add byte ptr [0x8040cc], 0xe
xor byte ptr [0x8040cc], 0x2c
add byte ptr [0x8040cc], 0xf
xor byte ptr [0x8040cc], 0x26
add byte ptr [0x8040cc], 0x10
xor byte ptr [0x8040cc], 0x1e
add byte ptr [0x8040cc], 0x11
xor byte ptr [0x8040cc], 0x1f
add byte ptr [0x8040cc], 0x12
xor byte ptr [0x8040cc], 0x20
add byte ptr [0x8040cc], 0x13
xor byte ptr [0x8040cc], 0x20
add byte ptr [0x8040cc], 0x14
xor byte ptr [0x8040cc], 0x21
add byte ptr [0x8040cc], 0x15
xor byte ptr [0x8040cc], 0x23
add byte ptr [0x8040cc], 0x16
xor byte ptr [0x8040cc], 0x27
add byte ptr [0x8040cc], 0x17
xor byte ptr [0x8040cc], 0x24
add byte ptr [0x8040cc], 0x18
xor byte ptr [0x8040cc], 0x25
add byte ptr [0x8040cc], 0x19
xor byte ptr [0x8040cc], 0x26
add byte ptr [0x8040cc], 0x1a
xor byte ptr [0x8040cc], 0x27
add byte ptr [0x8040cc], 0x1b
xor byte ptr [0x8040fc], 0x70
xor byte ptr [0x8040fc], 0x7c
xor byte ptr [0x8040fc], 0x73
xor byte ptr [0x8040fc], 0x78
xor byte ptr [0x8040fc], 0x6f
xor byte ptr [0x8040fc], 0x27
xor byte ptr [0x8040fc], 0x2a
xor byte ptr [0x8040fc], 0x2c
xor byte ptr [0x8040fc], 0x7f
xor byte ptr [0x8040fc], 0x35
xor byte ptr [0x8040fc], 0x2d
xor byte ptr [0x8040fc], 0x32
xor byte ptr [0x8040fc], 0x37
xor byte ptr [0x8040fc], 0x3b
xor byte ptr [0x8040fc], 0x22
xor byte ptr [0x8040fc], 0x59
xor byte ptr [0x8040fc], 0x53
xor byte ptr [0x8040fc], 0x8e
xor byte ptr [0x8040fc], 0x3d
xor byte ptr [0x8040fc], 0x2a
xor byte ptr [0x8040fc], 0x59
xor byte ptr [0x8040fc], 0x27
xor byte ptr [0x8040fc], 0x2d
xor byte ptr [0x8040fc], 0x29
xor byte ptr [0x8040fc], 0x34
xor byte ptr [0x8040fc], 0x2d
xor byte ptr [0x8040fc], 0x61
xor byte ptr [0x8040fc], 0x32
xor byte ptr [0x80415c], 0x6c
xor byte ptr [0x80415c], 0xa1
xor byte ptr [0x80415c], 0xb1
xor byte ptr [0x80415c], 0xe5

提取值手搓解密即可。

1
2
3
4
5
6
7
xor1 = [0x16, 0x17, 0x10, 0x12, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x24, 0x2c, 0x26, 0x1e, 0x1f, 0x20, 0x20, 0x21, 0x23, 0x27, 0x24, 0x25, 0x26, 0x27]
xor2 = [0x70, 0x7c, 0x73, 0x78, 0x6f, 0x27, 0x2a, 0x2c, 0x7f, 0x35, 0x2d, 0x32, 0x37, 0x3b, 0x22, 0x59, 0x53, 0x8e, 0x3d, 0x2a, 0x59, 0x27, 0x2d, 0x29, 0x34, 0x2d, 0x61, 0x32]
flag = ''
for i in range(len(xor1)):
flag += chr((xor2[i]-i)^xor1[i])
print(flag)
#flag{366c950370fec47e34581a0

注意上面输出的最后4句汇编不再是对0x8040fc处的异或,而是在0x80415c处进行异或,所以后4位flag显然是换了验证方式。

对0x80415c下硬件断点可以发现,其逻辑就是两个一组的二元方程,自己解或者z3解均可……

不过比赛的时候给了flag的SHA256值,所以也可以直接爆破。

当然了,毕竟flag只剩最后4位(实际上是3位,因为最后一位一定是}),再去看这个逻辑属实有点麻烦。所以对于我这么一个爆破走天下的人来说当然还是直接爆破方便一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import subprocess
import itertools

fr = open("libsecret.so","rb")
f = fr.read()
fr.close()
for i in itertools.product('1234567890abcedf',repeat = 3):
s = i[0] + i[1] + i[2]
fw = open("libsecret.so","wb")
fw.write(f[:0x305c] + s.encode() + f[0x305f:])
out = subprocess.check_output(["./deeprev"])
if b'Fail' not in out:
print(s)
break
#574

flag{366c950370fec47e34581a0574}

下面是逆向分享会的文档

前置知识

ELF动态重定位条目的结构:

1
2
3
4
5
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;

info包括一个type和symble(可选)索引:

1
2
#define ELF64_R_SYM(info)             ((info)>>32)
#define ELF64_R_TYPE(info) ((Elf64_Word)(info))

动态符号表条目的结构:

1
2
3
4
5
6
7
8
typedef struct {
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_other;
Elf64_Half st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;

本题使用了以下重定位信息:

REL(type 8)

将重定位信息设置为一个相对基址的值(但是基址始终为0)

1
*(uint64_t *)r.r_offset = r.r_addend;

立即数寻址:mov *0xbeef0000, $0x04
写成重定位条目就是{type=RELATIVE, offset=0xbeef0000,symbol=0,addend=0x04}
在IDA中显示为Elf64_Rela <beef0000h, 8, 4h>

CPY(type 5)

从符号地址复制n字节到重定位地址

1
2
Elf64_Sym sym = symbols[ELF64_R_SYM(r.r_info)];
memcpy(r.r_offset, sym.st_value, sym.st_size);

立即数寻址:mov *0xbeef0000, [%foo]
{type=COPY, offset=0xbeef0000,symbol=foo,addend=0}
Elf64_Rela <beef0000h, ?00000005, 0>
复制foo指向的n个字节到0xbeef0000,n是foo的st_size。

R64(type 1)

将重定位信息设置为 符号值+常数

1
*(uint64_t *)r.r_offset = symbols[ELF64_R_SYM(r.r_info)] + r.r_addend;

怎么做?

调试

当我们对这些数据具体如何填充并不清楚的时候,最好的办法当然是让链接器来帮我们做这些事情。我们只需要下断点调试,去找数据的加密逻辑就可以了。
对于这个题而言,调试就可以发现它不断地copy so库里的flag到这个地址,然后经过一个异或和加的运算,再把密文存到另一处(0x8042BA处),然后再不断copy此处存的密文到0x8040FC处,最后再对这个地址异或一个值。

不过其实可以发现,这个题虽然对单个字符串的加密数据不同,但大部分代码是复用的。一个典型的表现就是每次复制数据、异或、加之后,都会有一个ret。所以我们可以认为ret前就是用于加密的关键代码,其他地方是一些对逆向价值不高的代码。
那么就可以写一个脚本来解析这些数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
import lief

b = lief.ELF.parse('./deeprev')

def parse(b) -> list:
relocs = list(b.relocations)

for i in range(3, 1300):
r = relocs[i]
if r.type == 8: #筛选REL类型的重定位信息
print(hex(r.addend), end=',')

parse(b)

这个脚本输出了所有REL类型的重定位信息的addend值,其中可能会有一些值不是我们需要的,但是不用管。
后续反编译的时候,capstone遇到无法反编译的数据就会直接跳过。

然后写一个脚本,筛选ret之前的值反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from capstone import *
from struct import *

# r.r_addend被lief解析为signed,所以pack可能会报错out of range(因为变成了负数)
# 写一个函数转换一下负数即可
def yun(i):
if i < 0:
i += 0x10000000000000000
return i

lst = [...]
md = Cs(CS_ARCH_X86, CS_MODE_64)
for j in range(len(lst)):
if lst[j] == 0xc3:
try:
CODE = pack('<Q',lst[j-1])
except:
t = yun(lst[j-1])
CODE = pack('<Q', t)
for i in md.disasm(CODE, 0):
print("%s\t%s" %(i.mnemonic, i.op_str))

自己写一个解析脚本

推荐阅读:
googlectf2022 eldar WP

顺便一提:eldar这个题是真正实现了使用元数据来隐藏代码逻辑的。
deeprev这个题更像是直接把机器码放在了重定位表里,所以上面那种偷懒的解析方法(直接dump重定位表的数据来反编译)才能奏效。
eldar里的重定位表是没有depprev那样的长数据机器码的,所以基本只能硬调,或者像上面这个大佬一样自己写脚本解析。

关于本文

本文作者 云之君, 许可由 CC BY-NC 4.0.