D^3CTF2025
d3rpg-revenge
是一个游戏,并且随着在游戏中的对话,会改变程序的内存(不可恢复,除非删了重来)。然后玩了下,发现走到二楼和NPC直接对话,会问你是reverse手,misc手还是musc手,选reverse手就会直接让你输入flag,然后回车,显示checking…,再回车,就会有回显,由于附件中有exe和不少dll,并且还有一个自定义的文件,.d3ssad文件。分析结果是:脱壳后的secret_dll.dll中的check_flag函数中的字符串就是密文
然后加密逻辑在d3rpg.dll文件中进行载入,载入的内容应该是d3rpg.d3ssad文件进行解密的结果(应该是这样)。那么我们知道当程序运行的时候,check flag的时候,肯定会将自定义的文件进行解密,加载到内存中,然后调用其中逻辑,进行check。既然如此,我们就可以用ce,重开游戏,直接走到二楼,和NPC对话,直接输入flag,然后回车,让程序就断在checking…这里,然后用ce去内存中找,找到加密逻辑
module Scene_RPG
class Secret_Class
DELTA = 0x1919810 | (($de1ta + 1) * 0xf0000000)
def initialize(new_key)
@key = str_to_longs(new_key)
if @key.length < 4
@key.length.upto(4) { |i| @key[i] = 0 }
end
end
def self.str_to_longs(s, include_count = false)
s = s.dup
length = s.length
((4 - s.length % 4) & 3).times { s << "\0" }
unpacked = s.unpack('V*').collect { |n| int32 n }
unpacked << length if include_count
unpacked
end
def str_to_longs(s, include_count = false)
self.class.str_to_longs s, include_count
end
def self.longs_to_str(l, count_included = false)
s = l.pack('V*')
s = s[0...(l[-1])] if count_included
s
end
def longs_to_str(l, count_included = false)
self.class.longs_to_str l, count_included
end
def self.int32(n)
n -= 4_294_967_296 while (n >= 2_147_483_648)
n += 4_294_967_296 while (n <= -2_147_483_648)
n.to_i
end
def int32(n)
self.class.int32 n
end
def mx(z, y, sum, p, e)
int32(
((z >> 5 & 0x07FFFFFF) ^ (y << 2)) +
((y >> 3 & 0x1FFFFFFF) ^ (z << 4))
) ^ int32((sum ^ y) + (@key[(p & 3) ^ e] ^ z))
end
def self.encrypt(key, plaintext)
self.new(key).encrypt(plaintext)
end
def encrypt(plaintext)
return '' if plaintext.length == 0
v = str_to_longs(plaintext, true)
v[1] = 0 if v.length == 1
n = v.length - 1
z = v[n]
y = v[0]
q = (6 + 52 / (n + 1)).floor
sum = $de1ta * DELTA
p = 0
while(0 <= (q -= 1)) do
sum = int32(sum + DELTA)
e = sum >> 2 & 3
n.times do |i|
y = v[i + 1];
z = v[i] = int32(v[i] + mx(z, y, sum, i, e))
p = i
end
p += 1
y = v[0];
z = v[p] = int32(v[p] + mx(z, y, sum, p, e))
end
longs_to_str(v).unpack('a*').pack('m').delete("\n")
end
def self.decrypt(key, ciphertext)
self.new(key).decrypt(ciphertext)
end
end
end
def validate_flag(input_flag)
c_flag = input_flag + "\0"
result = $check_flag.call(c_flag)
result == 1
end
def check
flag = $game_party.actors[0].name
key = Scene_RPG::Secret_Class.new('rpgmakerxp_D3CTF')
cyphertext = key.encrypt(flag)
if validate_flag(cyphertext)
$game_variables[1] = 100
else
$game_variables[1] = 0
end
end
def check1
flag = $game_party.actors[0].name
if flag == "ImPsw"
$game_variables[2] = 100
else
$game_variables[2] = 0
end
end
密钥也有了,delta的值,是
0x1919810 | ((0 + 1) * 0xf0000000))
$delta的值是0,那么就可以写脚本解密了
from ctypes import *
import libnum
def MX(z, y, sum1, k, p, e):
return c_uint32(((z.value >> 5 ^ y.value << 2) + (y.value >> 3 ^ z.value << 4)) ^ (
(sum1.value ^ y.value) + (k[(p & 3) ^ e.value] ^ z.value)))
def btea(v, k, n, delta):
if n > 1:
sum1 = c_uint32(0)
z = c_uint32(v[n - 1])
rounds = 6 + 52 // n
e = c_uint32(0)
while rounds > 0:
sum1.value += delta
e.value = ((sum1.value >> 2) & 3)
for p in range(n - 1):
y = c_uint32(v[p + 1])
v[p] = c_uint32(v[p] + MX(z, y, sum1, k, p, e).value).value
z.value = v[p]
y = c_uint32(v[0])
v[n - 1] = c_uint32(v[n - 1] + MX(z, y, sum1, k, n - 1, e).value).value
z.value = v[n - 1]
rounds -= 1
else:
sum1 = c_uint32(0)
n = -n
rounds = 6 + 52 // n
sum1.value = rounds * delta
y = c_uint32(v[0])
e = c_uint32(0)
while rounds > 0:
e.value = ((sum1.value >> 2) & 3)
for p in range(n - 1, 0, -1):
z = c_uint32(v[p - 1])
v[p] = c_uint32(v[p] - MX(z, y, sum1, k, p, e).value).value
y.value = v[p]
z = c_uint32(v[n - 1])
v[0] = c_uint32(v[0] - MX(z, y, sum1, k, 0, e).value).value
y.value = v[0]
sum1.value -= delta
rounds -= 1
return v
if __name__ == '__main__':
a = [0x2e, 0x15, 0x6f, 0x7d, 0xea, 0x72, 0xc0, 0x52, 0x2c, 0x1d,
0xbf, 0x06, 0xf2, 0x43, 0x5d, 0xbb, 0x8f, 0x49, 0xde, 0x4d]
a = [int.from_bytes(a[i:i + 4], "little") for i in range(0, len(a), 4)]
for i in range(4):
print(hex(a[i]), end=", ")
print()
k = [0x72, 0x70, 0x67, 0x6d, 0x61, 0x6b, 0x65, 0x72, 0x78, 0x70,
0x5f, 0x44, 0x33, 0x43, 0x54, 0x46]
k = [int.from_bytes(k[i:i + 4], "little") for i in range(0, len(k), 4)]
for i in range(4):
print(hex(k[i]), end=", ")
print()
n = len(a)
delta = 0xf1919810
res = btea(a, k, -n, delta)
print(','.join(hex(x) for x in res))
flag = ''
for i in res:
flag += (libnum.n2s(i)[::-1].decode())
print(flag)
d3ctf{Y0u_R_RPG_M4st3r}
d3piano
一、前言
有点可惜,比赛做的时候不够细心,导致很多错过关键性的一些细节处。赛后复现…
二、开始
是一个模拟钢琴的APK,但只包含了一个八度,7个白键5个黑键。Java层只有APK启动页面的琴键绘制的逻辑,check逻辑都在so层
然后每个键所对应的琴音,根据so层的keysoundnames数组的内容来加载琴音资源
但是点击的逻辑,不知道什么原因,我的jadx反编译不正常,但是使用jeb就好了
完整代码
@Override // android.view.View
public boolean onTouchEvent(MotionEvent event) {
Intrinsics.checkNotNullParameter(event, "event");
int v = event.getActionMasked();
float f = event.getX();
float f1 = event.getY();
switch(v) {
case 0:
case 5: {
int v2 = this.keyPaths.size();
for(int i = 0; i < v2; ++i) {
Region region = new Region();
RectF rectF = new RectF();
((Path)this.keyPaths.get(i)).computeBounds(rectF, true);
region.setPath(((Path)this.keyPaths.get(i)), new Region(((int)rectF.left), ((int)rectF.top), ((int)rectF.right), ((int)rectF.bottom)));
if(region.contains(((int)f), ((int)f1))) {
this.pressedKeys.add(Integer.valueOf(i));
SoundPool soundPool0 = this.soundPool;
if(soundPool0 != null) {
soundPool0.play(((Number)this.keySounds.get(i)).intValue(), 1.0f, 1.0f, 1, 0, 1.0f);
}
if(this.check(i)) {
String s = this.getflag();
Toast.makeText(this.Mycontext, "Congratulations, this is music exclusive to Flag", 0).show();
Log.i("D3CTF_FLAG", s);
}
this.invalidate();
return true;
}
}
return true;
}
case 1:
case 6: {
int v4 = this.keyPaths.size();
break;
}
default: {
return true;
}
}
for(int i = 0; i < v4; ++i) {
Region region = new Region();
RectF rectF = new RectF();
((Path)this.keyPaths.get(i)).computeBounds(rectF, true);
region.setPath(((Path)this.keyPaths.get(i)), new Region(((int)rectF.left), ((int)rectF.top), ((int)rectF.right), ((int)rectF.bottom)));
if(region.contains(((int)f), ((int)f1))) {
this.pressedKeys.remove(Integer.valueOf(i));
this.invalidate();
return true;
}
}
return true;
}
获取当前点击位置,判断属于哪个琴键,切换琴键,然后发出声音。如果是0号白键就使用keySound[0],而keySound[0]的音调由keysoundnames数组决定,比如keysoundname[0] == ‘C’,则0号白键就发出”C”的音调。,每次按下都会将琴键的序号传给so层的check函数进行判断,如果返回true就执行getflag的逻辑,然后log出flag。
接下来分析so层,从Java层分析得知apk只加载了一个libD3Piano.so。
先看JNI_OnLoad函数,由于ida对一些结构体无法识别,手动修复一下
很显然,check方法是动态注册的
跟进分析
__int64 sub_29BD4()
{
__int64 v0; // x0
size_t n; // x0
int i; // [xsp+40h] [xbp-480h]
unsigned __int8 v4; // [xsp+5Ch] [xbp-464h]
_BYTE result1[16]; // [xsp+80h] [xbp-440h] BYREF
char result2[16]; // [xsp+90h] [xbp-430h] BYREF
char resul3[1048]; // [xsp+A0h] [xbp-420h] BYREF
__int64 v8; // [xsp+4B8h] [xbp-8h]
v8 = *(_ReadStatusReg(TPIDR_EL0) + 40);
memset(resul3, 0, sizeof(resul3));
sub_29DB0(&input_str);
if ( getlength(&input_str) != 42 )
goto LABEL_7;
__gmpz_inits();
WHAT(&input_str);
__gmpz_set_str(result1, v0, 12); // 十二进制转十进制
sub_29B44(result2, result1); // RSA
__gmpz_get_str(resul3, 16, result2);
for ( i = 0; i < __strlen_chk(resul3, 0x418u); ++i )
resul3[i] = encrypt(resul3[i]); // 还进行一个针对除大写字母以外的其他字符处理
n = __strlen_chk(resul3, 0x418u);
if ( !memcmp(resul3, aC901acacbb426c, n) ) // "c901acacbb426c9c447acda82513965ccc3faf6c9dc58d24ed34b62c7fb1548f9ad06b9355c7d20704cfdfdfc89a3f893801e31719564683fdc7de26d807ed27f898edb3efd51b6e8e2a192d6a0929554342adfed541cd8399da0fbacfeaa5b608b887fd74f4f0e31f9bb5816c54163b8e46d27553798233bef6eaf848c64e"
{
__android_log_print(4, "D3PianoLog", "YOooou are Right??!");
v4 = 1;
}
else
{
LABEL_7:
v4 = 0;
}
_ReadStatusReg(TPIDR_EL0);
return v4;
}
根据具体逻辑的分析,对函数进行了一些重命名和注释
跟进RSA加密函数
__int64 __fastcall RSA_part(__int64 a1, __int64 a2, __int64 a3)
{
__int64 result; // x0
int v4; // [xsp+3Ch] [xbp-84h]
int n11; // [xsp+44h] [xbp-7Ch]
unsigned int v6; // [xsp+48h] [xbp-78h]
_BYTE v10[32]; // [xsp+68h] [xbp-58h] BYREF
_BYTE v11[16]; // [xsp+88h] [xbp-38h] BYREF
_BYTE v12[16]; // [xsp+98h] [xbp-28h] BYREF
_BYTE v13[16]; // [xsp+A8h] [xbp-18h] BYREF
__int64 v14; // [xsp+B8h] [xbp-8h]
v14 = *(_ReadStatusReg(TPIDR_EL0) + 40);
v6 = 0;
srand(0x221221u); // 伪随机
for ( n11 = 0; n11 <= 11; ++n11 )
{
v4 = *(key + 4LL * n11);
v6 += v4 * rand();
}
__gmpz_inits();
__gmp_randinit_mt();
__gmp_randseed_ui(v10, v6);
sub_29940(v13, v10, 512);
sub_29940(v12, v10, 512);
__gmpz_mul(a1, v13, v12);
__gmpz_sub_ui(v13);
__gmpz_sub_ui(v12);
__gmpz_mul(v11, v13, v12);
__gmpz_set_ui(a2, 65537);
__gmpz_invert(a3, a2, v11);
__gmpz_clears(v13, v12, v11, 0);
result = __gmp_randclear(v10);
_ReadStatusReg(TPIDR_EL0);
return result;
}
for循环中有一个数组,交叉引用后发现是在Java_com_d3ctf2025_D3piano_PianoLayout_getkeysoundnames中进行初始化的,也就是琴键的琴音的索引组成的数组,然后根据index生成soundnames数组
对部分数组进行重命名
__int64 __fastcall Java_com_d3ctf2025_D3piano_PianoLayout_getkeysoundnames(JNIEnv *a1, __int64 a2, signed int a3)
{
__int64 v4; // [xsp+10h] [xbp-50h]
signed int i; // [xsp+1Ch] [xbp-44h]
__int64 v6; // [xsp+20h] [xbp-40h]
jclass Class_w; // [xsp+28h] [xbp-38h]
__int64 v10; // [xsp+48h] [xbp-18h]
_BYTE v11[4]; // [xsp+54h] [xbp-Ch] BYREF
__int64 v12; // [xsp+58h] [xbp-8h]
v12 = *(_ReadStatusReg(TPIDR_EL0) + 40);
keysoundindex = getkeysoundindex(a3);
Class_w = FindClass_w(a1, "java/lang/String");
if ( Class_w )
{
v6 = sub_293D4(a1, a3, Class_w, 0);
if ( v6 )
{
for ( i = 0; i < a3; ++i )
{
v11[0] = aCdefgabdegab[*(keysoundindex + 4LL * i)];
v11[1] = 0;
v4 = sub_29418(a1, v11);
if ( !v4 )
{
v10 = 0;
goto LABEL_11;
}
sub_2944C(a1, v6, i, v4);
sub_29490(a1, v4);
}
v10 = v6;
}
else
{
v10 = 0;
}
}
else
{
v10 = 0;
}
LABEL_11:
_ReadStatusReg(TPIDR_EL0);
return v10;
}
再跟进分析getkeysoundindex函数,
std::chrono::system_clock *__fastcall getkeysoundindex(int a1)
{
unsigned __int64 v1; // x0
std::chrono::system_clock *v2; // x0
int j; // [xsp+3Ch] [xbp-74h]
__int64 v5; // [xsp+40h] [xbp-70h]
__int64 v6; // [xsp+48h] [xbp-68h]
unsigned int v7; // [xsp+54h] [xbp-5Ch]
int i; // [xsp+58h] [xbp-58h]
std::chrono::system_clock *v9; // [xsp+68h] [xbp-48h]
char v11[8]; // [xsp+78h] [xbp-38h] BYREF
__int64 v12; // [xsp+80h] [xbp-30h] BYREF
__int64 v13; // [xsp+88h] [xbp-28h] BYREF
_QWORD v14[4]; // [xsp+90h] [xbp-20h] BYREF
v14[3] = *(_ReadStatusReg(TPIDR_EL0) + 40);
sub_294C4(v14, a1);
if ( a1 >> 62 )
v1 = -1;
else
v1 = 4LL * a1;
v2 = operator new[](v1);
v9 = v2;
for ( i = 0; i < a1; ++i )
{
v2 = sub_295E4(v14, i);
*v2 = i;
}
v12 = std::chrono::system_clock::now(v2);
v13 = sub_29608(&v12);
v7 = sub_29660(&v13);
sub_29678(v11, v7);
v6 = sub_29718(v14);
v5 = sub_29748(v14);
sub_296A4(v6, v5, v11);
for ( j = 0; j < a1; ++j )
*(v9 + j) = *sub_295E4(v14, j);
sub_29778(v14);
_ReadStatusReg(TPIDR_EL0);
return v9;
}
静态分析的时候,结合ai的分析结果,误以为是打乱随机的,没有把注意具体点击的时候发出的声音,其实根本没有打乱,琴键的音调是完全正确的。这里的生成乱序keysoundindex没有生效,keysoundindex就是0-11的顺序…(比赛的时候通过Frida hook的时候确实发现每个琴键所对应的index是固定的,但是没注意每个index和每个音调也是一一对应的)
动态注册的check函数里使用的GMP大数库,看官方wp说很多函数的符号ida分析错误,但貌似影响也不大。check函数的逻辑就是把琴键的index拼接成一个12进制的字符串然后作为大数值,完了进行加密,再转换成十六进制字符串,进行比较。
比赛的时候一直以为keysoundindex是随机的,所以RSA一直解不出来…
官方wp中的解密脚本
from Crypto.Util.number import long_to_bytes
p = 0xcd88775691357147eea5dc584718edab9ca314cdd52a8c1cf847dbbb8371798f15e9bdca2bfaa4595d47eecae21bea38691a26e1c707867b5ea2f6f2f03bf4c+1
q = 0x565c0138487b57e4b76d0924163f67facb17a77f83e354cc3c8432879dab4611c2442cdd73f71c9e6cb4e56a7c45a403148e6d558f986ec6505882ae095c34d2+1
d = 0x2e4f4254c529f4ef40d28a6595e60bb28b1d11ab13328000fb63cf77836a423259af8a8881d73e1868cd66bbd78920f38033f9f1e0e406b82f0aee50fdbe2567e0ac329e80e9abbb7075f4711157844e9fcb666200b1c4ef05b3cc66278221c9e6bd7250caf2be2a3793cf1f2dd43918d6ead0ee851eccf43e48750b6fab2f9
e = 65537
c = 0xc901acacbb426c9c447acda82513965ccc3faf6c9dc58d24ed34b62c7fb1548f9ad06b9355c7d20704cfdfdfc89a3f893801e31719564683fdc7de26d807ed27f898edb3efd51b6e8e2a192d6a0929554342adfed541cd8399da0fbacfeaa5b608b887fd74f4f0e31f9bb5816c54163b8e46d27553798233bef6eaf848c64e
n = p * q
mm = pow(c,e,n)
print("解密后的flag(字符串):", long_to_bytes(mm).decode())
确实,如同check函数中的log提示,这是fake flag。
回顾前面的分析过程,getkeysoundindex并没有生成打乱的0-11,并且在尝试调试或者frida hook的时候发现有检测,想hook只能通过附加进程调试,但是在这个so文件里并没有看到很明显的检测逻辑,同时lib目录里还有一个so文件,并且是可以在libD3Piano.so中验证另一个so文件的可疑点
libD3Piano.so链接了另外的两个库,但是很明显使用了libgmp.so,没有使用另一个所以 这个libMediaPlayer.so肯定在 init_array里面藏了东西。因为在链接时会调用 init_array 的内容。
这其中可疑的函数只有sub_8938CC()
初始化了三个信号量,创建了四个线程,每个线程都执行不同的函数
- 从 unk_BBBF04 到 unk_BBBF34,每间隔 16 字节初始化一个信号量(sem_init),初值为 0。
- 4 个线程,每个线程执行的函数来自
off_BBAD20
表中的函数指针(共 4 个)。
依次进行分析
fun1是Frida检测的逻辑
__int64 fun1()
{
__int64 v0; // x0
__int64 *v2; // [xsp+48h] [xbp-388h]
__int64 *i; // [xsp+D0h] [xbp-300h]
__int64 v4; // [xsp+E0h] [xbp-2F0h]
_QWORD v5[3]; // [xsp+118h] [xbp-2B8h] BYREF
_QWORD v6[3]; // [xsp+130h] [xbp-2A0h] BYREF
_BYTE v7[16]; // [xsp+148h] [xbp-288h] BYREF
_QWORD v8[2]; // [xsp+158h] [xbp-278h] BYREF
_BYTE v9[16]; // [xsp+168h] [xbp-268h] BYREF
_QWORD v10[2]; // [xsp+178h] [xbp-258h] BYREF
_BYTE v11[16]; // [xsp+188h] [xbp-248h] BYREF
_BYTE v12[24]; // [xsp+198h] [xbp-238h] BYREF
_BYTE v13[24]; // [xsp+1B0h] [xbp-220h] BYREF
_BYTE v14[24]; // [xsp+1C8h] [xbp-208h] BYREF
_BYTE v15[344]; // [xsp+1E0h] [xbp-1F0h] BYREF
_BYTE v16[144]; // [xsp+338h] [xbp-98h] BYREF
__int64 v17; // [xsp+3C8h] [xbp-8h] BYREF
v17 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
sub_892D64(v14, "/proc/self/task", 0);
sub_892DC8(v13);
sub_892DEC(v12);
sub_892E68();
sub_892E68();
sub_892E68();
sub_892E68();
sub_892E68();
sub_892E68();
sub_892EEC(v11, v14);
sub_892F50();
sub_892F20(v10, v9);
sub_892F7C(v9);
sub_892F50();
sub_892FA0(v8, v7);
sub_892F7C(v7);
while ( (sub_892FC8(v10, v8) & 1) != 0 )
{
v4 = sub_892FFC(v10);
v0 = sub_893060(v4);
sub_893074(v5, v0);
sub_893020(v6, v5, "/comm");
sub_8930AC();
sub_9E4E94(v6);
sub_9E4E94(v5);
sub_8930E0(v15, v13, 24);
if ( (sub_893124(v15) & 1) != 0 )
{
sub_893150(v15, v12);
for ( i = (__int64 *)v16; i != &v17; i += 3 )
{
if ( sub_8931A8(v12, i, 0) != -1 )
{
_ReadStatusReg(TPIDR_EL0);
exit(0);
}
}
}
sub_893220(v15);
sub_89325C(v10);
}
sub_892F7C(v8);
sub_892F7C(v10);
sub_892F7C(v11);
sleep(5u);
v2 = &v17;
do
{
sub_9E4E94(v2 - 3);
v2 -= 3;
}
while ( v2 != (__int64 *)v16 );
sub_9E4E94(v12);
sub_893284(v13);
sub_893284(v14);
_ReadStatusReg(TPIDR_EL0);
return 0;
}
通过检测线程名称来检测是否处于调试/hook状态。检测到就直接exit退出,绕过检测直接nop掉exit函数。
剩下三个fun涉及到了信号量。执行顺序为fun3 -> fun2 ->fun4
从fun3开始分析。
并且官方wp中还进行了如何进行hook的补充。
也可以进入**sub_893630()**函数里找一下,就是进去的第一个结构体。
func3→MyListener1、func2→MyListener2、func4→MyListener3。
func3 函数hook函数sub291B4。修改了返回值,然后func2运行。sub_896A28就是on_enter,sub_896A3C就是on_leave
fun2 hook了基地址+0x29BD4地址的函数,就是check函数
__int64 __fastcall fun2_on_enter(__int64 a1, __int64 a2)
{
_BYTE *v2; // x0
unsigned __int8 *v3; // x0
__int64 v4; // x0
size_t n; // x0
__int64 result; // x0
char v7; // [xsp+14h] [xbp-ACh]
int n90_1; // [xsp+28h] [xbp-98h]
int n90; // [xsp+2Ch] [xbp-94h]
__int64 v12; // [xsp+30h] [xbp-90h]
int n11; // [xsp+4Ch] [xbp-74h] BYREF
_OWORD s2_[6]; // [xsp+50h] [xbp-70h] BYREF
int v15; // [xsp+B0h] [xbp-10h]
__int64 v16; // [xsp+B8h] [xbp-8h]
v16 = *(_ReadStatusReg(TPIDR_EL0) + 40);
v12 = (*(*a2 + 24LL))(a2, 2);
v15 = 0;
memset(s2_, 0, sizeof(s2_));
if ( sub_897580(a1 + 8) < 0x5B ) // 检测长度
{
result = sub_897630(a1 + 8, &aCdefgabdegab[v12]);// "CDEFGABdegab"
}
else
{
for ( n90 = 0; n90 <= 90; ++n90 ) // 长度大于90,先异或
{
v7 = byte_BBACC2[n90];
v2 = sub_8975A0(a1 + 8, n90);
*v2 ^= v7;
}
for ( n90_1 = 0; n90_1 <= 90; ++n90_1 ) // 然后根据异或后的字符串,获取到对应的音调的下标保存到_obj
{
for ( n11 = 0; n11 <= 11; ++n11 )
{
v3 = sub_8975A0(a1 + 8, n90_1);
if ( *v3 == aCdefgabdegab[n11] ) // "CDEFGABdegab"
sub_8975C4(&obj_, &n11);
}
}
(*(*a1 + 32LL))(a1); // 调用虚表函数1
v4 = sub_897580(a1 + 8);
(*(*a1 + 40LL))(a1, s2_, v4, a1 + 32, aCdefgabdegab, 2232865);// 调用虚表函数2
n = sub_897580(a1 + 8);
result = memcmp(&s1_, s2_, n); // cmp
if ( !result )
byte_BBBF00 = 1;
}
_ReadStatusReg(TPIDR_EL0);
return result;
}
on_leave修改返回值
在func2 attach上函数后,func4执行。func4 hook 偏移0x28E80,也就是get_flag 函数的tostr。
其实就是把func2_on_enter函数里面根据异或加密后的音调在转换成12进制数,丢 给tostr。
那么现在就需要分析两个虚表函数加密,然后写出解密获取到输入的音调数组转换成 12 进制再转换成字符串输出就可以。那么来分析 enc1 与 enc2,enc1 函数有点抽象, 先分析enc2函数。很明显的salsa20加密。
可以尝试先解密enc2函数,key是类成员,找到类的构造函数。
这就是key,被加密了,解密一下。
# 模拟 .rodata 段中的两个 xmmword 数据(共32字节)
xmmword_252578 = [
0x88, 0x9A, 0x93, 0xBC, 0x90, 0xB2, 0xBA, 0xA0, 0xC8,
0xCF, 0xA0, 0xBB, 0xCC, 0x9C, 0xC8, 0xB9
]
xmmword_252588 = [
0xA0, 0xCD, 0xCF, 0xCD, 0xCA, 0xD2, 0x8D, 0xCC, 0xA9,
0xBA, 0xAD, 0xAC, 0xBA, 0xDE, 0xDE, 0xDE
]
# 拼接两个 xmmword 成为 32 字节原始数据
combined = xmmword_252578 + xmmword_252588
# 对这 32 字节做按位取反(模仿 ~data 的行为)
inverted = [~b & 0xFF for b in combined]
# 模拟 a1:前32字节占位,后32字节是结果
a1 = [0] * 32 + inverted
# 打印可见字符
for byte in a1:
if 0x20 <= byte <= 0x7E:
print(chr(byte), end='')
print()
key: welCoME_70_D3c7F_2025-r3VERSE!!!
解密代码
#include <cstdio>
#include <cstring>
#include <format>
#include <iostream>
#include <fstream>
#include <cstdint>
#include <vector>
#define ROTL(a, b) ((((uint32_t)(a)) << ((uint32_t)(b))) | (((uint32_t)(a)) >> (32 - ((uint32_t)(b)))))
#define QUARTERROUND(a, b, c, d) \
a += b; \
d ^= a; \
d = ROTL(d, 16); \
c += d; \
b ^= c; \
b = ROTL(b, 12); \
a += b; \
d ^= a; \
d = ROTL(d, 8); \
c += d; \
b ^= c; \
b = ROTL(b, 7);
unsigned char flag_enc[74] = {
0x2E, 0xD2, 0xDF, 0x53, 0x41, 0xE6, 0x51, 0xA2, 0xD0, 0x8E,
0x43, 0x59, 0x6F, 0xC4, 0x15, 0xAD,
0x97, 0xC2, 0x98, 0xBD, 0x11, 0x05, 0xFE, 0xFF, 0x96, 0x4C,
0xE8, 0x06, 0x50, 0x0E, 0x1D, 0xCA,
0x0E, 0xB2, 0x18, 0xCA, 0x06, 0x54, 0x2E, 0xFA, 0xCD, 0x19,
0xD2, 0x9E, 0xDB, 0x9E, 0x33, 0xCC,
0x5D, 0xAF, 0xED, 0x69, 0x4A, 0xEF, 0x17, 0xB8, 0xD8, 0x40,
0x14, 0x48, 0xCD, 0x37, 0xFC, 0xD0,
0x14, 0x5C, 0x3C, 0x31, 0xC9, 0x15, 0xE6, 0xCF, 0x77, 0x28
};
void salsa20_core(uint32_t out[16], const uint32_t in[16]) {
uint32_t x[16];
int i;
for (i = 0; i < 16; ++i) {
x[i] = in[i];
}
for (i = 0; i < 10; ++i) {
// 列轮
QUARTERROUND(x[0], x[4], x[8], x[12])
QUARTERROUND(x[5], x[9], x[13], x[1])
QUARTERROUND(x[10], x[14], x[2], x[6])
QUARTERROUND(x[15], x[3], x[7], x[11])
// 行轮
QUARTERROUND(x[0], x[1], x[2], x[3])
QUARTERROUND(x[5], x[6], x[7], x[4])
QUARTERROUND(x[10], x[11], x[8], x[9])
QUARTERROUND(x[15], x[12], x[13], x[14])
}
for (i = 0; i < 16; ++i) {
out[i] = x[i] + in[i];
}
}
void salsa20_encrypt(uint8_t *ciphertext, size_t length, const uint8_t *key, const uint8_t *nonce, uint64_t counter) {
uint32_t state[16];
uint32_t block[16];
size_t i, j;
uint8_t *keystream = (uint8_t *)block;
uint8_t *plaintext = (uint8_t *)malloc(74);
for (int n = 0; n < 74; n++) {
plaintext[n] = flag_enc[n];
}
// 初始化状态
state[0] = 0x61707865;
state[1] = *(uint32_t *)&key[0];
state[2] = *(uint32_t *)&key[4];
state[3] = *(uint32_t *)&key[8];
state[4] = *(uint32_t *)&key[12];
state[5] = 0x3320646e;
state[6] = *(uint32_t *)&nonce[0];
state[7] = *(uint32_t *)&nonce[4];
state[8] = (uint32_t)(counter & 0xFFFFFFFF);
state[9] = (uint32_t)(counter >> 32);
state[10] = 0x79622d32;
state[11] = *(uint32_t *)&key[16];
state[12] = *(uint32_t *)&key[20];
state[13] = *(uint32_t *)&key[24];
state[14] = *(uint32_t *)&key[28];
state[15] = 0x6b206574;
for (i = 0; i < length; i += 64) {
salsa20_core(block, state);
for (j = 0; j < 64 && i + j < length; ++j) {
ciphertext[i + j] = plaintext[i + j] ^ keystream[j];
}
// 更新计数器
if (++state[8] == 0) {
++state[9];
}
}
}
int main() {
uint8_t key[] = "welCoME_70_D3c7F_2025-r3VERSE!!!";
uint8_t nonce[] = "CDEFGABdegab";
uint64_t counter = (uint64_t)0x221221;
uint8_t ciphertext[74];
salsa20_encrypt(ciphertext, sizeof(flag_enc), key, nonce, counter);
for (int i = sizeof(flag_enc) - sizeof(ciphertext); i < sizeof(flag_enc); i++) {
printf("%c", ciphertext[i]);
}
return 0;
}
后面的题解参考官方wp
void LZW_decode(std::vector<uint8_t> &compressed)
{
std::unordered_map<int, std::vector<uint8_t>> dictionary;
for (int i = 0; i < 256; ++i)
{
dictionary[i] = {static_cast<uint8_t>(i)};
}
std::vector<uint8_t> codes;
for (uint8_t byte : compressed)
{
codes.push_back(static_cast<int>(byte));
}
std::vector<uint8_t> result;
uint8_t next_code = 0;
if (codes.empty())
return;
uint8_t prev_code = codes[0];
result.insert(result.end(), dictionary[prev_code].begin(), dictionary[prev_code].end());
for (size_t i = 1; i < codes.size(); ++i)
{
uint8_t curr_code = codes[i];
std::vector<uint8_t> entry;
if (curr_code == next_code)
{
entry = dictionary[prev_code];
entry.push_back(dictionary[prev_code][0]);
}
else if (dictionary.count(curr_code))
{
entry = dictionary[curr_code];
}
else
{
std::cerr << "Error: Invalid code " << curr_code << std::endl;
return;
}
result.insert(result.end(), entry.begin(), entry.end());
std::vector<uint8_t> new_entry = dictionary[prev_code];
new_entry.push_back(entry[0]);
dictionary[next_code++] = new_entry;
prev_code = curr_code;
}
std::string decoded_string(result.begin(), result.end());
std::cout << decoded_string << std::endl;
}
int main()
{
uint8_t key[] = "welCoME_70_D3c7F_2025-r3VERSE!!!";
uint8_t nonce[] = "CDEFGABdegab";
uint64_t counter = (uint64_t)0x221221;
uint8_t ciphertext[74];
salsa20_encrypt(ciphertext, sizeof(flag_enc), key, nonce, counter);
std::vector<uint8_t> inputkeysname;
for (int i = 0; i < sizeof(ciphertext); i++)
{
inputkeysname.push_back(ciphertext[i]);
}
LZW_decode(inputkeysname);
return 0;
}
得到:
bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAF gdgDBDBgeggbAeFagaEedbA
from Crypto.Util.number import long_to_bytes
enc = "bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAFgdgDBDBgeggbAeFagaEedbA"
keys = "CDEFGABdegab"
twelve = "0123456789AB"
tmp = ""
for i in enc:
tmp += twelve[keys.index(i)]
tmp = int(tmp, 12)
print(long_to_bytes(tmp).decode())
d3ctf{Fly1ng_Pi@n0_Key$_play_4_6e@utiful~melody}
三、结语
好题,但感觉像像套着apk的PC题
最后
一次蛮不错体验的比赛,就是比赛时间不太友好(端午假期+周五晚八点-周六晚九点[因开赛时平台卡顿延迟了一小时])。其他题目就没复现啦,详见https://github.com/D-3CTF/D3CTF-2025-Official-Writeup
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1621925986@qq.com