@Raaquilla

Totally Not Malware

0
0m read

Challenge description

This definitely isn’t malware :)


The script looks something like this:

E=range
D=len
import base64 as H
import ctypes as A
import os
import mmap as B
def I(key,data):
	A=list(E(256));C=0;F=bytearray()
	for B in E(256):C=(C+A[B]+key[B%D(key)])%256;A[B],A[C]=A[C],A[B]
	B=C=0
	for G in data:B=(B+1)%256;C=(C+A[B])%256;A[B],A[C]=A[C],A[B];H=A[(A[B]+A[C])%256];F.append(G^H)
	return bytes(F)
def C():J=H.b64decode('MASSIVE B64 STRING');K='9aad98c1'.encode('ascii');C=I(K,J);E=os.sysconf('SC_PAGE_SIZE');F=(D(C)+E-1)//E*E;L=B.mmap(-1,F,prot=B.PROT_READ|B.PROT_WRITE|B.PROT_EXEC);M=A.c_char*F;G=M.from_buffer(L);N=A.addressof(G);A.memmove(N,C,D(C));O=A.cast(G,A.CFUNCTYPE(A.c_void_p));O()
C()

With MASSIVE B64 STRING being around 4600 characters.

We can do some manual cleanup to deobfuscate the script.

import base64
import ctypes
import os
import mmap

def decrypt(key, data):
    A = list(range(256))
    C = 0
    F = bytearray()
    for B in range(256):
        C = (C + A[B] + key[B % len(key)]) % 256
        A[B], A[C] = A[C], A[B]
    B = C = 0
    for G in data:
        B = (B + 1) % 256
        C = (C + A[B]) % 256
        A[B], A[C] = A[C], A[B]
        H = A[(A[B] + A[C]) % 256]
        F.append(G ^ H)
    return bytes(F)

def main():
    # decrypt the data
    data = base64.b64decode("MASSIVE B64 STRING")
    key = "9aad98c1".encode("ascii")
    result = decrypt(key, data)

    # allocate memory
    page_size = os.sysconf("SC_PAGE_SIZE")
    required_memory = (len(result) + page_size - 1) // page_size * page_size
    memory = mmap.mmap(-1, required_memory, prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
    char_array_type = ctypes.c_char * required_memory
    char_array = char_array_type.from_buffer(memory)
    address = ctypes.addressof(char_array)

    # copy result to a function pointer
    ctypes.memmove(address, result, len(result))
    some_function = ctypes.cast(char_array, ctypes.CFUNCTYPE(ctypes.c_void_p))

    # run the function
    some_function()

main()

It decodes a large base64 encoded string and uses decrypt with a key to decrypt the data. It then loads the data into memory, casts it to a function pointer and calls it.

To be able to decompile it, we want to write it to a file instead of running it. We can do so with a small change to main.

def main():
    # decrypt the data
    data = base64.b64decode("MASSIVE B64 STRING")
    key = "9aad98c1".encode("ascii")
    result = decrypt(key, data)

    # write to file
    with open("totally_not_malware", "wb") as f:
        f.write(result)

We can run the script and decompile the file it creates.

int64_t sub_0() {
  sub_9f3();
  return syscall(0);
}

void *sub_1c(int64_t arg1) {
  void *result = sub_0;

  while (*(result + arg1))
    result += 1;

  return result;
}

int64_t sub_4f(int64_t arg1) { return syscall(1, arg1, sub_1c(arg1)); }

int64_t sub_86(int64_t arg1) { return syscall(2, arg1, sub_1c(arg1)); }

void sub_bd(void *arg1, void *arg2) {
  for (int32_t i = 0; i <= 3; i += 1) {
    int32_t rax_5 = *(arg1 + (i << 2));
    *(arg2 + (i << 2)) = rax_5;
    *(arg2 + (i << 2) + 1) = rax_5 >> 8;
    *(arg2 + (i << 2) + 2) = rax_5 >> 0x10;
    *(arg2 + (i << 2) + 3) = rax_5 >> 0x18;
  }
}

uint64_t sub_176(char arg1) __pure {
  int32_t rax_3;

  rax_3 = arg1 >= 0 ? 0 : 0x1b;

  return rax_3 ^ (arg1 * 2);
}

uint64_t sub_1a2(char arg1, uint8_t arg2) {
  char var_1c = arg1;
  uint8_t i = arg2;
  char var_9 = 0;

  for (; i; i u >>= 1) {
    if (i & 1)
      var_9 ^= var_1c;

    var_1c = sub_176(var_1c);
  }

  return var_9;
}

void sub_1ed(void *arg1, void *arg2) {
  for (int32_t i = 0; i <= 0xf; i += 1)
    *(arg2 + i) = *(arg1 + i);

  int32_t i_1 = 0x10;
  int32_t var_14 = 1;

  while (i_1 <= 0xaf) {
    char var_21;

    for (int32_t j = 0; j <= 3; j += 1)
      (&var_21)[j] = *(arg2 + j + i_1 - 4);

    if (!(i_1 & 0xf)) {
      char rax_12 = var_21;
      char var_20;
      var_21 = var_20;
      var_21 = *(var_21 + &data_b00);
      char var_1f;
      var_20 = *(var_1f + &data_b00);
      char var_1e;
      var_1f = *(var_1e + &data_b00);
      var_1e = *(rax_12 + &data_b00);
      int32_t rax_33 = var_14;
      var_14 = rax_33 + 1;
      var_21 = (var_21 ^ *(rax_33 + &data_d00));
    }

    for (int32_t j_1 = 0; j_1 <= 3; j_1 += 1) {
      *(arg2 + i_1) = *(arg2 + i_1 - 0x10) ^ (&var_21)[j_1];
      i_1 += 1;
    }
  }
}

void sub_37a(void *arg1, void *arg2) {
  for (int32_t i = 0; i <= 0xf; i += 1)
    *(arg1 + i) ^= *(arg2 + i);
}

void sub_3d4(void *arg1) {
  for (int32_t i = 0; i <= 0xf; i += 1)
    *(arg1 + i) = *(*(arg1 + i) + &data_c00);
}

int64_t sub_426(void *arg1) {
  char rax_1 = *(arg1 + 0xd);
  *(arg1 + 0xd) = *(arg1 + 9);
  *(arg1 + 9) = *(arg1 + 5);
  *(arg1 + 5) = *(arg1 + 1);
  *(arg1 + 1) = rax_1;
  char rax_14 = *(arg1 + 2);
  *(arg1 + 2) = *(arg1 + 0xa);
  *(arg1 + 0xa) = rax_14;
  char rax_21 = *(arg1 + 6);
  *(arg1 + 6) = *(arg1 + 0xe);
  *(arg1 + 0xe) = rax_21;
  char result = *(arg1 + 3);
  *(arg1 + 3) = *(arg1 + 7);
  *(arg1 + 7) = *(arg1 + 0xb);
  *(arg1 + 0xb) = *(arg1 + 0xf);
  *(arg1 + 0xf) = result;
  return result;
}

void sub_529(void *arg1) {
  for (int32_t i = 0; i <= 3; i += 1) {
    int32_t rax_2 = i << 2;
    char rax_6 = *(arg1 + rax_2);
    char rax_11 = *(arg1 + rax_2 + 1);
    char rax_16 = *(arg1 + rax_2 + 2);
    char rax_21 = *(arg1 + rax_2 + 3);
    *(arg1 + rax_2) = sub_1a2(rax_6, 0xe) ^ sub_1a2(rax_11, 0xb) ^
                      sub_1a2(rax_16, 0xd) ^ sub_1a2(rax_21, 9);
    *(arg1 + rax_2 + 1) = sub_1a2(rax_6, 9) ^ sub_1a2(rax_11, 0xe) ^
                          sub_1a2(rax_16, 0xb) ^ sub_1a2(rax_21, 0xd);
    *(arg1 + rax_2 + 2) = sub_1a2(rax_6, 0xd) ^ sub_1a2(rax_11, 9) ^
                          sub_1a2(rax_16, 0xe) ^ sub_1a2(rax_21, 0xb);
    *(arg1 + rax_2 + 3) = sub_1a2(rax_6, 0xb) ^ sub_1a2(rax_11, 0xd) ^
                          sub_1a2(rax_16, 9) ^ sub_1a2(rax_21, 0xe);
  }
}

int64_t sub_72f(void *arg1, void *arg2) {
  sub_37a(arg1, arg2 + 0xa0);

  for (int32_t i = 9; i > 0; i -= 1) {
    sub_426(arg1);
    sub_3d4(arg1);
    sub_37a(arg1, (i << 4) + arg2);
    sub_529(arg1);
  }

  sub_426(arg1);
  sub_3d4(arg1);
  return sub_37a(arg1, arg2);
}

uint64_t sub_7e1(void *arg1, int32_t arg2, void *arg3, void *arg4) {
  if (arg2 <= 0 || arg2 & 0xf)
    return 0xffffffff;

  void var_d8;
  sub_1ed(arg3, &var_d8);
  char var_e8[0x10];

  for (int32_t i = 0; i <= 0xf; i += 1)
    var_e8[i] = *(arg4 + i);

  for (int32_t i_1 = 0; i_1 < arg2; i_1 += 0x10) {
    char var_f8[0x10];

    for (int32_t j = 0; j <= 0xf; j += 1)
      var_f8[j] = *(arg1 + j + i_1);

    sub_72f(i_1 + arg1, &var_d8);

    for (int32_t j_1 = 0; j_1 <= 0xf; j_1 += 1)
      *(arg1 + j_1 + i_1) ^= var_e8[j_1];

    for (int32_t j_2 = 0; j_2 <= 0xf; j_2 += 1)
      var_e8[j_2] = var_f8[j_2];
  }

  char rax_36 = *(arg1 + arg2 - 1);

  if (!rax_36 || rax_36 > 0x10)
    return 0xfffffffe;

  char var_1d_1 = 0;

  for (int32_t i_2 = 0; i_2 < rax_36; i_2 += 1)
    var_1d_1 |= *(arg1 + arg2 - 1 - i_2) ^ rax_36;

  if (!var_1d_1)
    return arg2 - rax_36;

  return 0xfffffffd;
}

int64_t sub_9f3() {
  void var_28;
  sub_bd(&data_ae0, &var_28);
  void var_38;
  sub_bd(&data_af0, &var_38);
  int32_t var_c = 0x20;
  int32_t rax = sub_7e1(&data_d60, 0x20, &var_28, &var_38);

  if (rax < 0)
    return sub_86("decrypt failed\n");

  if (rax > 0x3f)
    return sub_86("plaintext too large to NUL-terminate");

  *(rax + &data_d60) = 0;
  return sub_4f(&data_d60);
}

Unfortunately, since the data is not an executable, it does not have an ELF header so we cant simply use hex-rays and get the data values. On the other hand looking at this code, its relatively easy to tell that it is AES decryption, so we wont need to write much code since we can use pycryptodome AES.

Since we dont have the data given to us, we will have to find where the data we want is and load it from there.

Generally for AES decrytion we want:

  • a 16 byte key
  • a 16 byte iv
  • a multiple of 16 byte length cyphertext

We can look at sub_9f3 and immediately tell whats what.

int64_t sub_9f3() {
  void var_28;
  sub_bd(&data_ae0, &var_28); // THIS LOADS 0xAE0
  void var_38;
  sub_bd(&data_af0, &var_38); // THIS LOADS 0xAF0
  int32_t var_c = 0x20;
  int32_t rax = sub_7e1(&data_d60, 0x20, &var_28, &var_38); // THIS LOADS 0xD60

  if (rax < 0)
    return sub_86("decrypt failed\n");

  if (rax > 0x3f)
    return sub_86("plaintext too large to NUL-terminate");

  *(rax + &data_d60) = 0;
  return sub_4f(&data_d60);
}

data_ae0, data_af0, and data_d60 tell us the locations of the data.

We can write a script to load those memory adresses from the binary and decrypt the flag.

from Crypto.Cipher import AES


def extract(blob):
    key_words = blob[0xAE0:0xAE0 + 16]  # 16 bytes
    iv_words = blob[0xAF0:0xAF0 + 16]  # 16 bytes
    ciphertext = blob[0xD60 : 0xD60 + 64]  # a multiple of 16 bytes

    # reconstruct key/iv
    # sub_bd expands each word little endian to 4 bytes
    def expand_words(data):
        out = bytearray()
        for i in range(0, 16, 4):
            word = int.from_bytes(data[i : i + 4], "little")
            out.extend(word.to_bytes(4, "little"))
        return bytes(out)

    key = expand_words(key_words)
    iv = expand_words(iv_words)

    return key, iv, ciphertext


if __name__ == "__main__":
    with open("totally_not_malware", "rb") as f:
        blob = f.read()

    key, iv, ciphertext = extract(blob)

    print(f"key: {key.hex()}")
    print(f"iv: {iv.hex()}")
    print(f"ciphertext: {ciphertext.hex()}")

    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)

    print("flag: ", plaintext.decode("utf-8", errors="ignore"))

Running the script gives us:

key: e79921cf440d93e720f34578427ee4ca
iv: 459ae03beb45edb844a968b2b650db84
ciphertext: d7032b4bf1fce7187040b84fdb99ed58e3bcca63373795d6d0c820ee0c75b709fcab60ad217c0073548b26e77fbf7619bc5a0c33766721c7c92727e14638ea40
flag: RISC{barely_broke_a_sweat_d0ec8cb6a9d8e9ad2ae0ba5b10db8254}

Our flag is:

RISC{barely_broke_a_sweat_d0ec8cb6a9d8e9ad2ae0ba5b10db8254}
View Source