[Google CTF:APT42 - Part 1] Unfinished RE 28 Jun 2018

Hello all, this is my unfinished write-up for the challenge from google ctf. I know that this is a little bit dumb to post unfinished work, but I made some progress, and want to share it with someone ;)

The task.

attach

So, we are given by some file, that is supposed to be infected, but originally it is used as NTP client. Let’s analyze it with IDA. The main() function is:

int __cdecl main(int argc, const char **argv, const char **envp)
{

...

  v31 = isatty(1);
  v3 = std::operator<<<std::char_traits<char>>(&std::cout, "NTP client v0.1 (");
  if ( v31 )
    v4 = "single";
  else
    v4 = "daemon";
  v5 = std::operator<<<std::char_traits<char>>(v3, v4);
  v6 = std::operator<<<std::char_traits<char>>(v5, " mode)");
  v7 = std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
  std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
  while ( 1 )
  {
    if ( v31 )
    {
      v8 = std::operator<<<std::char_traits<char>>(&std::cout, "---");
      std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
    }
    if ( !GetLocalTime() )
    {
      v9 = std::operator<<<std::char_traits<char>>(&std::cout, "Error reading local time.");
      std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
    }
    else if ( (unsigned __int8)GetNTPTime() ^ 1 )
    {
      v10 = std::operator<<<std::char_traits<char>>(&std::cout, "Error fetching reference time.");
      std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>);
    }
    else if ( current_time == ntp_time )
    {
      if ( v31 )
      {
        v27 = std::operator<<<std::char_traits<char>>(&std::cout, "System time is in perfect sync!");
        std::ostream::operator<<(v27, &std::endl<char,std::char_traits<char>>);
      }
    }
    else
    {
      v30 = *(_QWORD *)std::max<long>(&current_time, &ntp_time);
      v11 = v30 - *(_QWORD *)std::min<long>(&current_time, &ntp_time);
      v12 = std::operator<<<std::char_traits<char>>(&std::cout, "Local time is ");
      v13 = std::ostream::operator<<(v12, v11);
      v14 = std::operator<<<std::char_traits<char>>(v13, " second");
      v15 = v11 == 1 ? &unk_40B306 : "s";
      v16 = std::operator<<<std::char_traits<char>>(v14, v15);
      v17 = v30 == current_time ? " ahead" : " behind";
      v18 = std::operator<<<std::char_traits<char>>(v16, v17);
      std::ostream::operator<<(v18, &std::endl<char,std::char_traits<char>>);
      v19 = std::operator<<<std::char_traits<char>>(&std::cout, " - local time: ");
      v20 = localtime(&current_time);
      v21 = asctime(v20);
      std::operator<<<std::char_traits<char>>(v19, v21);
      v22 = std::operator<<<std::char_traits<char>>(&std::cout, " - reference time: ");
      v23 = localtime(&ntp_time);
      v24 = asctime(v23);
      std::operator<<<std::char_traits<char>>(v22, v24);
      std::operator<<<std::char_traits<char>>(&std::cout, "Adjusting... ");
      v25 = stime(&ntp_time) == -1 ? "failed (are you root?)" : "OK.";
      v26 = std::operator<<<std::char_traits<char>>(&std::cout, v25);
      std::ostream::operator<<(v26, &std::endl<char,std::char_traits<char>>);
    }
    if ( v31 )
      break;
    sub_400A40(60LL);
  }
  v28 = std::operator<<<std::char_traits<char>>(&std::cout, "---");
  std::ostream::operator<<(v28, &std::endl<char,std::char_traits<char>>);
  return 0;
}

What we can see here, is some C++ code. The binary can be executed in single mode or in daemon mode, the call to isatty() is responsible for choosing the mode. There is some unnamed function that is called almost at the end, and only in daemon mode. Let’s look at it:

Wow, we had found some shellcode-style code. You can simply press u to undefine that code, and c to make code again in proper place, to analyze file futher.

Reversing and patching.

There is a bunch of such shellcode-style functions in binary. If we will analyze binary dynamically with gdb, we can stop what the code is doing. The code is deobfuscating from itself some constants, search libc in memory, and execute functions bypassing .got.plt trampolines. The major check are:

Then I spot some weird behavior. The code is sending all possible signals using bsd_signal()and sighandler_t = 1. Here is strace log:

rt_sigaction(SIGHUP, {SIG_IGN, [HUP], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGINT, {SIG_IGN, [INT], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGQUIT, {SIG_IGN, [QUIT], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGILL, {SIG_IGN, [ILL], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGTRAP, {SIG_IGN, [TRAP], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGABRT, {SIG_IGN, [ABRT], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGBUS, {SIG_IGN, [BUS], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGFPE, {SIG_IGN, [FPE], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGKILL, {SIG_IGN, [KILL], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, 0x7fff195daea8, 8) = -1 EINVAL (Invalid argument)
rt_sigaction(SIGUSR1, {SIG_IGN, [USR1], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGSEGV, {SIG_IGN, [SEGV], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGUSR2, {SIG_IGN, [USR2], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGPIPE, {SIG_IGN, [PIPE], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGALRM, {SIG_IGN, [ALRM], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGTERM, {SIG_IGN, [TERM], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGSTKFLT, {SIG_IGN, [STKFLT], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGCHLD, {SIG_IGN, [CHLD], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGCONT, {SIG_IGN, [CONT], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGSTOP, {SIG_IGN, [STOP], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, 0x7fff195daea8, 8) = -1 EINVAL (Invalid argument)
rt_sigaction(SIGTSTP, {SIG_IGN, [TSTP], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGTTIN, {SIG_IGN, [TTIN], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGTTOU, {SIG_IGN, [TTOU], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGURG, {SIG_IGN, [URG], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGXCPU, {SIG_IGN, [XCPU], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGXFSZ, {SIG_IGN, [XFSZ], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGVTALRM, {SIG_IGN, [VTALRM], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGPROF, {SIG_IGN, [PROF], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGWINCH, {SIG_IGN, [WINCH], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGIO, {SIG_IGN, [IO], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGPWR, {SIG_IGN, [PWR], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGSYS, {SIG_IGN, [SYS], SA_RESTORER|SA_RESTART, 0x7fd5be4114b0}, {SIG_DFL, [], 0}, 8) = 0

I do not understand why it needed, but anyways after it, the code creates some IPC objects (memory map and mutex) and run clone() function with new entry 0x40a740.

Btw, python pwn tools and peda provided us great opportunity to patch binary on disk, or in memory with gdb. Let me give you some examples:

#!/usr/bin/env python
from pwn import *
...

log.info("patch only")

e = ELF("./ntpdate")

e.asm(0x409400, "xor rax, rax; nop; nop") # isatty() check

# time check, krb5.conf check, domain check...

e.write(0x408E7E, "\x90\x90") 
e.write(0x408E65, "\x90\x90")
e.write(0x408E6E, "\x90\x90")

e.save("./ntpatch")

This simple script will patch all the checks in the file, and force it to communicate with the server (but please crate krb5.conf anyways).

This simple script will do all the patches is a memory and set breakpoints, for debugging:

#!/usr/bin/env python
from pwn import *

def patchMem(addr, data):

	o = ""
	idx = 0

	for d in data:

		o += 'set *(char *)(%s+%d)=0x%x\n' % (addr, idx, ord(d))

		idx += 1

	return o
...

c = gdb.debug("./ntpdate", """
b main
c
""" + patchMem("isatty", asm("mov rax, 0; ret")) + patchMem("0x408E7E", "\x90\x90")  + patchMem("0x408E65", "\x90\x90")  + patchMem("0x408E6E", "\x90\x90")  + """
b *0x40ab0f
c
set $rdx=0x090900
set follow-fork-mode child
b * 0x40a740
b * 0x40A106
""")

c.interactive()

To debug the code after clone() call, we need to clear flag CLONE_UNTRACED and set follow-fork-mode child in gdb. But I prefer to just patch binary and analyze further at runtime.

After clone() execution we face the most fun part – communication with the server.

The protocol.

The code is communicating with the server every one minute. It receives some commands, executes it and send a result to the server. Let’s look at some examples from wireshark:

The actual sequence of commands given by server is:

exec echo $USER
exec hostname
exec uname -a
exec ip a
rm

After receiving rm the code stops and removes itself on disk.

The protocol is simple, we can reverse it. And talk to the server by ourselves. Let’s try.

The typical sequence is: hello –> <recv command> –> <send result> –> la revedere. la revedere is just goodbye in Romanian. This sequence is true for all, except last message. There is no la revedere is the last message, but just ok instead.

Let’s analyze the packet format:

There is also stream command that is folowed by any string ending ‘\x0a’.

Now we can write the script, to force the server to send as all the commands.

#!/usr/bin/env python

from pwn import *
from struct import *
import sys


rn = randoms(8)


def sendData(cmd):

	data = rn
	data += cmd + '\x00'

	a = 0
	for c in data:
		a = 0xff & (a ^ ord(c))

	data += p8(a)

	r.send(p32(len(data)) + data)

context.log_level = "debug"


if __name__ == "__main__":

	cmds = ""

	while 1:

		r = remote("mlwr-part1.ctfcompetition.com", 4242)

		sendData("hello")

		sleep(0.5)
		size = unpack("<I", r.recvn(4))[0]
		log.info("recn: %d", size)
		data = r.recvn(size)
		cmd = data[8:-2]
		cmd = cmd.replace("\x00", ' ')

		cmds += cmd + "\n"

		log.info("command = %s", cmd)

		sendData("stream")
		r.send("1\x0a")

		sendData("la revedere")

		if cmd == 'rm':
			break

	log.info("commands: %s", cmds)

	#r.interactive()
	

Ok, it works. But still there is no any kind of flag :( I had tried to send different commands instead of hello, for example, flag or ls or cat flag, with no luck. Then I’ve tried not to send la revedere or send it where it is not needed. Still with no luck, the server is just ignored my commands. Then I gave up.

UPD:

After p4 team shared their solution, I realized that I needed to send part1 flag instead of hello. And this is not stupid guessing since this string was hardcoded and obfuscated in the binary itself, but never executed. So I would need to do more static analisys, to get the flag, that was my mistake.

UPD2:

Here is the script that dumps obfuscated strings from asm listing. As you can see part1 flag is among of them:

[*] extracted: waitpid
[*] extracted: /etc/krb5.conf
[*] extracted: open
[*] extracted: read
[*] extracted: close
[*] extracted: domain.google.com
[*] extracted: strcasestr
[*] extracted: __stack_chk_fail
[*] extracted: 4242
[*] extracted: mlwr-part1.ctfcompetition.com
[*] extracted: getaddrinfo
[*] extracted: signal
[*] extracted: socket
[*] extracted: connect
[*] extracted: close
[*] extracted: socket
[*] extracted: setsockopt
[*] extracted: htons
[*] extracted: bind
[*] extracted: close
[*] extracted: listen
[*] extracted: send
[*] extracted: send
[*] extracted: send
[*] extracted: send
[*] extracted: strlen
[*] extracted: recv
[*] extracted: recv
[*] extracted: recv
[*] extracted: recv
[*] extracted: hello
[*] extracted: part1 flag
[*] extracted: stream
[*] extracted: recv
[*] extracted: send
[*] extracted: __stack_chk_fail
[*] extracted: close
[*] extracted: close
[*] extracted: close
[*] extracted: close
[*] extracted: accept
[*] extracted: waitpid
[*] extracted: __errno_location
[*] extracted: close
[*] extracted: syscall
[*] extracted: close
[*] extracted: waitpid
[*] extracted: __errno_location
[*] extracted: hello
[*] extracted: exec
[*] extracted: exec
[*] extracted: strlen
[*] extracted: strlen
[*] extracted: syscall
[*] extracted: error
[*] extracted: sh
[*] extracted: -c
[*] extracted: stream
[*] extracted: dup2
[*] extracted: dup2
[*] extracted: dup2
[*] extracted: -c
[*] extracted: sh
[*] extracted: /bin/sh
[*] extracted: execl
[*] extracted: syscall
[*] extracted: waitpid
[*] extracted: __errno_location
[*] extracted: la revedere
[*] extracted: bg
[*] extracted: bg
[*] extracted: strlen
[*] extracted: strlen
[*] extracted: system
[*] extracted: ok
[*] extracted: rm
[*] extracted: /proc/self/exe
[*] extracted: readlink
[*] extracted: unlink
[*] extracted: ok
[*] extracted: kill
[*] extracted: syscall
[*] extracted: error
[*] extracted: upgrade
[*] extracted: error
[*] extracted: ok
[*] extracted: close
[*] extracted: nop
[*] extracted: ok
[*] extracted: error
[*] extracted: __stack_chk_fail
[*] extracted: close
[*] extracted: sleep
[*] extracted: gettimeofday
[*] extracted: getpid
[*] extracted: getppid
[*] extracted: ELF
[*] extracted: send
[*] extracted: syscall
[*] extracted: syscall
[*] extracted: mmap
[*] extracted: syscall
[*] extracted: syscall
[*] extracted: clone
[*] extracted: munmap