[DSO-NUS CTF 2021] YALA2 Writeup 题解

YALA 2

250 pts


This blog entry is available in multiple languages
这篇文章在多个语言下可用
中文/Chinese
英文/English

题解

拿到密码后点登陆还是登录失败,应该是有什么其他验证方式。

Screenshot

看APK反编译的代码,我们很快发现在验证完密码以后他调用了一个native函数ix

if (var4.ix(var4.a, var4.b, var16) == -1) {
    var13 = new b.b.a.d.c.b(new Exception("Initialization failed"));
    break label89;
}

它的定义是

public native int ix(byte[] arr1, byte[] arr2, byte[] arr3)

arr3是上一题用户名:密码的SHA256, 也就是0xAdmin:aeroplane的SHA256, 34f37b328d0e1666dcf86307dc1bdbbdb3605750385650069ac74ac1edeb359f

随后可以看到arr1被用作AES解密的密钥,arr2是密文。

byte[] var19 = var4.a;
var16 = var4.b;
SecretKeySpec var20 = new SecretKeySpec(var19, "AES");
Cipher var25 = Cipher.getInstance("AES");
var25.init(2, var20);
var16 = var25.doFinal(var16);

解密后的结果应该以“FLAG”开头,代表解密成功,登录成功。

if (var16[0] == 70 && var16[1] == 76 && var16[2] == 65 && var16[3] == 71) {
    label54: {
        try {
            var17 = new StringBuilder();
            var17.append("CONGRATS! The last flag is ");
            var17.append(b.b.a.c.b(var16));
            var17.append(", you have completed this challenge.");
            Log.d("ctflevel3", var17.toString());
            ......

而flag就会是解密的内容。唯一可能出问题的就是这个native方法了

反编译native库, 函数ix就是 sub_DFC (armv7a)

int __fastcall sub_DFC(JNIEnv *a1, jobject a2, jbyteArray a3, jbyteArray a4, jbyteArray a5)
{
  int v8; // r0
  unsigned int v9; // r5
  char *v10; // r6
  int v11; // r0
  char *v12; // r2
  unsigned int i; // r0
  char *v14; // r1
  char v15; // r3
  jsize v16; // r10
  int v17; // r5
  int v18; // r4
  jsize v19; // r10
  jbyteArray v21; // [sp+8h] [bp-88h]
  char v22[56]; // [sp+10h] [bp-80h] BYREF
  char v23[16]; // [sp+48h] [bp-48h] BYREF
  _QWORD v24[2]; // [sp+58h] [bp-38h] BYREF
  char v25; // [sp+68h] [bp-28h]

  v8 = sub_C0C();
  if ( !sub_DC4(v8) )
    return -1;
  v9 = 0;
  v10 = (*a1)->GetByteArrayElements(a1, a5, 0);
  v11 = sub_B00(v10);
  if ( !v11 )
  {
    (*a1)->ReleaseByteArrayElements(a1, a3, v10, 2);
    return -1;
  }
  v21 = a4;
  v24[0] = 0LL;
  v24[1] = 0LL;
  v25 = 0;
  while ( v9 < 16 )
  {
    v12 = (char *)v24 + v9;
    *((_BYTE *)v24 + v9) = v11;
    v9 += 2;
    v12[1] = BYTE1(v11);
  }
  for ( i = 0; i < 16; i += 2 )
  {
    *((_BYTE *)v24 + i) ^= v10[i];
    v14 = (char *)v24 + i;
    v15 = v10[i + 1];
    v14[1] ^= v15;
  }
  v16 = (*a1)->GetArrayLength(a1, a3);
  v17 = 0;
  (*a1)->GetByteArrayRegion(a1, a3, 0, v16, v23);
  while ( v17 != 16 )
  {
    v23[v17] = aACFJandrgukxp[v17] ^ *((_BYTE *)v24 + v17);
    ++v17;
  }
  v18 = 0;
  (*a1)->SetByteArrayRegion(a1, a3, 0, v16, v23);
  v19 = (*a1)->GetArrayLength(a1, v21);
  qmemcpy(v22, &unk_2A30, 0x31u);
  (*a1)->SetByteArrayRegion(a1, v21, 0, v19, v22);
  (*a1)->ReleaseByteArrayElements(a1, a3, v10, 2);
  return v18;
}

这个函数调用了 sub_B00, 把返回值和 arr3 异或然后再与 aACFJandrgukxp 异或, 存在 arr1 中。注意只有 sub_B00 返回值的前16位被用到了。接下来,unk_2A30 被复制到了 arr2 种,函数结束。很明显,可能出问题的就是 sub_B00 的返回值了。

int __fastcall sub_B00(const char *a1)
{
  int v2; // r6
  int v3; // r0
  int v4; // r4
  struct hostent *v5; // r0
  char *v6; // r0
  size_t v7; // r0
  ssize_t v8; // r6
  __int64 v10; // [sp+0h] [bp-440h] BYREF
  _QWORD v11[2]; // [sp+8h] [bp-438h]
  unsigned __int16 dest[512]; // [sp+18h] [bp-428h] BYREF
  struct sockaddr v13; // [sp+418h] [bp-28h] BYREF

  v10 = unk_29F0;
  v11[0] = unk_29F8;
  *(_DWORD *)((char *)v11 + 7) = 16463121;
  sub_AE8(&v10, &unk_2A61, 18);
  v2 = 0;
  v3 = socket(2, 1, 0);
  if ( v3 >= 0 )
  {
    v4 = v3;
    v5 = gethostbyname((const char *)&v10);
    if ( !v5 )
      goto LABEL_7;
    *(_QWORD *)&v13.sa_family = 2LL;
    *(_QWORD *)&v13.sa_data[6] = 0LL;
    _memmove_chk(&v13.sa_data[2], *v5->h_addr_list, v5->h_length, 12);
    *(_WORD *)v13.sa_data = -28641;
    if ( connect(v4, &v13, 16) < 0 )
      goto LABEL_7;
    v6 = strncpy((char *)dest, a1, 0x400u);
    v7 = _strlen_chk(v6, 0x400u);
    if ( _write_chk(v4, dest, v7, 1024) >= 0
      && (memset(dest, 0, sizeof(dest)), v8 = read(v4, dest, 0x400u), close(v4), v8 >= 2) )
    {
      v2 = dest[0];
    }
    else
    {
LABEL_7:
      v2 = 0;
    }
  }
  return v2;
}

这个函数建立了一个socket,连接的地址是 unk_29F0unk_2A61 ,18字节数据的异或。解密后就是 getuid.api.service,端口是 8080。返回值就是接收到的数据。因为只有前十六位被用到了,这里可以穷举因为只有2^16种可能

穷举代码:

from Crypto.Cipher import AES

def bxor(ba1,ba2):    
    return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)])

for i in range(0, 2**16):
    b = i.to_bytes(2, 'little')
    barr = b * 8
    karr = b"\x34\xF3\x7B\x32\x8D\x0E\x16\x66\xDC\xF8\x63\x07\xDC\x1B\xDB\xBD"
    narr = bxor(barr, karr)
    #print(barr)
    a4 = b"A%C*F-JaNdRgUkXp"
    fa4 = bxor(narr, a4)
    data = b"\x97\xD6\x30\x3E\x85\x5E\xDE\xA5\x84\x83\xF6\x92\xC2\x1D\x92\x0F\x9D\xCE\x2B\x72\x4D\xC8\xF7\x7E\x14\x56\x34\x1C\x64\x90\x89\x8E\x51\x5C\xA7\x94\x4A\x20\xCA\x63\xFA\x4D\xAF\xE7\xDE\xF3\x68\xCF"
    cipher = AES.new(fa4, AES.MODE_ECB)
    de = cipher.decrypt(data)

    if de[:2] == b"FL":
        print(i)
        print(de)

正确的数据就是 16192,解密后是 FLAG6w9z$C&F)J@NcQfTjWnZr4u7x!A%,所以flag就是 DSO-NUS{464C41473677397A24432646294A404E635166546A576E5A7234753778214125}

我们也可以改下手机的hosts文件,建立下服务器来验证下这个答案:

su
echo "127.0.0.1 getuid.api.service" >> /system/etc/hosts
echo -ne "\x40\x3f" | nc -l -p 8080

点击下登录键,登录成功!

这时候logcat里面也会输出flag

03-03 00:41:52.653 25023 25023 D ctflevel3: CONGRATS! The last flag is 464C41473677397A24432646294A404E635166546A576E5A7234753778214125, you have completed this challenge
点赞
  1. tygq13说道:
    Firefox Windows 10
    高中生。。。好奇大佬哪个学校的
    1. 溯洄 溯洄说道:
      Google Chrome Windows 10
      在国外啦
      1. tygq13说道:
        Firefox Windows 10
        同打了dso-ctf,有机会认识一下吗?

发表评论

电子邮件地址不会被公开。必填项已用 * 标注