pbctf2020 Corey's Coredump write-up

2020-12-07

This challenge gives ELF core dump and encryped flag. Challenge purpose is finding the flag decryption routine and gathering information from core dump.

description

Analyzing the core dump

IDA supports loading ELF core dump, so we can analyzed core dump using IDA.

First, core dump contains most of the memory when the crash occurred. Therefore we can extract original binary from core dump. I searched the ELF header from core dump and I extracted it. Let analysis the binary.

Recoverying libc symbol

Since the extracted binary was loaded in memory, not the original ELF, symbol information and the address is not resolved. So I did symbol recovery work first.

The function which we can clearly guess the offset of function is __libc_start_main. I searched for this offset in libcdb and found libc an offset exactly matched among several libc’s (link).

After that, I have recovered most of the symbols.

recovered_symbol

Analyzing signal handler

When I started analyzing the main function, I felt something strange. This is because there are an instructions int 3, ud2, which is not normally used in most binaries. Also most of instructions are not disassembled.

obfinst

Therefore, I thought that the binary is obfuscated and analyzed sub_1F00, which is a function that installs signal handler for handling interrupts and exceptions.

Let see how handler handles for instruction ud2 and int 3 instruction.

Handling ud2 instruction

Handler decrypts 8 bytes of instruction using modified mersenne twister prng when ud2 instruction was executed.

ud2

mersenne twister seed is lower 12 bits of ud2 instruction address. and simply xor with prng random value.

It’s time to debofuscation. I wrote ida python script for deobfuscating ud2 instruction.

import ida_bytes

class MersenneTwister:
    def __init__(self):
        self.state = []
        self.index = 0

    def seed(self, seed):
        """
        Initialize generator.
        :param seed: An integer value to seed the generator with
        """
        self.state = []
        self.index = 0
        self.state.append(seed)
        for i in range(1, 624):
            # n = (0x6c078965 * (self.state[i-1] ^ (self.state[i-1] >> 30)) + i)
            n = 0x17B5 * self.state[i-1]
            n &= 0xffffffff
            self.state.append(n)

    def randint(self):
        """
        Extracts a random number.
        :rtype: A random integer
        """
        if self.index == 0:
            self.generate()

        y = self.state[self.index]
        y ^= y >> 11
        y ^= (y << 7) & 0x9d2c5680
        y ^= (y << 15) & 0xefc60000
        y ^= y >> 18

        self.index = (self.index + 1) % 624
        return y

    def generate(self):
        """
        Generates 624 random numbers and stores in the state list.
        """
        new_st = self.state[::]
        for i in range(624):
            n = new_st[i] & 0x80000000
            n += new_st[(i+1) % 624] & 0x7fffffff
            self.state[i] = self.state[(i+397) % 624] ^ (n >> 1)
            if n % 2 != 0:
                self.state[i] ^= 0x9908b0df

def decrypt_ud2(ea):
    mt = MersenneTwister()
    mt.seed(ea & 0xfff)
    ida_bytes.patch_dword(ea + 3, ida_bytes.get_dword(ea + 3) ^ mt.randint())
    ida_bytes.patch_dword(ea + 7, ida_bytes.get_dword(ea + 7) ^ mt.randint())
    ida_bytes.patch_byte(ea, 0x90)
    ida_bytes.patch_byte(ea + 1, 0x90)
    ida_bytes.patch_byte(ea + 2, 0x90)

The ud2 obfuscation didn’t apply much, so I called the decrypt_ud2 function by hand.

int 3 SIGTARP signal handler was obfuscated as ud2 instruction, but it has been solved and now we can analyze int 3 signal handler.

Handling int 3 instruction

int 3 SIGTARP signal handler calls function by switch case number.

int3

Switch case number is passed through eax register, so we should find eax value when face the int 3 instruction.

Also, int 3 handler decrypt string data when switch case number is 2.

strobf

Obfuscation method is same with ud2 instruction, but it decrypts string by byte align.

def decrypt_str(ea):
    mt = MersenneTwister()
    mt.seed((ea + 3) & 0xfff)
    i = 0
    while True:
        c = ida_bytes.get_byte(ea + 3 + i)
        if c == 0:
            break
        ida_bytes.patch_byte(ea + 3 + i, c ^ ((mt.randint() | 0x80) & 0xff))
        i += 1

Okay, now analyze main function again.

Analyzing main function

There are many __debugbreak() (int 3 instruction), but not too complex.

main

Here’s what main function does.

  1. read first password
  2. check md5(first password) == “d6261b74ac627e5acf1e148b7994182e”
  3. copy first password to bss
  4. read /dev/urandom 8 byte
  5. read second password
  6. save second password to /tmp/secure
  7. encrypt second password
    7-1. encrypt second password using int 3 instruction
    7-2. xor second password with first password
    7-3. xor second password with mersenne twister prng (seed is urandom value)
  8. read encrypted flag
  9. read original second password from /tmp/secure
  10. xor encrypted flag with sha256(second password)

The binary crashed at stage 8 (read encrypted flag). So we should gather information from core dump for finding password1 and password2.

Gathering information from core dump

At stage 3, first password stored in bss section, so read the first password simply ;)

pass1

And now we should find encrypted password2 and urandom seed.

First, encrypted second password stored in stack and stack address can be found from gdb.

gdb

Then, I audited all stack area from current stack address in IDA by hand and finally got encrypted second password.

secpass

Next, we should find urandom seed value. But the binary fills urandom seed value in stack to zero.

So I searched heap area for finding IO_FILE structure. Because the binary readed urandom data using fread.

I searched siguature 0xfbad and found structure at 0x000055555555A2A0. offset of _IO_read_base and _IO_read_ptr is 8, so I was sure it is urandom structure.

urand

Decrypt flag

It is final stage, just decrypt second password and xor encrypted flag with sha256(second password).

class MersenneTwister:
    def __init__(self):
        self.state = []
        self.index = 0

    def seed(self, seed):
        """
        Initialize generator.
        :param seed: An integer value to seed the generator with
        """
        self.state = []
        self.index = 0
        self.state.append(seed)
        for i in range(1, 624):
            # n = (0x6c078965 * (self.state[i-1] ^ (self.state[i-1] >> 30)) + i)
            n = 0x17B5 * self.state[i-1]
            n &= 0xffffffff
            self.state.append(n)

    def randint(self):
        """
        Extracts a random number.
        :rtype: A random integer
        """
        if self.index == 0:
            self.generate()

        y = self.state[self.index]
        y ^= y >> 11
        y ^= (y << 7) & 0x9d2c5680
        y ^= (y << 15) & 0xefc60000
        y ^= y >> 18

        self.index = (self.index + 1) % 624
        return y

    def generate(self):
        """
        Generates 624 random numbers and stores in the state list.
        """
        st = self.state[::]
        for i in range(624):
            n = st[i] & 0x80000000
            n += st[(i+1) % 624] & 0x7fffffff
            self.state[i] = self.state[(i+397) % 624] ^ (n >> 1)
            if n % 2 != 0:
                self.state[i] ^= 0x9908b0df

enc_password = [0x7c, 0xa1, 0xdc, 0x9, 0x86, 0x93, 0x9b, 0xa5, 0x96, 0xbc, 0x75, 0xd6, 0x38, 0x54, 0x51, 0xa9, 0xe3, 0x54, 0xb7, 0x5c, 0x6c, 0x46, 0xc8, 0x9, 0xfd, 0xab, 0x2f, 0x11, 0x1a, 0x8d, 0x70, 0xb6, 0x44, 0xee, 0xd8, 0x68, 0xa5, 0x34, 0x86, 0xf6, 0x4f, 0x1c, 0xb4, 0x3, 0x3e, 0x66, 0x6d, 0xce, 0x28, 0xa2, 0x47, 0x95, 0xe4, 0x30, 0x4a, 0xa9, 0xeb, 0xb6, 0x88, 0xe4, 0x47, 0x7a, 0xd5, 0x9a, 0xd8, 0xa9, 0x52, 0x3e, 0x9f, 0xae, 0x80, 0x6c, 0xab, 0x6c, 0x4f, 0xb7, 0x9a, 0xc7, 0x76, 0x69, 0x55, 0x45, 0x68, 0x4b, 0x10, 0x49, 0x41, 0x90, 0x6e, 0x13, 0x89, 0xf4, 0x67, 0xbe, 0x8a, 0xd5, 0x7, 0x82, 0x15, 0x4b]
password1 = bytearray('mypassword_is_secure!!'.ljust(len(enc_password), '\x00'))
mt = MersenneTwister()
mt.seed(0x0B1584C802E81D67)
count = mt.randint() & 0xfff

for i in range(count):
    for j in range(len(enc_password)):
        enc_password[j] ^= mt.randint() & 0xff

for i in range(len(enc_password)):
    enc_password[i] ^= password1[i]

idx = enc_password.index(0)
garbage = enc_password[idx:]
enc_password = enc_password[:idx]
mt = MersenneTwister()
mt.seed((0x840 + 3) & 0xfff)
for i in range(len(enc_password)):
    enc_password[i] ^= (mt.randint() | 0x80) & 0xff
password2 = ''.join(chr(i) for i in enc_password + garbage)

from hashlib import *

key = bytearray(sha256(password2).digest())
flag = bytearray(open("../Downloads/flag.enc", "rb").read())
for i in range(0x20):
    flag[i] ^= key[i]

print ''.join(chr(i) for i in flag)

flag: pbctf{I_hate_c0re_dumps_now_:(}