pbctf2020 Corey's Coredump write-up
This challenge gives ELF core dump and encryped flag. Challenge purpose is finding the flag decryption routine and gathering information from core dump.
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.
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.
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.
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.
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.
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.
Here’s what main function does.
- read first password
- check md5(first password) == “d6261b74ac627e5acf1e148b7994182e”
- copy first password to bss
- read /dev/urandom 8 byte
- read second password
- save second password to /tmp/secure
- encrypt second password
7-1. encrypt second password usingint 3
instruction
7-2. xor second password with first password
7-3. xor second password with mersenne twister prng (seed is urandom value) - read encrypted flag
- read original second password from /tmp/secure
- 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 ;)
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.
Then, I audited all stack area from current stack address in IDA by hand and finally got encrypted second password.
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.
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_:(}