ptrace - Linux injection ( code injection / so injection )
리눅스는 윈도우의 CreateRemoteThread()같은 API를 제공하지 않기 때문에, injection에 ptrace()를 이용한다.
타인의 권한으로 실행된 프로세스에는 TRACEME와 ATTACH가 불가능하기 때문에 동작하지 않는다. 타인 소유 파일을 내가 실행하는 경우는 동작한다.
code injection
특정 실행파일에서 원하는 정보를 추출하거나 원하는 동작을 하게 하는 데 유용하게 쓰일 수 있다. code injection을 통해 target 프로세스가 system("echo Code inject");을 호출하게 해 “Code inject” 문자열을 출력하도록 해보았다. Ubuntu 15.04에서 진행했다.
실행 흐름을 injection code 쪽으로 돌리기 위해서 eip를 조정해야 하므로 target의 레지스터를 담을 구조체가 필요하다. 보통 레지스터를 담을 구조체로 user\_regs_struct를 이용하며, sys/user.h에 architecture 별로 정의되어 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/user.h>
struct user\_regs\_struct
{
long int ebx;
long int ecx;
long int edx;
long int esi;
long int edi;
long int ebp;
long int eax;
long int xds;
long int xes;
long int xfs;
long int xgs;
long int orig\_eax;
long int eip;
long int xcs;
long int eflags;
long int esp;
long int xss;
};
target : injection의 대상이 될 대상 프로세스 ( tracee )
1
2
3
4
5
int main(){
printf("%d\n", getpid());
getchar();
return 0;
}
실행하면 자신의 pid를 출력하고 대기한다.
대상 프로세스에 code를 injection할 때, 기계어를 써야 하므로 대상 프로세스에서 동작시킬 코드를 인라인 어셈블리로 작성하고 이에 대응되는 기계어 코드를 얻어낸다.
inlinecode : injection code의 assembly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
\_\_asm\_\_ \_\_volatile\_\_(
"xor %eax,%eax \n\t"
"push %eax \n\t"
"push $0x7463656a \n\t"
"push $0x6e692065 \n\t"
"push $0x646f4320 \n\t"
"push $0x6f686365 \n\t"
"mov %esp,%eax \n\t"
"push %eax \n\t"
"mov $0xb7e42160,%ebx \n\t"
"call \*%ebx \n\t"
"add $0x18, %esp \n\t"
);
return 0;
}
어셈블리 코드가 제대로 동작하는지 테스트.
- 이제 objdump 등을 이용해 기계어를 얻어내고,
ptrace(PTRACE_POKEDATA, ...)를 이용해 타겟 프로세스에 쓰고(injection),- EIP를 수정해 실행 흐름을 injection code 쪽으로 돌려서 code를 실행하면 된다.
다음은 injection을 수행할 프로세스의 코드다. injectcode[] 부분이 타겟 프로세스에 inject하게 될, little endian으로 정렬된 기계어 코드다. ptrace(PTRACE_POKEDATA, ...)의 입출력 단위는 long이다. 작업을 진행한 OS는 32bit로, sizeof(long) == 4다. 메모리에 4byte 씩 쓰려면 little endian으로 정렬해주어야 하기 때문에 injectcode를 little endian으로 정렬해 주었다. 처음에 0x90909090을 안넣으면 fault가 나서 추가해줬다. (core dump를 확인해보니 아마 eip 때문인 듯 싶다)
injector _code : ( tracer )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/ptrace.h>
long targetAddr(pid\_t pid){
FILE \*fp;
char filename[30];
char line[85];
long addr;
char str[20];
sprintf(filename, "/proc/%d/maps", pid);
fp=fopen(filename, "r");
if(fp == 0)
exit(1);
while(fgets(line, 85, fp) != 0){
printf("%s", line);
sscanf(line, "%x-%\*x %\*s %\*s %s", &addr, str, str, str, str);
//break at first 00:00 (device value)
if(strcmp(str, "00:00") == 0)
break;
/\*
//break at stack area
if(addr >= 0xbf000000)
break;
\*/
}
fclose(fp);
return addr;
}
int main(int argc, char \*\*argv){
pid\_t tracee;
struct user\_regs\_struct oldregs,regs;
long addr;
long peek;
int i;
long injectcode[] = {
0x90909090,
0x6850c031,
0x7463656a,
0x69206568,
0x6320686e,
0x6568646f,
0x896f6863,
0x60bb50e0,
0xffb7e421,
0x18c483d3
};
if(argc != 2){
puts("argc error");
exit(1);
}
tracee=atoi(argv[1]);
ptrace(PTRACE\_ATTACH, tracee, NULL, NULL);
wait(NULL);
ptrace(PTRACE\_GETREGS, tracee, NULL,regs);
printf("[old-eip = %p]\n", regs.eip);
printf("\n------------\n");
addr = targetAddr(tracee);
printf("[new-eip(=addr=injection\_point) = %p]\n", addr);
memcpy(&oldregs,regs, sizeof(regs));
regs.eip = addr+4;
printf("=====%p's old data=====\n", addr);
for(i = 0; i < sizeof(injectcode); i += sizeof(long)){
peek = ptrace(PTRACE\_PEEKDATA, tracee, addr+i, 0);
printf("%x, ", peek);
}
for(i = 0; i < sizeof(injectcode); i += sizeof(long)){
ptrace(PTRACE\_POKEDATA, tracee, addr+i, injectcode[i/sizeof(long)]);
}
printf("\n=====%p's new data=====\n", addr);
for(i = 0; i < sizeof(injectcode); i += sizeof(long)){
peek = ptrace(PTRACE\_PEEKDATA, tracee, addr+i, 0);
printf("%x, ", peek);
}
ptrace(PTRACE\_SETREGS, tracee, NULL,regs);
ptrace(PTRACE\_CONT, tracee, NULL, NULL);
wait(NULL); // wait int3
printf("\n...receive int3, restore original eip\n");
ptrace(PTRACE\_SETREGS, tracee, NULL, &oldregs);
ptrace(PTRACE\_DETACH, tracee, NULL, NULL);
}
procfs를 이용해 target에서 injection할만한 주소를 얻어낸 다음 그 자리에 injection하게된다.
이제 target을 실행하고, injectproc에 target의 pid를 넘기면 된다.
injectproc의 출력을 보면 target의 0xb7e06000에 제대로 injection되었음을 알 수 있다. 그러나 target에는 아무것도 출력되지 않았다. 왜인가 했더니 procfs의 flag를 보면 0xb7e06000에 실행권한이 없다. 즉 DEP가 걸려있다. DEP를 해제하고 다시 시도.
* DEP를 해제하지 않고 mprotect()로 x권한을 주는 식으로 진행해야 하지만, 귀찮았다.
제대로 injection되었고, injection code가 실행되어 target에서 code inject 문자열이 출력되었다.
so injection
windows의 dll injection이 CreateRemoteThread()를 이용해 LoadLibrary()를 호출하는 것 말고는 code injection과 별 차이가 없는 것 처럼 so injection도 그냥 injection code가 dlopen()을 호출하도록 하면 된다. 그래서 injection을 수행할 프로그램으로 위의 injectproc를 그대로 사용했으며 injectcode[]부분만 c dlopen()을 호출하도록 변경해주었다. 유의해야 할 것은, dlopen()이 libdl에 있기 때문에 libdl을 로드하는 프로세스만 injection이 된다는 것이다. 그래서 활용도가 상당히 떨어진다. 굳이 so injection해야 한다면,LD_PRELOAD 를 이용하는 편이 여러모로 낫다. hooking도 자동으로 되니까.
libshell.so : 타겟 프로세스에 injection할 library
1
2
3
4
\_\_attribute\_\_((constructor))
void constructorPrint(){
printf("Ok, injected\n\n");
}
\_\_attribute\__((constructor))가 붙어있으니 로드되자마자 constructorPrint()를 호출할 것이다.
target : injection 대상 프로세스 ( tracee )
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <dlfcn.h>
int main(){
dlopen(0, RTLD\_NOW); //to avoid optimization
printf("%d\n", getpid());
getchar();
printf("\nback to target\n");
getchar();
return 0;
}
처음에 -ldl만 써도 되는줄 알고 -ldl만 줬더니 최적화 때문에 libdl을 로드를 안한다. dlopen을 호출해야 libdl이 로드된다. 결국 injection code에서 dlopen()을 호출하려면 traget이 libdl을 로드한 상태여야 하기 때문에, libdl을 로드하도록 dlopen을 호출해줬다.
inlinecode : injection code의 assembly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <dlfcn.h>
int main(){
dlopen(0, RTLD\_NOW); //to avoid optimization
\_\_asm\_\_ \_\_volatile\_\_(
"xor %edx,%edx \n\t"
"mov $0x1,%dl \n\t"
"xor %eax,%eax \n\t"
"push %eax \n\t"
"push $0x6f732e6c \n\t"
"push $0x6c656873 \n\t"
"push $0x62696c2f \n\t"
"push $0x706d742f \n\t"
"mov %esp,%eax \n\t"
"push %edx \n\t"
"push %eax \n\t"
"mov $0xb7fbdcd0,%ebx \n\t"
"call \*%ebx \n\t"
);
return 0;
}
여기서는 딱히 dlopen을 호출할 필요는 없었으나, 컴파일해서 어셈블리 코드가 잘 동작하는지 확인해보기 위해서는 libdl이 있어야 하므로 dlopen을 호출해줬다.
injection을 수행할 프로그램으로는 위의 injectproc를 그대로 사용하면 된다. 단, injectcode부분을 dlopen을 호출하도록 변경해주어야한다.
injector _so : ( tracer )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
int injectcode[] = {
0x90909090,
0x01b2d231,
0x6850c031,
0x6f732e6c,
0x65687368,
0x6c2f686c,
0x2f686269,
0x89706d74,
0xbb5052e0,
0xb7fbdcd0,
0x90ccd3ff
};
...
성공적으로 target 프로세스에서 OK, injected 문자열이 출력되었다.