오랜만에 글을 올린다. 그 이유는 해킹 공부 이외 공부를 수행하느라, 글을 쓸 여유가 없었던 것이다.
서론
전체 일반부에서 33등을 차지했다.
처음 나가는 외국 CTF이고, 문제 스타일을 알 수 있어서 유용했다. 나갈 생각이 없었지만 친구의 권유로 참가하게 되었다.
리버싱 문제만 풀었는데, 리버싱 문제는 대체로 쉬웠던 것 같다. 하지만 이 문제를 다 풀어도 돌아오는 혜택이 없다는 것을 인지한 터라 열심히 참가를 하지 않아서, 결국 올킬을 못하고 2개의 문제를 남겼다.
aplet321
연립방정식을 풀어서 please의 개수, pretty의 개수를 맞춰 조건에 맞게 보내면 된다. 익스 코드 안 짰다.
lactf{next_year_i'll_make_aplet456_hqp3c1a7bip5bmnc}
the-secret-of-java-island
jar 파일이 주어지는데 이것을 jadx에서 디컴파일한 뒤 보면 'd', 'p'를 적절히 배열한 결과의 sha256 값이 특정 값과 일치하는지 비교한다. 알맞은 배열을 찾은 뒤 서버로 보내면 된다. 익스 코드 안 짰다.
lactf{the_graphics_got_a_lot_worse_from_what_i_remembered}
flag-finder
CTF 치고 게임을 은근 잘 만들었다. 플래그처럼 생긴 객체에 말을 하면 방 안에 'key'가 있고 'key'를 가져온다면 플래그를 준다고 한다. 여기서 다른 양배추같은 아이템을 가져다 주면 'Wrong item... smh'라는 문자열이 재생되는 것으로 보아, 'key'는 일종의 아이템이라고 생각할 수 있다.
GameMaker로 만들어진 바이너리인 것을 data.win의 존재로 확인할 수 있다. 따라서 모딩 툴인 UndertaleModTool로 data.win을 뜯어 본다면 다음과 같이 key가 gameMain 방 안에 숨겨져 있는 것을 확인할 수 있었다.
여기서 또한 key의 초기 X좌표와 Y좌표를 볼 수 있는데, 각각 1152, -800이였다.
이곳은 게임에서 정상적으로 갈 수 없는 곳이므로 초기 좌표를 바꾼 뒤 플래그 오브젝트에 전달하면 플래그가 등장한다.
glottem
쉘 파일이지만 동시에 Python 파일이기도 하고, Javascript 파일이기도 하다.
두 가지 모두 각각 다른 연산을 실행하여 특정 값과 비교하는데, Python 루틴일 때 비교하는 범위가 [6, len(s-2)) 이고, 최종 플래그가 34글자라는 것을 보아 처음 6글자가 "lactf{" 이고, 마지막 글자가 "}"라는 것을 어렵지 않게 유추할 수 있다.
그렇다면 len(s-2)가 아니라 len(s-1)이여야 하지 않냐라고 물을 수 있지만 연산 내에서 s[i]와 s[i+1]을 연산에 사용하기 때문에 실질적인 범위는 len(s-1)이다.
Python 루틴에서 중요한 점은 비교하는 숫자가 다른 것이 아니고 260인 것에 있다. 연산을 수행하고 나온 d의 최솟값이 260이므로, 이를 이용한 dfs로 만족하는 플래그 후보를 검색한 후, Javascript 루틴에서 알맞은 플래그를 검열하면 된다.
e = ...
def dfs(idx, now):
if idx == 32:
return [ [now] ]
ret = []
for nxt in range(27):
if e[idx - 6][now][nxt] <= 10:
res = dfs(idx + 1, nxt)
for r in res:
r = [ now ] + r
ret.append(r)
return ret
alpha = b"abcdefghijklmnopqrstuvwxyz_"
for i in range(27):
res = dfs(6, i)
for r in res:
k = b'lactf{'
for i in r:
k += alpha[i].to_bytes(1, 'big')
k += b'}'
assert(len(k) == 34)
d = 0
for a in range(34):
d = (d * 31 + k[a]) % 93097
if d == 61343:
print(k)
exit(0)
lactf{solve_one_get_two_free_deal}
meow meow
바이너리를 실행하게 되면 그냥 Input을 받고 끝나는 것을 알 수 있는데 따라서 IDA로 분석해 보았다.
main 함수에서 발견한 것은 Input을 어떠한 규칙에 따라 정수로 변환하고 그것을 저장한 후, 5글자 단위로 쪼개서 그것을 병렬 쓰레드로 돌리는 것이였다.
병렬 쓰레드 루틴 속 대충 중요해 보이는 루틴이 있었는데 그것은 다음과 같았다.
data{x} 파일을 연 뒤 트리를 탐색하는 것처럼 Input 5조각에 따라 탐색한 뒤 그것이 0x63617400과 같은지 비교한다.
똑같이 코드를 짜서 복구할 수 있다.
from pwn import *
with open('data1', 'rb') as f:
data = f.read()
origin = data.index(b'\0tac')
alpha = "abcdefghijklmnopqrstuvwxyz_{}"
while True:
nxt = origin // 116
dat = alpha[(origin - (origin // 116 * 116)) // 4]
print(dat, end='')
origin = data.index(p32(nxt))
if nxt == 0:
break
lactf{meow_you_found_me_epcsihnxos}
rbp
참신한 문제였다. 일단 이 문제도 똑같이 입력만 받고 끝나는 것을 볼 수 있는데, 따라서 바로 IDA를 통한 분석에 돌입했다.
표준 리눅스 함수가 특이한 방식으로 strip되어있었다.
하지만 결국 이 모든 것을 분석하지 않아도 strip된 함수와 표준 함수가 일대일대응이 된다는 점에서 착안하여 함수에 들어가는 인자를 바탕으로 함수를 추론할 수 있다.
이렇게 작업을 마치고 나면 main의 함수에서 SIGSEGV가 일어났을 때에 사용자 정의 함수를 실행하게 sigaction 함수를 사용하는데, 그 함수의 개형은 다음과 같다.
byte_70B00에 대해 처음부터 Size만큼 오른쪽으로 한칸 카이사르 암호를 돌리는 것을 알 수 있다.
더 주목할 점은 섹션 세 개가 추가로 들어간 점인데, 각각 .r, .b, .p이다. 각각의 섹션에는 한 개의 대응하는 함수가 있으며, 각각 sub_68000, sub_69000, sub_70000으로 명명한다. 이제 메인 체크 함수에 들어가보고 스키밍을 한 결과 중요한 루틴을 발견했다.
sub_711A1에서는 ResultIn68000의 값에 따라 .r, .b, .p 중 한 개의 섹션에 Execute 권한을 부여하고, 나머지 섹션들에게서 Execute 권한을 박탈한다. ResultIn68000이 0이면 .r에, 1이면 .b에, 2이면 .p에 Execute 권한을 부여한다. 다음으로 저 체크를 모두 통과하지 못하면 체크에 실패하고 프로그램이 종료하게 된다.
중요한 점은 저 각각의 체크 함수에서 무조건 1개만 Execute 권한이 있고 2개는 Execute 권한이 없다는 것이다. Execute 권한이 없는 섹션에서 실행할 경우 SIGSEGV 시그널이 발생하게 되는데 이의 결과로 byte_70B00이 두 칸 돌아가게 된다. 따라서 한 번 저 반복문을 돌 때마다 byte_70B00이 두 칸 돌아가게 된다는 것이다. 이제 각각의 체크 루틴에 들어가 보면,
들어간 문자가 소문자인지 확인한 후 byte_70B00의 Size를 늘리고 저장한다. 그리고 다음 조건을 만족하면 ResultIn68000은 1 또는 2로 변경된다.
굳이 다른 루틴을 사진과 함께 설명하지 않겠다. sub_69000은 들어간 문자가 숫자인지 확인하고 이것을 미리 정해진 숫자와 확인한 뒤 틀리면 실패 처리하고, 맞으면 정답 처리한다.
sub_70000은 문자가 '_'인지 비교한다. 틀리면 실패 처리, 맞으면 정답 처리한다.
이제 이에 대해 정연산을 구현할 것인데 왜 정연산을 구현하냐면, 역연산을 짜기엔 너무 복잡하고 어짜피 하나의 문자가 하나의 체크 결과에 대응하기에 가능하다. 그리고 사실 구현할 필요도 없이 그냥 바이너리를 디버깅하면서 하나하나 알아내주면 된다.
#lactf{rub1sc0_b4s3d_ph0t0synth3s1s}
def translate(whatATrans, to):
step = ord(whatATrans) - ord('a')
fr = ord(to) - step
if fr < 97:
return fr + 26
else:
return fr
print(chr(translate('k', 'r')))
lactf{rub1sc0_b4s3d_ph0t0synth3s1s}
후기
다른 친구들도 모두 잘한다. 그렇기에 33위에 랭크될 수 있었던 것 같다.. 이제 디미고 2학년이 되기 때문에 다양한 면에서 더 열심히 공부해야 할 것 같다. 방학 동안 수학 I, II를 모두 끝냈기 때문에 이제 방학 남은 동안 해킹을 좀 더 할 수 있을 것 같다. 다음 포스트는 커널을 좀 더 공부한 것을 올리려고 했지만 먼저 이를 활용한 작품을 올릴 예정이다.
총 두 개인데, 첫째는 VAC를 커널 모드에서 Bypass한 것과 VAC 자체를 분석하여 유저 모드에서 Bypass한 것,
둘째는 osu!lazer의 보안에 사용되는 osu.Game.Auth와 AuthNative.dll을 커널 모드에서 Bypass한 것과 자체를 분석하여 유저 모드에서 Bypass한 뒤 핵을 개발한 일지를 작성할 것이다.
다음 Windows 커널 캘린더로는 아마 커널의 함수 또는 기능을 들여다 보면서 커널 안티치트에게서 몸을 숨기는 방법을 공부하도록 하겠다.
'Security - Player > CTF' 카테고리의 다른 글
CCE Jr 1st (1) | 2024.09.14 |
---|---|
Dreamhack Invitationals 후기 (1) | 2024.05.28 |
osu!gaming CTF 2024 Reversing Write-Up (0) | 2024.03.04 |
Dreamhack CTF Season 5 Round #1 Write-Up (0) | 2024.01.06 |