Post

STDIN \_IO\_FILE structure <\_IO\_2\_1\_stdin\_> & fgets VS argv

STDIO

Note )
파이프와 STDIO는 다르다!

파이프는 키보드나 모니터같은 하드웨어 장치와 프로세스를 연결하는 방법이 아니라, 한 프로세스의 stdout을 다른 프로세스의 stdin으로 연결하는 IPC 방법이다. 따라서 STDIO는 pipe 구조로 이루어져 있는 것이 아니다! pipe에 접근하든 STDIO에 접근하든 file philosophy에 의해 fd를 이용해 접근하기 때문에 착각할 수 있음.

파이프로 리다이렉션 하지 않는 경우 stdin, stdout, stderr은 모두 같은 character device를 가리키면서 , read-only가 아니다. 따라서 stdin을 이용해 출력하거나, stdout을 이용해 데이터를 쓰는 것이 가능하다. * stdio를 PIPE로 리다이렉션 하는 경우는 device를 가리키는게 아니라 PIPE를 가리키기 때문에 서로를 구분하게 된다.

1
2
3
4
5
6
7
8
9
10
11
umbum:~/workspace/pwn/vdso (master) $ ll /dev/std\*
lrwxrwxrwx 1 root root 15 Sep  2 05:01 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Sep  2 05:01 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Sep  2 05:01 /dev/stdout -> /proc/self/fd/1
umbum:~/workspace/pwn/vdso (master) $ ll /proc/self/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Sep  2 07:29 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Sep  2 07:29 ../
lrwx------ 1 ubuntu ubuntu 64 Sep  2 07:29 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Sep  2 07:29 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Sep  2 07:29 2 -> /dev/pts/1

fgets의 stdin 처리

1
2
3
4
5
6
7
8
9
10
11
12
fgets 호출 부분 :
0x080484d4 <main+80>:   sub    $0x4,%esp
0x080484d7 <main+83>:   pushl  0x8049788
0x080484dd <main+89>:   push   $0x400
0x080484e2 <main+94>:   lea    0xfffffae8(%ebp),%eax
0x080484e8 <main+100>:  push   %eax
0x080484e9 <main+101>:  call   0x804838c <\_init+56>
(gdb) x/x 0x8049788
0x8049788 <stdin@@GLIBC\_2.0>:   0x0083f720
(gdb) x/4x 0x0083f720
0x83f720 <\_IO\_2\_1\_stdin\_>:      0xfbad2288      0xf6ffe006      0xf6ffe006     0xf6ffe000

* fgets 호출 이후 시점의 메모리 상태.

_IO_FILE structure

stdin 객체 <\_IO\_2\_1\_stdin\_>\_IO\_FILE type이다. \_IO_FILE은 /usr/include/libio.h 에 정의되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct \_IO\_FILE {
int \_flags; /\* High-order word is \_IO\_MAGIC; rest is flags. \*/
#define \_IO\_file\_flags \_flags


/\* The following pointers correspond to the C++ streambuf protocol. \*/
/\* Note: Tk uses the \_IO\_read\_ptr and \_IO\_read\_end fields directly. \*/
char\* \_IO\_read\_ptr; /\* Current read pointer \*/
char\* \_IO\_read\_end; /\* End of get area. \*/
char\* \_IO\_read\_base; /\* Start of putback+get area. \*/
char\* \_IO\_write\_base; /\* Start of put area. \*/
char\* \_IO\_write\_ptr; /\* Current put pointer. \*/
...
}

첫번째는 flag, 두번째는 현재 읽어야할 위치, 세번째는 get area의 끝, 네번째는 get area의 시작점이다.

get area 가 흔히 말하는 입력 버퍼다. ( fgets 수행 이후에는 버퍼를 읽었기 때문에 두번째와 세번째값이 같아진다. ) stdin으로 넘긴 데이터는 네번째 entry ~ 세번째 entry 공간에 존재한다는 것을 알 수 있다.

입력 버퍼 초기화

fgets가 리턴하기 전에 stdin의 입력 버퍼 데이터를 초기화해주고 리턴하는게 맞는걸까? 아니면 퍼미션으로 제어해야 하는걸까? 사실 \_IO_FILE구조체는 fgets에서 인자로 넘어왔을 뿐이니까, fgets와는 관련이 없다. 또한fflush(), rewind(), while (getchar() != '\n'); 같은게 입력 버퍼 초기화 방법으로 알려져 있지만, 사실 입력 버퍼 자체를 초기화 하는게 아니다. 이는 \_ IO_FILE의 현재 읽어야 하는 위치인 두번째 엔트리 값을 get area의 끝을 나타내는 세번째 엔트리 값과 동일하게 만드는 역할을 할 뿐이지, get area 자체를 초기화 하는 작업은 하지 않는다. (fgets는 개행까지 읽는데, scanf 등을 사용할 때 끝에 개행문자가 남는 경우 사용하게 된다.) 정 초기화 하고 싶다면 직접 stdin 파일기술자를 이용해 데이터를 덮어 쓰거나 memset()을 사용할 수 있다. 아무튼 이런 저런 이유로 직접 초기화하기 보다는 퍼미션으로 제어하도록 되어 있는데, 실행 권한이 없더라도 고정된 위치에 data가 남는다는 점을 이용해 파라미터를 넘기는데 활용할 수 있다. 따라서 결국은 ASLR로 주소를 특정하지 못하게 하는 방법을 사용해야 한다.

fgets / read / argv

strcpy()는 origin data에 0x00이 있는지만 따져보면 된다.
참고 ) strcpy()시 알아서 ` 0x00`을 넣어준다.

fgets

  • 입력 데이터에 0x0a가 있는지 따져보아야 한다.
  • EOF 또는 개행문자(0x0a)를 입력하지 않는 경우 계속 입력을 대기하기 때문에 특히 소켓을 사용할 때 끝에 \n 을 보내주어야 한다. 이 때 입력한 \n(0x0a) 까지 포함해서 받기 때문에 당연히 문자열 끝에 0x00이 들어갈 거라고 생각하면 안된다.
  • EOF와 개행문자(0x0a)만 인식하므로 공백(0x20)이나 0x00도 모두 받을 수 있다.
\x00

제대로 들어간다.

1
2
$ python -c 'print "abc"+"\x00"+"def"' | ./2
0xfee84cf0  61 62 63 00 64 65 66 0a 00 4d e8 fe 56 86 04 08   abc.def..M..V...
\x20

제대로 들어간다.

1
2
$ python -c 'print "abc"+"\x20"+"def"' | ./2
0xfee8b390  61 62 63 20 64 65 66 0a 00 b3 e8 fe 56 86 04 08   abc def.....V...
\x0a

\x0a까지만 들어간다.

1
2
$ python -c 'print "abc"+"\x0a"+"def"' | ./2
0xfefa6e90  61 62 63 0a 00 00 00 00 b8 6e fa fe 56 86 04 08   abc......n..V...

read

fgets()와 같이 stdin으로 받지만, read()는 개행 문자와 관련 없이 최대한 읽어들인다. 즉, 모든 stdin이 개행까지 받는다고 생각하면 안된다. 개행까지 받는건 gets()계열의 특성이다.

  1. 버퍼에 있는 데이터가 nbytes 보다 작은 경우 모두 읽어오고 종료
  2. 버퍼에 있는 데이터가 nbytes 보다 큰 경우 nbytes만큼 읽어오고 종료
1
2
3
4
$ python -c 'print "a\x0ab"' | ./gets
61 a 0 0 0
$ python -c 'print "a\x0ab"' | ./read
61 a 62 a 0

argv

  • 입력 데이터에 0x00, 0x20, 0x0a가 있는지 따져보아야 한다.

Note ) argv에 \x00넘기는 법 : bash ./aaa ""
\x20 | \x0a도 같은 방법으로 넘기면 된다.

\x00

\x00은 그냥 무시하고 끝까지 들어간다.

1
2
3
$ ./1 `python -c 'print "abc"+"\x00"+"def"'`

0xfef1cc41  61 62 63 64 65 66 00 48 4f 53 54 4e 41 4d 45 3d   abcdef.HOSTNAME=
\x20

\x20이 \x00으로 바뀌지만 끝까지 들어간다.

* \x20을 argv 구분자로 사용하기 때문. 즉 argv[1] == "abc", argv[2] == "def"

1
2
3
$ ./1 `python -c 'print "abc"+"\x20"+"def"'`

0xfef4fc40  61 62 63 00 64 65 66 00 48 4f 53 54 4e 41 4d 45   abc.def.HOSTNAME
\x0a

\x0a가 \x00으로 바뀌지만 끝까지 들어간다.

* \x20과 같다.

1
2
3
$ ./1 `python -c 'print "abc"+"\x0a"+"def"'`

0xfef01c40  61 62 63 00 64 65 66 00 48 4f 53 54 4e 41 4d 45   abc.def.HOSTNAME
This post is licensed under CC BY 4.0 by the author.