3.1.1 Linux系统调用原理
每个系统调用都是通过一个单一的入口点多路传入内核。eax 寄存器用来标识应当调用的某个系统调用,这在 C 库中做了指定(来自用户空间应用程序的每个调用)。当加载了系统的 C 库调用索引和参数时,就会调用一个软件中断(0x80 中断),它将执行 system_call 函数(通过中断处理程序),这个函数会按照 eax 内容中的标识处理所有的系统调用。在经过几个简单测试之后,使用 system_call_table 和 eax 中包含的索引来执行真正的系统调用了。从系统调用中返回后,最终执行 syscall_exit,并调用 resume_userspace 返回用户空间。然后继续在 C 库中执行,它将返回到用户应用程序中。
Unistd.h文件中各个系统调用编号:
#ifndef _ASM_I386_UNISTD_H_
#define _ASM_I386_UNISTD_H_
/*
* This file contains the system call numbers.
*/
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_lchown 16
#define __NR_break 17
#define __NR_oldstat 18
#define __NR_lseek 19
#define __NR_getpid 20
#define __NR_mount 21
#define __NR_umount 22
#define __NR_setuid 23
#define __NR_getuid 24
#define __NR_stime 25
#define __NR_ptrace 26
#define __NR_alarm 27
3.1.2 中断描述符表和中断描述符寄存器
在保护模式下,中断向量表中的表项由8个字节组成,如图所示,中断向量表也改叫做中断描述符表IDT(Interrupt Descriptor Table)。其中的每个表项叫做一个门描述符(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。
其中类型占3位,表示门描述符的类型,主要门描述符是:
· 中断门(Interrupt gate)
其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。
· 陷阱门(Trap gate)
其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。
· 系统门(System gate)
这是Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门,因此,门描述符的DPL为3。通过系统门来激活4个Linux异常处理程序,它们的向量是3、4、5及128,也就是说,在用户态下,可以使用int3、into、bound 及int0x80四条汇编指令。
最后,在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。为此,CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存IDT的基址
3.1.3 中断描述符表寄存器和中断描述符表的获取(32位系统)
定义:
// 中断描述符表寄存器结构
struct {
unsigned short limit;
unsigned int base;
} __attribute__((packed)) idtr;
// 中断描述符表结构
struct {
unsigned short off1;
unsigned short sel;
unsigned char none, flags;
unsigned short off2;
} __attribute__((packed)) idt;
//获取代码
// 查找sys_call_table的地址
// 获取中断描述符表寄存器的地址
asm("sidt %0":"=m"(idtr));
// 获取0x80中断处理程序的地址,中断为256种,0x80即第128种,每个idt的大小是8个字节,所以偏移是8 * 0x80
memcpy(&idt, idtr.base+8*0x80, sizeof(idt));
sys_call_off=((idt.off2<<16)|idt.off1); //偏移量计算地址,off2是高位地址, off1是低十六位地址
//搜索call sys_call_table * (% eax, 4)对应的机器码0xff 0x14 0x85 *addres
p=sys_call_off;
for (i=0; i<100; i++)
{
if (p[i]=='/xff' && p[i+1]=='/x14' && p[i+2]=='/x85')
{
sys_call_table=*(unsigned int*)(p+i+3);
printk("addr of sys_call_table: %x/n", sys_call_table);
return sys_call_table;
}
}
3.1.4 中断描述符表寄存器和中断描述符表的获取(64位系统)
定义:
struct idtr {
unsigned short limit;
unsigned long base; //in 64bit mode, base address is 8 bytes
} __attribute__ ((packed));
/**
* in long mode -- 64bit mode and compatity mode,
* every IDT entry has a 16-byte size
*/
struct idt {
u16 offset_low;
u16 segment;
unsigned ist : 3, zero0 : 5, type : 5, dpl :2, p : 1;
u16 offset_middle;
u32 offset_high;
u32 zero1;
} __attribute__ ((packed));
//获取中断描述符寄存器
asm("sidt %0"
:"=m"(idtr)
: );
dbgprint("idtr base at %p/n", (void *)idtr.base);
/**
* Read in IDT for vector 0x80 (syscall)
*/
//查找0x80即128对应的描述符表,在64位下idt为16个字节
memcpy(&idt, (char *) idtr.base + 16 * 0x80, sizeof(idt));
//地址为64位,原理和32位一样,组合各个偏移地址,即可获得
sys_call_off = ( ( (unsigned long)idt.offset_high ) << 32 ) |
( ((idt.offset_middle << 16 ) | idt.offset_low) & 0x00000000ffffffff );
//搜索call sys_call_table * (% eax, 4)对应的机器码0xff 0x14 0x85 *addres
3.1.5 如何实现app抓连接功能
一般的tcp clinet 建立过程如下:
Create -> bind -> connect -> send 或者recv
所以只需要对connect的时候做一点手脚,就可以实现连接转移功能。
也就是将connect调用的第二个参数struct sockaddr_in
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
目的ip地址和端口进行修改即可达到该目的。
Tcp的系统调用都是由sys_socketcall进行多路分解,sys_socketcall的实现如下:
asmlinkage long sys_socketcall(int call, unsigned long *args)
{
unsigned long a[6];
unsigned long a0,a1;
int err;
if(call<1||call>SYS_RECVMSG)
return -EINVAL;
/* copy_from_user should be SMP safe. */
if (copy_from_user(a, args, nargs[call]))
return -EFAULT;
a0=a[0];
a1=a[1];
switch(call)
{
case SYS_SOCKET:
err = sys_socket(a0,a1,a[2]);
break;
case SYS_BIND:
err = sys_bind(a0,(struct sockaddr *)a1, a[2]);
break;
case SYS_CONNECT:
err = sys_connect(a0, (struct sockaddr *)a1, a[2]);
break;
case SYS_LISTEN:
err = sys_listen(a0,a1);
break;
case SYS_ACCEPT:
err = sys_accept(a0,(struct sockaddr *)a1, (int *)a[2]);
break;
case SYS_GETSOCKNAME:
err = sys_getsockname(a0,(struct sockaddr *)a1, (int *)a[2]);
break;
case SYS_GETPEERNAME:
err = sys_getpeername(a0, (struct sockaddr *)a1, (int *)a[2]);
break;
case SYS_SOCKETPAIR:
err = sys_socketpair(a0,a1, a[2], (int *)a[3]);
break;
case SYS_SEND:
err = sys_send(a0, (void *)a1, a[2], a[3]);
break;
case SYS_SENDTO:
err = sys_sendto(a0,(void *)a
我们只需要处理call == SYS_CONNECT的情况,当然也要做相应的过滤,防止影响其他的正常调用,实现代码如下:
// 劫持系统调用
static int __init init_get_sys_call_table(void)
{
sys_call_table = (long *)disp_sys_call_table();
printk("addr of sys_call_table %x/n" ,sys_call_table);
old_call = sys_call_table[102]; //unistd.h 中sys_socketcall 为 102
sys_call_table[102] = new_sys_socketcall;
return 0;
}
//我们实现的系统调用
asmlinkage static long new_sys_socketcall(int call, unsigned long *args)
{
if (call == SYS_CONNECT) //判断是connect调用
{
unsigned long a[6];
unsigned long a0,a1;
int err;
int fd = NULL;
if(call<1||call>SYS_RECVMSG)
return -EINVAL;
if (copy_from_user(a, args, nargs[call])) //获取系统调用参数
return -EFAULT;
a0=a[0];
a1=a[1];
struct sockaddr* pSockaddr = (struct sockaddr*)a1;
if (pSockaddr ->sa_family == AF_INET) //判断是IP族
{
//判断是tcp连接, 这里省略
struct sockaddr_in *usin = (struct sockaddr_in *)a1;
struct sockaddr_in dst;
dst.sin_family = AF_INET;
dst.sin_port = htons(443);
dst.sin_addr.s_addr = in_aton("200.200.30.122");//劫持到122,443
if (usin->sin_addr.s_addr == in_aton("200.200.30.228") && port)//是否要劫持的
{
//修改用户空间的参数,实现劫持
if (copy_to_user(args[1], &dst, sizeof(struct sockaddr_in)))
{
printk("my god /n");
}
}
}
}
return old_call(call, args); //走以前的逻辑
}
3.1.6 后续相关处理
(1) 这里是以驱动的方式实现对连接的抓取,假如用户机器的内核版本不在我们提供的范围之内,需要在用户本机编译
(2) 资源列表可以以模块参数的形式传入或者其他方式传入,需要实现高效的过滤方式
(3) 如何让应用层识别接入连接具体到用户内网的资源
需要修改如下:
//修改用户空间的参数,实现劫持
if (copy_to_user(args[1], &dst, sizeof(struct sockaddr_in)))
{
printk("my god /n");
}
Err = old_call(call, args); //判断连接本地是否成功,
If (成功)
{
Getsockname; //获取该连接分配的本地端口
Notify //通知应用层 端口和资源的对应关系 }
Return;