AntCTF-D3CTF2023
d3sky #
Beautiful sky~
main 函数很简单,sub_401190 是 RC4
- 数据的位宽是两字节,XOR 时只解密最低字节
- 每次调用加密函数 i 和 j 都会重新设置,但 sbox 的状态是继承的
对 sbox 做交叉引用可以找到一个 TLS 回调函数 sub_401050
该函数里引用了 RC4 的密钥,并通过除零异常触发 SEH 拼接最终的密钥 YunZh1JunAlkaid
回看 main 函数可以看到 data 数组其实是一个状态机,这个状态机除了输入、输出之外只有一种指令 NAND
回顾一下 NAND 的自足性:
- $\neg A \equiv \neg(A \land A)$
- $A \land B \equiv \neg (\neg (A \land B))$
- $A \lor B \equiv \neg(\neg A \land \neg B)$
所以实际上作者的设计就是一个单指令的虚拟机
手动打 log
from pwn import u16
from Crypto.Util.number import long_to_bytes
import z3
fp = open("data.bin", "rb")
data = fp.read()
fp.close()
state = [u16(data[i : i + 2]) for i in range(0, len(data), 2)]
def rc4_ksa(key):
sbox = bytearray(range(256))
j = 0
for i in range(256):
j = (j + sbox[i] + ord(key[i % len(key)])) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
return sbox
def rc4(sbox, msg, idx, length):
i = j = 0
for k in range(length):
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
x = sbox[(sbox[i] + sbox[j]) % 256]
msg[idx + k] ^= x
box = rc4_ksa("YunZh1JunAlkaid")
rc4(box, state, 2772, 74)
def play():
global flags
global state
cnt = 0
while state[0] != 0xFFFF:
if state[2] == 1:
state[2] = 0
print(f"Says: {chr(state[3])}")
if state[7] == 1:
state[7] == 0
print("Reads one byte from stdin")
cnt += 1
if cnt == 37 and state[8] != 126:
print(f"Says: Wrong! (But I don't care)")
if state[19] != 0:
print(f"Says: Wrong! (But I don't care)")
state[19] = 0
idx = state[0]
rc4(box, state, idx, 3)
t0, t1, t2 = state[idx], state[idx + 1], state[idx + 2]
state[0] = idx + 3
rc4(box, state, idx, 3)
state[t2] = (~(state[t0] & state[t1])) & 0xFFFF
print(f"{idx}:\tnand\t[{t2}], [{t0}], [{t1}]")
play()
exit(0)
下面是输出截取的一部分,可以看到 519 到 528 是在转储位于地址 8 的输入到地址 2807;531 到 579 是地址 2772, 2773, 2774, 2775 的 XOR;582 到 594 是把上一步的结果和地址 2809 比较
Reads one byte from stdin
519: nand [7], [8], [8]
522: nand [2807], [7], [7]
525: nand [11], [20], [20]
528: nand [7], [11], [11]
Reads one byte from stdin
Says: Wrong! (But I don't care)
531: nand [7], [8], [8]
534: nand [2808], [7], [7]
537: nand [11], [2772], [2772]
540: nand [11], [11], [2773]
543: nand [12], [2773], [2773]
546: nand [12], [12], [2772]
549: nand [17], [11], [12]
552: nand [11], [2774], [2774]
555: nand [11], [11], [2775]
558: nand [12], [2775], [2775]
561: nand [12], [12], [2774]
564: nand [18], [11], [12]
567: nand [11], [17], [17]
570: nand [11], [11], [18]
573: nand [12], [18], [18]
576: nand [12], [12], [17]
579: nand [18], [11], [12]
582: nand [11], [2809], [2809]
585: nand [11], [11], [18]
588: nand [12], [18], [18]
591: nand [12], [12], [2809]
594: nand [19], [11], [12]
由于 NAND 只是一堆线性约束,所以直接跑一遍约束再用 z3
flags = [z3.BitVec(f"flag{i}", 8) for i in range(37)]
def sim():
global flags
global state
solver = z3.Solver()
cnt = 0
while state[0] != 0xFFFF:
if state[2] == 1:
state[2] = 0
# print(f"Says: {chr(state[3])}")
if state[7] == 1:
state[7] == 0
state[8] = flags[cnt]
cnt += 1
if cnt == 37:
solver.add(state[8] == 126) # 最后一个字符必须是 126
if state[19] != 0:
# print(f"Says: Wrong! (But I don't care)")
state[19] = 0
idx = state[0]
rc4(box, state, idx, 3)
t0, t1, t2 = state[idx], state[idx + 1], state[idx + 2]
state[0] = idx + 3
rc4(box, state, idx, 3)
if t2 != 19:
state[t2] = (~(state[t0] & state[t1])) & 0xFFFF
else: # 出现对地址 19 的赋值,这表明一个约束
solver.add(((~(state[t0] & state[t1])) & 0xFFFF) == 0)
return solver
solver = sim()
solver.check()
m = solver.model()
solved = m.eval(z3.Concat(*flags)).as_long()
solved = long_to_bytes(solved)
solved = solved.decode()
print(solved)
# A_Sin91e_InS7rUcti0N_ViRTua1_M4chin3~
d3rc4 #
simple rc4! but it seems a little strange... plz wrap your flag with antd3ctf{}
main 函数很简单,是一个纯粹的 RC4
sub_1A20 位于 init_array,主要就是初始化正确和错误的提示字符串,并把 key 每个字节 XOR 上 43
sub_16C5 用 fork 叉出一个子进程,在父进程中算出所有 3 ~ flag_len 中的奇数并传递到管道里,而子进程调用函数 sub_13D8,参数就是刚才的管道
接下来父进程从一个全局管道(在 sub_1A20 就开好了)里读取数据并拼接到 RC4 的 key 中
sub_13D8 使用了一样的技巧并向它的子进程传递所有不是第一个数的倍数的数
笔者在这里记录三个坑点:
- fork 拷贝程序地址空间后,全局变量就和它父进程中的原型隔离了,无论子进程怎么改变,都不会影响父进程,所以第 37 行之后都是废话
- pipefd 的第一个 int 是读取端文件描述符,第二个 int 是写入端文件描述符
- 父进程在 fork 时打开的文件描述符,子进程会一样打开,所以 fork 一个管道之后,关闭父进程的读取端,再关闭子进程的写入端,就可以得到一个父进程到子进程的单向管道,反之亦然
上面的筛法最终会筛出没有比本身更小的因子的数,即质数
父进程最终的 flag 校验,这里 cnt 是统计的被筛走的数的个数,由于子进程的 cnt 操作对它无效,它始终只统计了所有 2 的倍数
byte_4180 的长度是 36,因此得到的质数序列应该是 $3, 5, 7, 11, 13, 17, 19, 23, 29, 31$,cnt 的最终值是 16
最后实现上除了注意应用密钥流的方式有变异,也不能忘记产生密钥流会改变 sbox 状态
test_secret = b"\xdb\xb6*\x04\xc7\xb9h\xe0\xbd>\x04o\xd38\x10kJ\xe5a\xa7$\r\xfcs\xaaq\xf8\x10\r}UnC\x00\x00\x00"
secret = b"\xf7_\xe7\xb0\x9a\xb4\xe0\xe7\x9e\x05\xfe\xd85\\r\xe0\x86\xdes\x9f\x9a\xf6\r\xdc\xc8O\xc2\xa4z\xb5\xe3\xcd`\x9d\x04\x1f"
test_buffer = bytearray(test_secret)
buffer = bytearray(secret)
key = bytes(x ^ 43 for x in b"|N\x1aH\x1bF\x18t_\x1btOu\x18H_M")
primes = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
def rc4_ksa(sbox, key):
j = 0
for i in range(256):
j = (j + sbox[i] + (key[i % len(key)])) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
def rc4_prga(sbox, length):
i = j = 0
for k in range(length):
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
yield sbox[(sbox[i] + sbox[j]) % 256]
def xor_mutated(enc, stream):
for i in range(0, len(stream), 2):
enc[i + 1] = (enc[i] - (enc[i + 1] ^ stream[i + 1])) % 256
enc[i] = ((enc[i] ^ stream[i]) - enc[i + 1]) % 256
def xor(enc, stream):
for i in range(len(stream)):
enc[i] ^= stream[i]
sbox = list(range(256))
rc4_ksa(sbox, key)
stream1 = bytes(rc4_prga(sbox, 36))
xor(test_buffer, stream1)
print(test_buffer)
rc4_ksa(sbox, key + bytes(primes))
for j in range(16):
_ = bytes(rc4_prga(sbox, 36)) # discard these
stream2 = bytes(rc4_prga(sbox, 36))
xor_mutated(buffer, stream2)
xor(buffer, stream1)
flag = buffer.decode()
print(flag)
# getting_primes_with_pipes_is_awesome
d3recover #
Oops! I accidentally deleted the original code while updating! Can you help me recover it?
Note: You may need python3.10 and set environment variable "PYTHONHOME" and "PYTHONPATH" to run this challenge.
Cython 原生,没符号很难逆
附件给了两个版本,版本一有逻辑没符号,版本二有符号没逻辑,IDA 导出版本二的符号,再导入版本一即可
这边笔者通过找 __pyx_string_tab 来做,在 .rodata 开头找到字符串区域最前面的引用,做几次交叉引用往上走就可以找到模块初始化的地方
然后查看里面 ___Pyx_CyFunction_New 的调用,可以轻松找到两个函数 check 和 Base64encode
有符号的话还是比较好读的,忽略垃圾回收和抛出异常的部分
比较密文用的是 base64
至于密文和 base64 码表倒是不用动调,直接到字符串视图里一眼就能看到
码表没有魔改
from base64 import b64decode
flag = b'08fOyj+E27O2uYDq0M1y/Ngwldvi2JIIwcbF9AfsAl4='
def inv_step1(x):
return bytes(xx ^ 0x23 for xx in x)
def inv_step2(x):
x = bytearray(x)
for j in reversed(range(len(x) - 2)):
x[j] ^= 84
x[j] = (x[j] - x[j+2]) & 0xff
return x
def inv_step3(x):
x = b64decode(x)
assert(len(x) == 32)
return x
flag = inv_step3(flag)
flag = inv_step2(flag)
flag = inv_step1(flag)
print(flag)
# flag{y0U_RE_Ma5t3r_0f_R3vocery!}
打到这电脑死机了,后面的都没做。。。
复盘 #
d3syscall #
要 root 权限跑码,懒人命令:
docker run -v$PWD:/app -it --rm --name test ubuntu:22.04 sh -c /app/d3syscall 2>&1
main 函数调用了 sub_13F5,里面使用了 syscall 0x14f ~ 0x153,查了一下都是未定义的
然后发现 .init_array 里有个函数,里面有加密的字符串,对它进行逆向
syscall 313 是 finit_module,即从 fd 加载一个内核驱动,而 /proc/kallsyms 是内核符号表,程序从里面提取 sys_call_table 的地址作为内核驱动的参数 magic
内核驱动被解密并暂时放在 /tmp/my_module,保持程序运行把它拷出来 docker cp test:/tmp/my_module .,然后 IDA 分析
这个驱动往系统调用表里添加了自定义的表项 0x14f ~ 0x154,上面以 0x150 为例,注意参数类型为 sys/user.h/user_regs_struct *
结合 sub_13F5 的字节码,可以知道这是一个虚拟机,位宽 64,4 个寄存器,有栈
打个 log
import unicorn
fp = open("dump.bin", "rb")
code=fp.read()
fp.close()
from unicorn import *
from unicorn.x86_const import *
from capstone import *
begin_addr = 0x13f5
end_addr = 0x182f
seg_size = 0x10000
page_size = 0x1000
def emu():
mu = Uc(UC_ARCH_X86, UC_MODE_64)
md = Cs(CS_ARCH_X86, CS_MODE_64)
mu.mem_map(0, seg_size)
mu.mem_write(begin_addr, code)
mu.reg_write(UC_X86_REG_RDI, 1234)
mu.reg_write(UC_X86_REG_RSI, 5678)
def hook_syscall(mu: Uc, user_data):
rax = mu.reg_read(UC_X86_REG_RAX)
rdi = mu.reg_read(UC_X86_REG_RDI)
rsi = mu.reg_read(UC_X86_REG_RSI)
rdx = mu.reg_read(UC_X86_REG_RDX)
def emit2(template):
nonlocal rsi
nonlocal rdx
print(template % (rsi, rdx))
def emit1(template):
nonlocal rsi
print(template % (rsi))
if rax == 0x14f:
if rdi != 0:
emit2("mov r%d, %d")
else:
emit2("mov r%d, r%d")
elif rax == 0x150:
if rdi == 0:
emit2("add r%d, r%d")
elif rdi == 1:
emit2("sub r%d, r%d")
elif rdi == 2:
emit2("mul r%d, r%d")
elif rdi == 3:
emit2("xor r%d, r%d")
elif rdi == 4:
emit2("shl r%d, r%d")
elif rdi == 5:
emit2("shr r%d, r%d")
else:
raise Exception("rdi wrong")
elif rax == 0x151:
if rdi != 0:
emit1("push %d")
else:
emit1("push r%d")
elif rax == 0x152:
print("pop r%d" % (rdi))
elif rax == 0x153:
print("syscall vm stopped, cleaning...")
else:
raise Exception("rax wrong")
# No need to skip insn for hook type UC_HOOK_INSN
# because unicorn will do it for us
try:
print("syscall vm started")
#mu.hook_add(UC_HOOK_CODE, hook_code)
mu.hook_add(UC_HOOK_INSN, hook_syscall, arg1=UC_X86_INS_SYSCALL)
mu.emu_start(begin=begin_addr, until=end_addr)
except Exception as e:
print(e)
mu.emu_stop()
emu()
syscall vm started
mov r0, 1234
mov r1, 5678
push r1
mov r2, r0
mov r1, 3
shl r2, r1 ; a0<<3
mov r1, 1374119038
add r2, r1 ; X=(a0<<3)+1374119038
mov r3, r0
mov r1, 3
mul r3, r1
mov r1, 3769897994
add r3, r1 ; Y=(a0*3)+3769897994
xor r2, r3
mov r3, r0
mov r1, 3868692263
add r3, r1 ; Z=a0+3868692263
xor r2, r3
pop r1
add r1, r2
push r1 ; push a1+(X^Y^Z)
push r0
mov r2, r1
mov r0, 6
shl r2, r0
mov r0, 1403212599
add r2, r0 ; X=(a1<<6)+1403212599
mov r3, r1
mov r0, 5
mul r3, r0
mov r0, 2554341709
add r3, r0 ; Y=(a1*5)+2554341709
xor r2, r3
mov r3, r1
mov r0, 1588479825
sub r3, r0 ; Z=a1-1588479825
xor r2, r3
pop r0
add r0, r2
push r0 ; push a0+(X^Y^Z)
syscall vm stopped, cleaning...
可以看到是单轮 Feistel 密码
syscall 0x154 是检查 flag,要求栈上的值满足一定条件,密文也在里面
解密脚本:
from pwn import *
from numpy import uint64
def decrypt(b: uint64, a: uint64):
assert type(a) is uint64 and type(b) is uint64
X = (b<<6)+1403212599
Y = (b*5)+2554341709
Z = b-1588479825
a -= (X^Y^Z)
X = (a<<3)+1374119038
Y = (a*3)+3769897994
Z = a+3868692263
b -= (X^Y^Z)
return (a, b)
flag = b''
def to_flag(a, b):
global flag
a, b = uint64(a), uint64(b)
a, b = decrypt(a, b)
flag += p64(a) + p64(b)
to_flag(0xB0800699CB89CC89, 0x4764FD523FA00B19)
to_flag(0x396A7E6DF099D700, 0xB115D56BCDEAF50A)
to_flag(0x2521513C985791F4, 0xB03C06AF93AD0BE)
print(flag)
#d3ctf{cef9b994-2547-4844-ac0d-a097b75806a0}
d3Hell #
Just waitting... The flag will come to find you.
总共有两个文件,d3_runtime.dll 和 d3.exe
先打开 d3.exe,IDA 会报两个错,不过可以正常打开
String table size 131072 is incorrect, maximum possible value is 7271. Do you want to continue with the new value?
Some imported modules will not be visible
because the IAT is located outside of memory range of the input file.
打开后看导入信息发现只有 kernel32.dll 和 msvcrt.dll,没有 d3_runtime.dll,有两中可能
- DLL 是使用 LoadLibrary 动态加载的
- IAT 被放在了其它地方
浏览 PE 文件,很明显是 mingw 带着 DWARF 调试信息编译的
可以看到导入目录被作者移动到最后一个调试信息节
调试信息节和程序执行无关,因此会有一个 IMAGE_SCN_MEM_DISCARDABLE (0x02000000) 标签,IDA 看到这个标签会拒绝读取这个节,也就造成了上面的第二个错误
修复方法是在 Characteristics 中去掉这个标签 (0x42100040 -> 0x40100040),修复之后可以看到成功显示导入的 DLL
从上面的修复过程中也可以看出作者并没有扒掉调试信息,然而 IDA 却没有读取到,这显然和第一个错误有关
Immediately following the COFF symbol table is the COFF string table. The position of this table is found by taking the symbol table address in the COFF header and adding the number of symbols multiplied by the size of a symbol. - COFF String Table
字符串表的位置固定在符号表的后面,而 mingw 编译的程序,其符号表被拼接在最后一个节的后面,可以看到它的起始地址在 0x1BC00
然而 PE 头里的符号表起始地址 (RAW) 却被改掉了
将它修复为 0x1BC00,IDA 就可以导入调试信息了
有了调试信息之后 main 函数就很清晰了
find 函数第一部分,其实就是判断 a1 是否为质数,如果是质数就 push 到 fac 数组
其中 Minter_Rabin 是指 Miller-Rabin primality test,这是一个概率检测素数的办法
bingbao 是指 冰雹猜想,猜想认为其总是返回 1,虽然执行时间可能会非常久
find 函数第二部分,其实就是使用 Pollard’s rho algorithm 分解 a1 为 axb,然后调用 find(a) 和 find(b)
综合来讲,find 函数就是在分解素因子,并把素因子全部 push 到 fac 数组;而 main 函数就是将 0x405020 的字符串转为整数,分解素因子,排序素因子,再通过计算重复个数提取出素因子的阶
在得到素因子(无重复)的列表后,将其按顺序转化为十进制拼接起来,将这个结果与 0x405060 的数据 XOR,得到的结果就是 flag
虽然这题没有输入,但是 main 函数里大量的 Sleep(rho 里也有一个)和冰雹猜想几乎是跑不完的,在动调之前先看一下 DLL
导入的函数 _61FC1517 只是输出 hello
但这个 DLL 有 DLLMain,并且它还调用 _61FC1628,这个函数有花,还有 SMC
下图红框的部分是 天堂之门,retf 将 cs:eip 改为 0x23:0x666c164f
0x23 是 x86 平坦模式代码段的选择子,此即让 CPU 从 x64 架构切换为 x86 架构来取指令
SMC 的算法是 x[i] ^= x[i-1]
全部改掉让 F5 能够识别
from ida_bytes import get_bytes, patch_bytes
base_addr = 0x666C0000
def nop_lize(begin_ra, end_ra):
begin = begin_ra + base_addr
size = end_ra - begin_ra
patch_bytes(begin, b"\x90" * size)
def xor_unpack():
begin = 0x169A + base_addr
end = 0x18E3 + base_addr
size = end - begin
buf = bytearray(get_bytes(begin, size))
for i in range(1, size):
buf[i] ^= buf[i - 1]
patch_bytes(begin, bytes(buf))
起始的代码就是设置一些数据,而第 19 行就是在检测 0x401F13 处的代码是否被修改过
0x401F13 是 rho 算法里调用 Sleep 函数的过程,主要是对抗 patch + 调试用的
而后第 25 行检测是否有调试器,并且是否 patch 过,如果都没有,就改变数据
可以看到 0x405020 的值来自 TEA 解密的值拼接,0x405060 的值应该全都是 0
其中 TEA 的密钥应该是 0x114514, 0x1919810, 0x24270047, 9
复原算法,factordb 分解因数拼接得到 flag,更好的办法可能是直接调 DLL 里的函数得到它的输出
$698740305822331500978964939673142241 = 768614336404564651 \times 909090909090909091$
d3Tetris #
find the bootId
这题只能获取 flag 前 32 位,据说后 4 位官方通过提示给出(笔者复盘的时候卡了好一会)
安卓逆向,有流量包,应该是要分析一个信息收集的后门
protobuf 流量,放到 在线工具 解码,值得注意的值主要有两个,一个是 16 字节的 hex 字符串,一个是 32 字节的字节串
{
//...
"2": {
"value": {
"bytes": "(32 bytes)",
"hex": "0xa6622e62f77ac35c6bf574446d8af6b2a48444f0f78ea1d0dd09c662270874e9"
},
"wireType": "Length-Delimited"
},
"3": {
"value": "3d354e98963a69b2",
"wireType": "Length-Delimited"
},
//...
}
JEB 打开,com.jetgame.tetris.logic.GameViewModel 里面有两个 native
有 native 先看 native,直接 IDA 打开可以看到 native 都是静态注册的,很友好
.init_array 里有个奇怪函数,结尾处通过 pthread 执行了一个新线程,在里面可以看到 /proc/self/task/%s/status 和 gum_js_loop 等字符串,按照经验这是在对抗 frida 注入
继续看 native,里面大部分都是异或解密字符串的代码,最好进行一下动态调试
在笔者机器上一调到 libnative.so 就调试停止,推测是它扫到了本地的 frida_server
这里在检测函数的 pthread_create 下断点,然后跳过它即可,中间会遇到几次异常,选择丢弃
动态调试解密字符串可以知道,逻辑就是读取 bootId,用某个 IV 和固定的 32 字节 key 作 AES256-CBC 加密
然后再用固定的 RC4 密钥再加密一次
IV 在流量里给出,其它的固定值都可以通过调试得出,但是这里密文只有前 32 字节,后 4 位是官方提示给出的
RC4 没有魔改,但是 AES 有魔改,sbox 变了,并且密钥延展没看到 rcon,无所谓,直接调试 dump 出来
AES 轮加密也魔改了,行移位和列混淆调换了位置
extended_key_dump="""41 20 53 49 4D 50 4C 45 20 4B 45 59 21 21 21 21
21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21
A4 C4 B7 AD E9 94 FB E8 C9 DF BE B1 E8 FE 9F 90
8A C2 DB 4E AB E3 FA 6F 8A C2 DB 4E AB E3 FA 6F
98 5C 28 9D 71 C8 D3 75 B8 17 6D C4 50 E9 F2 54
DB 8F D8 A0 70 6C 22 CF FA AE F9 81 51 4D 03 EE
6D B3 9A 2D 1C 7B 49 58 A4 6C 24 9C F4 85 D6 C8
C0 06 46 E0 B0 6A 64 2F 4A C4 9D AE 1B 89 9E 40
6E 7F 0E F8 72 04 47 A0 D6 68 63 3C 22 ED B5 F4
68 09 64 FB D8 63 00 D4 92 A7 9D 7A 89 2E 03 3A
59 90 CF F3 2B 94 88 53 FD FC EB 6F DF 11 5E 9B
8E E9 30 E9 56 8A 30 3D C4 2D AD 47 4D 03 AE 7D
96 EE 4F 02 BD 7A C7 51 40 86 2C 3E 9F 97 72 A5
74 1E CD BB 22 94 FD 86 E6 B9 50 C1 AB BA FE BC
FC 0D BD 32 41 77 7A 63 01 F1 56 5D 9E 66 24 F8
"""
extended_key = []
for l in extended_key_dump.splitlines():
key_line = list(bytes.fromhex(l))
extended_key.extend([key_line[i:i+4] for i in range(0, 16, 4)])
assert len(extended_key) == 15 * 4# AES 256
sbox_dump = b'\x90zP\xef\xf0\x9c/}\xa04#\xcaO!fk=\xe0\xc2\xb3\xfci\x08\xff\x7f\x16H\xd5\xebY\xd8\x0c\xf3\xe4\xa8\xea\xb9\x81\x01(\x13\xb8l\xcb\xdc\x8a\'\x19\xd2\xa4\xd3\x99IW\x87\xdf-J\xc1X\xb4h\xde\xc7\x94{\xaa\xe2\x8cS\xad\x1e\x04\xc5\x18\x00\xed\xf1yCQ\xb0\x84Z\xee\x97\x88&\xb6s\x9d[\xfe\xe5T\xf4\xb1\x06?\xbc1`\xa1\x85\xc9\xf8\xc6\xe7\xae\xc3\x82\x9f\xfb\xce\xfd2\x8d.A\xbf7\xb7\xecw\x02\x80;v\x92\x14\xd0L8\x89\x1cF+\x0b\xc4r\x0e\xcdb\x10o\t\x8f|\xd4\xd7m\xf7\x9b\xac \x12d\xdd\xcc\xfap\xf95\x1d\x9aR\xf5\r\xaf\xba\xa60)\x8b~\xc0\x83\x95aD\xa3"u^\xa7U*\x91\xf2B\xa2V\xb5_\xdb6G\xbe\x86\xa5@\x15]\x8e\xbd\xc8\x1a\xd1<\x07\x113\xbbn\x9e\x96:\xda9\\,\x1f\x17\xe6%j\n>\x93\xe9te\xabM\xcf\xe8x\x0f\xb2\xe1\xf6q\x03\xa9\x1b\xd6cN\xd9$\x98gE\x05\xe3K'
sbox = list(sbox_dump)
inv_sbox = [0] * 256
for i in range(256):
inv_sbox[sbox[i]] = i
# learnt from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def text2matrix(text):
matrix = []
for i in range(16):
byte = (text >> (8 * (15 - i))) & 0xFF
if i % 4 == 0:
matrix.append([byte])
else:
matrix[i // 4].append(byte)
return matrix
def matrix2text(matrix):
text = 0
for i in range(4):
for j in range(4):
text |= (matrix[i][j] << (120 - 8 * (4 * i + j)))
return text
class AES256KeyExtended:
def __init__(self, extended_key):
self.round_keys = extended_key
print(self.round_keys)
def decrypt(self, ciphertext):
self.cipher_state = text2matrix(ciphertext)
self.__add_round_key(self.cipher_state, self.round_keys[-4:])
self.__inv_shift_rows(self.cipher_state)
self.__inv_sub_bytes(self.cipher_state)
for i in range(13, 0, -1):
self.__round_decrypt(self.cipher_state, self.round_keys[4 * i : 4 * (i + 1)])
self.__add_round_key(self.cipher_state, self.round_keys[:4])
return matrix2text(self.cipher_state)
def __add_round_key(self, s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
def __round_decrypt(self, state_matrix, key_matrix):
self.__add_round_key(state_matrix, key_matrix)
self.__inv_shift_rows(state_matrix)
self.__inv_mix_columns(state_matrix)
self.__inv_sub_bytes(state_matrix)
def __inv_sub_bytes(self, s):
for i in range(4):
for j in range(4):
s[i][j] = inv_sbox[s[i][j]]
def __inv_shift_rows(self, s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
def __mix_single_column(self, a):
# please see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def __mix_columns(self, s):
for i in range(4):
self.__mix_single_column(s[i])
def __inv_mix_columns(self, s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v
self.__mix_columns(s)
def rc4_ksa(sbox, key):
j = 0
for i in range(256):
j = (j + sbox[i] + (key[i % len(key)])) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
def rc4_prga(sbox, length):
i = j = 0
for k in range(length):
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
yield sbox[(sbox[i] + sbox[j]) % 256]
rc4_key=b"SKJ<HANIOSHDLJKa"
iv = b'3d354e98963a69b2'
ct=bytes.fromhex('a6622e62f77ac35c6bf574446d8af6b2a48444f0f78ea1d0dd09c662270874e9')
rc4_sbox = list(range(256))
rc4_ksa(rc4_sbox, rc4_key)
rc4_prng = rc4_prga(rc4_sbox, 0x1000)
ct = bytes(b^next(rc4_prng) for b in ct)
def decrypt_cbc(aes, iv, ct):
blocks = [iv] + [ct[i:i+16] for i in range(0, len(ct), 16)]
results = []
for i in range(1, len(blocks)):
mid = aes.decrypt(int(blocks[i].hex(), 16))
mid = bytes.fromhex(hex(mid)[2:].zfill(32))
results.append(bytes(a^b for a, b in zip(blocks[i-1], mid)))
return b''.join(results)
aes = AES256KeyExtended(extended_key=extended_key)
flag=decrypt_cbc(aes, iv, ct)
print(flag)
#dd4ee7c9-031c-4073-bba5-a3efa8fe????
总结 #
IDA 调试 native 层的步骤 #
网上的资料太乱了,这里也是贴一下 IDA 调试 native 层的步骤
在手机上先以 root 权限启动 IDA 的 android_server
adb push /opt/ida90/dbgsrv/android_server /data/local/tmp
adb 转发 23946
adb forward tcp:23946 tcp:23946
Activity Manager 启动程序,并让它等待调试器
adb shell am start -D -n com.jetgame.tetris/com.jetgame.tetris.MainActivity
如果程序启动后不等调试器说明系统属性 ro.debuggable 不为 1,需要设置
resetprop ro.debuggable 1
然后重开 Android 框架即可
stop;start
也可以改 APK 清单里的 android:debuggable="true",但这样要重新打包并签名,太麻烦了
在程序等调试器的暂停状态,IDA 里配置调试器到本地的 23946,勾选在新库加载时暂停
附加,搜索程序名字,并记下 PID,如 30384
这时候 IDA 会暂停,F9,这是为了让接下来的 jdwp 能够响应连接
然后将 jdwp 转发出来并用 jdb 连接,这是为了让程序继续运行,连上去就可以不管了
adb forward tcp:8700 jdwp:30384
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
程序恢复运行后,应该可以看到 IDA 在 linker 暂停,持续 F9,直到目标被加载
正常下断点调试即可