[DSO-NUS CTF 2021] YALA2 Writeup

YALA 2

250 pts


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

Solution

After getting the credentials, click the "Login" button still produces "Login failed" output. This suggests there might be another layer of authentication.

Screenshot

By inspecting the decompiled code of the APK, we can find that it calls a native method ix

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

And the definition of the method is

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

The content of arr3(var16) is the SHA256 hash of username:password of the previous challenge, which is the SHA256 of 0xAdmin:aeroplane, 34f37b328d0e1666dcf86307dc1bdbbdb3605750385650069ac74ac1edeb359f

After that, arr1 is used as the key for AES decryption while arr2 is the content.

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);

and the result should starts with "FLAG", indicating that decryption is successful and we are logged in!

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());
            ......

The flag would therefore be the decrypted content. This tells us that something wrong has happened inside the native code so we have to look at it.

Decompiling the native code, the ix function corresponds to 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;
}

The function calls sub_B00, and takes the return value, xor the return value with the value of arr3 and then xor it with aACFJandrgukxp, and store it in arr1. Notice that only the first 16 bits of the return value from sub_B00 is used. Then it copies unk_2A30 to arr2 and ends the function. Apparently, the only thing that can go wrong is the return value of the function.

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;
}

The function creates a socket. The address it connects to is the xor value of 18 bytes of data from unk_29F0 and unk_2A61. Decrypting it gives us getuid.api.service The port number is 8080. Then it returns the data read from the socket. As only the first 16-bit data is used later, brute-forcing is possible as there are only 2^16 possibilities.

Here is the code for brute-forcing:

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)

The correct data is thus 16192 and the result is FLAG6w9z$C&F)J@NcQfTjWnZr4u7x!A%, which gives us the flag DSO-NUS{464C41473677397A24432646294A404E635166546A576E5A7234753778214125}

This can be further verified by changing the hosts file of the phone and set up a server that sends the correct data. For example:

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

and clicking the login button. And yes, we are logged in!

The flag is also printed through logcat.

03-03 00:41:52.653 25023 25023 D ctflevel3: CONGRATS! The last flag is 464C41473677397A24432646294A404E635166546A576E5A7234753778214125, you have completed this challenge
点赞

发表评论

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