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()
계열의 특성이다.
- 버퍼에 있는 데이터가
nbytes
보다 작은 경우 모두 읽어오고 종료 - 버퍼에 있는 데이터가
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