Backdoor CTF
2023.12.16 21:00 ~ 2023.12.18 21:00 기간동안 jeopardy-style의 Backdoor CTF가 진행되었다
총 50문제 중 10문제를 풀었고 우리팀은 787팀중 141등으로 마무리했다 ㅎㅎ
팀원분들 모두 하루정도만 참여가 가능했던 점이 아쉬웠다
좀더 오래 참여했다면 100등 안에도 들었을 것 같다
제일 처음 해결한 문제는 이미지 분석 문제이다
Begginer 라는 문제태그가 쉬워보여서 이 문제들 위주로 해결하고자 했다
문제 파일이 제공되었고 이걸 열어보면 이미지 파일 하나가 확인된다
바로 HxD에 넣어서 확인해보면 flag를 찾을 수 있다
그 다음은 간단한 웹문제를 풀었다
문제파일이 제공되었고 주소도 제공되었다
writeup을 작성하는 지금은 서버가 닫혀 Docker로 실행시켜서 스크린샷을 찍었다
Docker를 통해 해당 문제환경을 구축하는법을 간단히 적어보면
우선 Docker Desktop을 설치해주고 실행한다
문제파일의 Dockerfile이 존재하는 경로를 cmd에서 이동한 후 docker-compose up
명령어를 입력해주면 된다
이후 제공된 포트로 이동하면 된다
url을 확인해서 해당 엔드포인트의 기능을 대충 유추해보자
http://localhost:4053/read_secret_message?file=message
file이라는 파라미터를 통해 입력값을 전달받고 화면에 어떤 메세지가 출력되는걸 보면
파일의 경로나 이름을 입력하면 서버가 이를 읽어들여 해당 파일의 내용을 화면에 출력해주는 기능이라고 생각했다
이 정보를 가지고 문제 코드를 확인해보자
역시 예상했던대로 file파라미터를 통해 전달받은 경로로 파일을 읽어와서 return해주는 걸 확인할 수 있다
하지만 중간에 file_param = useless(file_param)
코드를 통해 무언가 입력값을 필터링 할거라는 생각이 들어 확인해봤다
path travelsal을 방지하기위해 .
과 /
를 공백으로 치환하는 함수가 정의되어있고 이를 2번까지 실행한다
이런경우 url의 encoding특성을 이용해서 %를 hex값으로 치환하여 doubleEncoding으로 필터링을 우회한다
.
= %2E
/
= %2F
%
= %25
%2E
= %252E
%2F
= %252F
해당 문제에서는 이런 double encoding공격을 막고자 useless함수를 정의해서 필터링 하고있다
하지만 이걸 3번 인코딩 해준다면 필터링을 우회할 수 있다
.
-> %2E
-> %252E
-> %25252E
즉, ../flag.txt
를 %25252E%25252E%25252Fflag%25252Etxt
로 입력해주면 된다
그나마 자신이 있는 웹 문제를 풀어보려 도전한 문제다
문제코드가 제공된 웹 문제를 찾다보니 이 문제를 고르게 된 것 같다
로그인 페이지와 이걸 깰 수 있냐는 문구가 적혀있다
아마도 SQL injection문제라고 생각된다
username과 password를 post방식으로 전달받아 sql query를 실행한 결과가 존재하고, $mysupersecurehash = md5(2*2*13*13*((int)$password));
내가 입력한 password가 해당 연산과정과 hash처리를 거쳐 하드코딩된 hash 값과 일치하면 You win문구와 결과데이터를 출력해준다
하지만 이 조건을 만족하지 못할 경우 Wrong password문자열만 출력해주기 때문에 SQL query결과는 확인할 수 없다
그래서 이 부분에서는 sql injection이 불가능하다고 판단했고 코드의 다른부분을 살펴봤다
user파라미터를 GET방식을 통해 전달 받은 후, 그 값이 all이 아니면 입력값을 기반으로 query를 실행하고 데이터를 출력해준다
여기부분도 마찬가지로 입력값을 필터링없이 query에 넣기 때문에 SQL injection이 가능하다
SQL injection을 진행하기 전에 데이터가 어떻게 구성되어있는지 확인해보자
반복문을 통해 499개의 dummy데이터를 넣어주고 flag가 들어간 하나의 데이터가 존재한다
flag가 들어간 데이터 중 bio부분에는 flag가 text형태로 그대로 들어가 있다
그러면 SQL injection을 통해 bio를 확인하게 된다면 flag를 찾을 수 있다는 것이다
일단 user=all을 통해 데이터를 확인해보자
결과 데이터를 index, username, password 까지만 보여주고 bio는 보여주지 않는다
그래서 SQL injection을 통해 row의 순서를 바꿔서 bio를 앞에 둔다면 Flag를 찾을 수 있다
payload는 다음과 같다
admin0' union select bio, username, password from users where '1'='1
다른 웹 문제가 너무 어려워서 포기하고 리버싱으로 넘어왔다
문제파일을 열어봤더니 ELF라고 적혀있어 바로 ida에 넣었다
main함수의 코드를 확인해보면 배열이 하나 선언되어있고 fgets를 통해 사용자로부터 입력값을 받아 어떠한 조건을 확인하는걸 알 수 있다
fun_1함수로 들어가서 어떤 조건인지 확인을 해보자
strlen(a1) == 32
로 입력한 문자열이 32글자인지 확인을 한다
그 후 반복문을 통해 a[i] ^ a[31-i] = a2[i]
를 체크한다
그다음 flag{c4n't_HESOY
문자열을 선언하고 a[i]
와 일치하는지 확인한다
여기서 알 수 있는점은
XOR은 교환법칙이 성립하니까 a1[31-i] = a1[i] ^ a2[i]
임을 알 수 있고
a1[0]~a1[15]
를 안다면 a1[15]~a1[31] = a1[0]~a1[15] ^ a2[i]
를 통해 값을 구할 수 있다
a1[0]~a1[15] == 'flag{c4n't_HESOY'
이므로 간단한 파이썬 코드를 통해 플래그 뒷부분을 구할 수 있다
payload
a1 = 'flag{c4n\'t_HESOY'
v4 = [27, 25, 81, 30, 36, 13, 0, 13, 120, 65, 110, 32, 114, 12, 2, 24]
result = [ord(a) ^ v for a, v in zip(a1, v4)]
for a in result:
print(chr(a))
이제 앞부분과 뒷부분을 합치면 플래그가 완성된다
이건 도구를 돌렸더니 플래그가 나와서 문제를 풀었다고는 못하겠다;;
암호학문제도 풀어보고 싶어서 도전한 문제다
output.txt와 script.py가 제공된다
코드 아래쪽을 보면 bytes_to_long(flag)
를 통해 m
을 생성한다
그리고 pow(m, RSA_E, n)
을 통해 c
를 생성한다
RSA_E
는 위에 3
으로 선언되어 있고 n
도 output.txt
를 통해 제공되었으니 pow
의 기능만 알면 m
을 알아내서 flag
를 알 수 있을거라 생각했다
pow(a, b, c)
는 a를 b번 거듭제곱하여 c로 나눈 나머지를 반환해주는 함수라고한다
그러면 pow(m, RSA_E, n)
은 m을 3제곱하여 n으로 나눈 나머지 값이 c라는 걸 알 수 있다
output.txt를 확인해보면 n이 c보다 압도적으로 큰 수라는걸 알 수 있다
그러니 m을 세제곱한 값이 n보다 작을것이라 생각하고 m^3 / n = m^3 = c 라고 생각했다
그래서 ∛c 을 하면 m을 구할 수 있을거라 생각했다
from Crypto.Util.number import long_to_bytes
def root(y):
x = y**(1/3)
return x
y = 5926440800047066468184992240057621921188346083131741617482777221394411358243130401052973132050605103035491365016082149869814064434831123043357292949645845605278066636109516907741970960547141266810284132826982396956610111589
result = int(root(y))
print(long_to_bytes(result))
∛c을 계산하는 코드를 작성한 후 실행해봤다
flag{R
까지는 나오지만 그 뒤는 나오지 않았다
이 방법은 아니라고 판단하고 RSA관련한 writeup을 찾아보던 중 RsaCtfTool을 사용하면 rsa크랙이 가능하다는걸 봤다
그래서 다운로드하고 프로그램을 실행시켰더니 다음과 같이 플래그가 나왔다
암호학은 어려워서 다시 리버싱 문제로 돌아왔다
main함수의 디스어셈블코드이다
첫번째 조건은 argc != 2
이다. 파일을 실행할때 인자값을 통해 입력을 받는다는걸 알 수 있다
두번째 조건은 strlen != 17
이다. 입력값이 17가 되어야 한다는걸 알 수 있다
그 다음 배열이 선언되어있고 while문을 통해 v8
문자열을 만드는걸 확인할 수 있다
그리고 "ThatsHardcoded!!!"
문자열을 v9
에 넣어준 다음 func_4
, func_3
을 통해 어떠한 연산과정을 거친 다음 func_2
의 결과값에 따라서 Wrong door
를 출력하거나 func_1
함수를 실행한다
우선 func_1
이 무슨 기능인지 살펴보자
제공된 파일이 chall.out과 encoded.bin 두개였다
지금 ida에 넣은 파일은 chall.out파일이다
코드를 살펴보면 encoded.bin 파일을 읽어들여서 어떤 연산과정을 거친 후 flag가 적힌 the_door.jpg를 만들어 주는거라고 추측했다
그러니 func_2
만 만족한다면 플래그는 프로그램이 만들어주는 이미지 파일로 확인할 수 있다고 생각했다
func_2
func_4
, func_3
를 실행한 후의 결과가 위 조건을 만족해야한다
특이한 점은 a1[1],a1[3]처럼 어떤 값을 만족하는것이 아니라 다른원소가 특정값이 아니어야 해당조건을 만족한다는 것이다
이러면 지정된 조건을 역연산하여 입력값을 알아내기는 힘들다
즉, 해당 조건을 모두 만족하는 입력값은 여러가지의 경우가 존재한다는 말이다
func_4과 func_3를 더 확인해보자
func_3
v4[i] = a1[i] ^ a2[i]
를 통해 v4배열을 만든 후 return해주는 역할이다
func_4
func_3과 차이가 없다
v5[i] = a1[i] ^ a2[i]
를 통해 v5배열을 만든 후 return해주는 역할이다
이제 모든 함수의 기능을 확인했으니 이를 똑같이 파이썬 코드로 작성해서 입력값을 찾으면 된다
그전에 v8을 먼저 만들어보자
LOBYTE는 하위 8바이트의 값만 가져오는 기능이라고 한다
이 과정을 직접 하기위한 코드는 아래와 같다
def enc(a1):
return [(a1[i] ^ (i + 17)) & 0xFF for i in range(len(a1))]
v12 = [66, 119, 101, 113, 123, 98, 114, 125, 119, 89, 115, 125, 111, 109, 62, 1, 0]
v8 = enc(v12)
result = ""
for i in v8 :
result = "".join([result,chr(i)])
print(result)
v8은 SeventeenChars!!!
라는걸 알 수 있다
v8을 알았으니 다음 과정으로 넘어가자
이 두가지 과정을 파이썬 코드로 작성하면 아래와 같다
def func_3(a1, a2):
return [(a1[i] ^ ord(a2[i])) for i in range(len(a2))]
def func_4(a1, a2):
return [(ord(a1[i]) ^ ord(a2[i])) for i in range(len(a1))]
def main():
v8 = "SeventeenChars!!!"
v9 = "ThatsHardcoded!!!"
argv_input = input("Enter argv (17 characters): ")
if len(argv_input) != 17:
print("Invalid input. Please enter 17 characters.")
return
v3 = func_4(v8, argv_input)
v7 = func_3(v3, v9)
print("Input argv:", argv_input)
print("Output after func_4:", v3)
print("Output after func_3:", v7)
if __name__ == "__main__":
main()
아무 입력값이나 일단 넣어보고 결과를 확인해봤다
마지막 3글자는 a의 ascii코드값은 97이 그대로 나왔다
func_2에서 마지막 3글자는 그냥 ascii코드 그대로 넣으면 된다는 뜻이다
그리고 첫번째 글자는 78이 나와야 한다
현재 a를 넣어 102가 나왔으니 ascii코드값으로 26을 빼면 I가 나온다
이제 이런식으로 계속 한글자씩 찾아보자
아까와 같은 논리대로 a1[2]==120이 되기위해 a보다 2큰 c를 넣었는데 오히려 값이 줄어들었다
여기서부터는 그냥 brute force로 찾아내야하는 것 같다
알파벳이 대략 27개니 대문자까지 합치면 54번정도, 이 중 12~13글자만 알아내면되니 총 500~600번 정도만 입력하면 된다
약 한두시간정도만 투자하면 입력값을 알아낼 수 있다는 것이다
이렇게 한글자씩 다 알아낸 결과는 다음과 같다
이 문자열을 프로그램을 실행할때 인자값으로 넘겨주면 된다
그 결과 하위 목록을 ls로 확인해보면 the_door.jpg
가 생성된 것을 확인할 수 있다
해당 사진을 열어보면 flag를 얻을 수 있다
이번문제는 링크를 통해서 파일을 다운받는다
apk파일을 리버싱하는 문제인가보다
바로 ida에 넣었지만 PE파일이 아니라며 실행이 안된다
그래서 apk 리버싱 하는법을 검색했더니 다음과 같은 결과를 확인했다
1번과정은 알집으로 압축해제하면 되지만 2, 3번과정은 도구를 다운받아서 진행해야한다
dex2jar.bat가 위치한 경로로 이동해 준 후 dex파일이 존재하는 경로로 아래 명령어를 입력해준다
d2j-dex2jar.bat --force C:\Users\kim\Desktop\DreamHack\BackdoorCTF\opensesame\public\open_sesame\classes.dex
그러면 bat파일을 실행한 위치에 jar파일이 생성된다
생성된 jar파일을 jd-gui.exe로 실행을 해보자
이런 화면이 나오게 되는데 java package와 같은 모양으로 나온다
이 중 com이라는 package가 아마도 사용자가 만들어낸 package로 추정된다
클릭해서 자세히 알아보자
username과 password를 체크한 후 어떤 연산과정을 거쳐 flag를 출력해주는 걸 확인할 수 있다
username과 password가 하드코딩되어있다
이 apk를 실행해서 username과 password를 입력하면 flag를 출력해준다는걸 확인했으니 실행을 해보자
하지만 에뮬레이터를 설치하고 apk를 또 전달해주기는 귀찮으니 gpt한테 파이썬 코드로 바꿔달라고 하자
class MainActivity:
valid_password = [52, 108, 49, 98, 97, 98, 97]
valid_user = "Jack Ma"
def __init__(self):
self.buttonLogin = None
self.editTextPassword = None
self.editTextUsername = None
def flag(self, param1, param2):
result = []
for b in range(len(param2)):
result.append(chr(ord(param2[b]) ^ ord(param1[b % len(param1)])))
return ''.join(result)
def it4chi(self, param):
return [ord(param[b]) for b in range(len(param))]
def n4ut1lus(self, param):
array_of_int = self.it4chi(param)
if len(array_of_int) != len(self.valid_password):
return False
for b in range(len(array_of_int)):
if array_of_int[b] != self.valid_password[b]:
return False
return True
def sh4dy(self, param):
result = []
for b in range(len(param)):
c = param[b]
if c.isdigit():
result.append(c)
return ''.join(result)
def showToast(self, param):
print(param) # 실제 Android의 Toast 대신 print를 사용
def sl4y3r(self, param):
return int(param) - 1
def validateCredentials(self):
str1 = input("Enter username: ").strip()
str2 = input("Enter password: ").strip()
if str1 == self.valid_user and self.n4ut1lus(str2):
i = self.sl4y3r(self.sh4dy(str2))
print(f"flag{{{self.flag(str(i), 'U|]rURuoU^PoR_FDMo@X]uBUg')}}}")
else:
self.showToast("Invalid credentials. Please try again.")
def onCreate(self):
self.validateCredentials()
# 액티비티를 생성하고 onCreate 메서드를 호출
main_activity = MainActivity()
main_activity.onCreate()
무료버전치고 성능이 좋다
username과 password를 입력하니 flag를 얻을 수 있었다
이번 대회는 난이도가 낮은 문제가 많아서 푸는 재미가 있었던 대회다
다음에는 실력을 더 키워서 100위 안으로 마무리할 수 있으면 좋겠다