当前位置: 首页 > 工具软件 > Argos > 使用案例 >

Argos-0.4.0 源码分析

闾丘京
2023-12-01

标记部分

寄存器标记

  1. 标记的类型及存储的内容

    这里讨论的是x86架构的情形。根据原作者的标记规则(在其文献中有说明),对8个通用寄存器和3个临时寄存器分别进行标记。这个标记是CPUX86State 结构体的成员。

/* ARGOS state */
    argos_rtag_t t0tag, t1tag, t2tag;// 3个临时寄存器如
    argos_rtag_t regtags[CPU_NB_REGS];// 8个通用寄存器

寄存器的标签类型是一个结构体 typedef argos_rtag argos_rtag_t , 这个结构体存了两种数据,即数据从内存中load进来时的内存物理地址(因为是32位系统所以是uint32_t =4B,若是64位系统则是uint64_t )和从网络中直接传过来数据的索引值(uint32_t )

struct argos_rtag {
    argos_paddr_t origin;    //!< Memory origin of register
    argos_netidx_t netidx;    //!< Network index of data
};

这个寄存器的标签是在 #ifdef ARGOS_NET_TRACKER 时才是一个结构体。如果没有def ,那么就直接是 typedef argos_paddr_t argos_rtag_t ,即只是 uint32_tuint64_t 的物理内存地址

同上面标签的定义类似,当#ifdef ARGOS_NET_TRACKER 时,这时候有一堆的宏定义关于netidx 和 stage 的。(这里有很大的疑问关于stage_mask 和 netidx_mask 有待解决!

solution :

 Remco Vermeulen 在他对ARGOS 的扩展中提到了,如果 def ARGOS_NET_TRACKER ,那么就将netidx 分成两块(这个两块其实是从比特位的角度考虑的),一块是stage ,一块是netidx 。还有个mask ,这个术语是在中文中通常翻译成掩码,这个掩码的意思可以做如下比方,e.g  map 这个可以表示成第一位是p,第二位是a,第三位是m,那么我们知道在c语言中& 位操作运算可以用来取特定位,那么我们这个如果要取第一位p,就要把map和1相&,那么它的mask ,可以说是1 ,同理取第二位a,那么就要把map和2相&,那么它的mask可以说是2。

因此下面的ARGOS_NET_MASK和 ARGOS_STAGE_MASK 都可以理解为 netidx 和 stage 的掩码。
#define ARGOS_NETIDX_MASK ( (ARGOS_MAX_NETIDX) >> (ARGOS_STAGE_SIZE) ) //因为max_netidx 是 0xffffffff 而 stage_size 是 2 (这里的2表示2个比特位),即可以表示NETIDX_MASK=0x3fffffff(即低30位)
#define ARGOS_STAGE_MASK ( (ARGOS_MAX_NETIDX) ^ (ARGOS_NETIDX_MASK) )// stage_mask 自然就是高2位,STAGE_MASK=0xbfffffff
#define ARGOS_GET_STAGE(N) ( ((N) & (ARGOS_STAGE_MASK)) >> ((ARGOS_MAX_STAGE_SHIFTS) - (ARGOS_STAGE_SIZE) ) )//取stage,因为是高2位,所以与mask &后得右移30位(这里的右移的位数是netidx的位数,即所存数据的位数减去stage位数)
#define ARGOS_SET_STAGE(N, X) ( (N) = ((((X) << ((ARGOS_MAX_STAGE_SHIFTS) - (ARGOS_STAGE_SIZE))) & (ARGOS_STAGE_MASK)) | ((N) & (ARGOS_NETIDX_MASK))) )// x 表示stage
#define ARGOS_INCREMENT_STAGE(N) ( ARGOS_SET_STAGE( (N), ( (ARGOS_GET_STAGE(N)) + 1) )  )// 增加stage
#define ARGOS_CLEAR_STAGE(N) ( (N) = ( (ARGOS_NETIDX_MASK) & (N) ) )//清楚stage
#define ARGOS_GET_NETIDX(N) ( (N) & (ARGOS_NETIDX_MASK) )//与netidx掩码&即可得到netidx
#define ARGOS_SET_NETIDX(N, X) ( (N) = ( ((N) & (ARGOS_STAGE_MASK) ) | ( (X) & (ARGOS_NETIDX_MASK) ) ) )// x表示netidx

当没有def net tracker 时,由于此时标签存储的内容就是物理内存的地址。

#define argos_tag_set(tag, addr) (*(tag) = (addr))
#define argos_tag_copy(dst, src)  (*(dst) = *(src))
#define argos_tag_clear(tag)     (*(tag) = 0)
#define argos_tag_isdirty(tag)    (*(tag) != 0)
#define argos_tag_origin(tag)    (*(tag))
 
  1. 对标签的操作

内存标记

bytemap

  1. bytemap的生成,在argos-bytemap.c 文件中,这里有一个create 和 createz 。在这里map是个指针,但是bytemap中map是个数组,能用指针表示数组吗?因为这里malloc了一段内存,并用map指向这块内存,因此可以当做数组用,指针加一相当于数组索引加一。
argos_bytemap_t * argos_bytemap_createz(size_t len)
{
argos_bytemap_t *map;
#ifdef ARGOS_NET_TRACKER
    len = len * 4;//这里乘以4,是因为有net_tracker,那么高两位就是stage ,自然map大小就要扩大4倍
#endif
    map = (argos_bytemap_t *)qemu_vmalloc(len);
    if (!map) {
        qemu_fprintf(stderr, "[ARGOS] Not enough memory\n");
        exit(1);
    }
    memset(map, 0, len);//在create中没有这个,这个是函数的功能是将map进行初始化
    return map;
}
  1. 操作

    1.not def ARGOS_NET_TRACKER主要的 load 操作如下

    #define ARGOS_BYTEMAP_LD_OP(opsfx, type)               
    static inline void                           
    glue(argos_bytemap_ld, opsfx)                       
    (argos_bytemap_t *map, unsigned long maddr, unsigned long paddr,    
                             argos_rtag_t *tag)   
    {                                   
        if (*(type *)(map + maddr) == 0)               
            argos_tag_clear(tag);                   
        else                               
            argos_tag_set(tag, paddr);               
    }
    

    而def ARGOS_NET_TRACKER 则是这样

    static inline void
    argos_bytemap_ldl(argos_bytemap_t *map, unsigned long maddr,
            unsigned long paddr, argos_rtag_t *tag)
    {
    #if 0
        if (map[maddr] || map[maddr + 1] || map[maddr + 2] || map[maddr + 3])
        {
    #endif
        int i;
        for (i = 0; i < 4; i++)
            if (map[maddr + i]) {
                argos_tag_set(tag, paddr, map[maddr + i]);
                return;
            }
    #if 0
        }
        else
    #endif
        argos_tag_clear(tag);
    }
    
    

    对比两个可以发现主要区别在 argos_tag_set() 这个函数,这函数上面提到了在是否定义了ARGOS_NET_TRACKER 是不同的,因为如果def 那么寄存器标签里就不仅仅只存了源物理地址还有网络数据的索引值,而这个map[] 数组存的就是网络数据的索引值;但是没有def 那么这个map[]数组里就只是存了0或1(1表示污染)。还有就是位图中标记的区域。

    2.store 操作

    not def ARGOS_NET_TRACKER

    #define ARGOS_BYTEMAP_ST_OP(opsfx, type)                   
    static inline void                               
    glue(argos_bytemap_st, opsfx)                           
    (argos_bytemap_t *map, unsigned long maddr, const argos_rtag_t *tag)       
    {                                       
        *(type *)(map + maddr) = (argos_tag_isdirty(tag))? ~(type)0 : (type)0;
    }
    

    def ARGOS_NET_TRACKER

    static inline void
    argos_bytemap_stl(argos_bytemap_t *map, unsigned long maddr,
            const argos_rtag_t *tag)
    {
        int i;
    
        for (i = 0; i < 4; i++)
            map[maddr + i] = argos_tag_netidx(tag);
    #if 0
        map[maddr + 3] = map[maddr + 2] = map[maddr + 1] = map[maddr] =
                (argos_tag_isdirty(tag))? argos_tag_netidx(tag) : 0;
    #endif
    }
    
    

    没有def 时,如果store多字节数据,在位图中也只存标志0或1,其size与store的数据size相同。有def时,如果store多字节数据,而内存中1地址存1byte数据,所以在位图中从首地址开始每个地址都存数据的索引值。


    在上面load操作时候,有个for循环来对load进寄存器的数据进行标记,根据数据大小遍历所取数据的内存地址,check是否有标记,然后对相应的load寄存器设置标记。这里有个疑问,每个寄存器的标签也就是一个,这每次循环check,若是污点,那就将此地址在寄存器中标记,但是并没有break,那下次循环又覆盖了上次标记的地址?


    3.move 操作

    没有 def

    #define ARGOS_BYTEMAP_MOV_OP(opsfx, type)                 
    static inline void                             
    glue(argos_bytemap_mov, opsfx)(argos_bytemap_t *map, unsigned long daddr, 
            unsigned long saddr)                     
    {                                     
        *(type *)(map + daddr) = *(type *)(map + saddr);         
    }
    ARGOS_BYTEMAP_MOV_OP(b, uint8_t)
    ARGOS_BYTEMAP_MOV_OP(w, uint16_t)
    ARGOS_BYTEMAP_MOV_OP(l, uint32_t)
    ARGOS_BYTEMAP_MOV_OP(q, uint64_t)
    
    

    有 def

    static inline void
    argos_bytemap_movl(argos_bytemap_t *map, unsigned long daddr,
            unsigned long saddr)
    {
    #if 0
        argos_bytemap_movw(map, daddr, saddr);
        argos_bytemap_movw(map, daddr + 2, saddr + 2);
    #endif
        int i;
    
        for (i = 0; i< 4; i++)
            map[daddr + i] = map[saddr + i];
    }
    

    也就是map[]中存的数据类型不同。

标记进来的网络数据

在上面store 操作中是对从寄存器传输到内存的数据是否被污染来进行污点的传播 ,check的是 寄存器的tag, 在没有net_tracker时就是存的此数据之前从内存传输到寄存器时的地址,否则就是netidx ,两者只要非0都判为被污染,然后将内存的位图进行标记**

而load操作是对从内存传输到寄存器的数据是否污染来进行污点的传播,check的是当前数据的地址位图中是否被标记,如果被标记则将寄存器的tag也进行标记。

那这个最初始的标记是哪里开始的,也就是网络数据从外面传进来这个污点的标记是哪里开始的?

*solution:既然得找传进来的网络数据,就得知道此版本的qemu是通过哪个网卡与外界进行网络通信的,原作者在其文献中提到对ne2000 进行仿真,那应该是通过此网卡,所以如果要切换网卡,模拟其它网卡设备,初始的污点数据都没有标记成功,所以自然不会检测到攻击。下面,详细看下 hw/ne2000.c 里的相关操作。

有个NE2000state的结构体,其中有个和标记相关的成员

ifdef ARGOS_NET_TRACKER
    argos_netidx_t tag[NE2000_MEM_SIZE];
#else
    uint8_t tag[NE2000_MEM_SIZE];
#endif

在ne2000_recieve() 函数里,有个while()

while (size > 0) {
        if (index <= s->stop)
            avail = s->stop - index;
        else
            avail = 0;
        len = size;
        if (len > avail)
            len = avail;
        memcpy(s->mem + index, buf, len);
#ifdef ARGOS_NET_TRACKER
    // Write data to log
    if (fwrite(buf, len, 1, argos_nt_fl) != 1)
    {
        fprintf(stderr, "Error writing net trace data\n");
        exit(1);
    }
    for (i = 0; i < len; i++)
    {
        //s->tag[index + i] = argos_ne2000_netidx++;
        // Here we split the assignment and the post increment operator,
        // because this can give unformentioned effects in combination
        // with macro's.
        ARGOS_SET_NETIDX(s->tag[index + i], argos_ne2000_netidx);
        argos_ne2000_netidx++;
    }
#else
    memset(s->tag + index, 0xff, len);
#endif
 
        buf += len;
        index += len;
        if (index == s->stop)
            index = s->start;
        size -= len;
    }

while() 之前的是接受数据包的一些操作,到while是将已经接收的数据包再写到存 ne2000 结构体里的 mem[] 数组里,同时对这些数据进行标记。整个while循环是根据数据包的size 和一次可以 写的len 来进行比较循环,其中嵌套了一个for 循环,是在一个len 里,对每个数据进行标记。如果def net_tracker ,那么就在 s-tag[] 里从初始位置开始写入 argos_ne2000_netidx++(从1开始) ,否则在这块size大小的区域写入0xff 。

当然mem[] 这块区域不只存从网络接收的数据,当你和外界通信时,数据要传出去,也要先存在这里,此时这个数据被认为是安全的数据,即会将存放的相应地址的污点标记给清除。比如下面这个往mem[] 里写的操作

static inline void ne2000_mem_writeb(NE2000State *s, uint32_t addr,
                                     uint32_t val)
{
    if (addr < 32 ||
        (addr >= NE2000_PMEM_START && addr < NE2000_MEM_SIZE)) {
        s->mem[addr] = val;
    argos_ne2000_clearb(s, addr);
    }
}

自然也会有从mem[] 里读的操作

static inline uint32_t ne2000_mem_readb(NE2000State *s, uint32_t addr,
        argos_rtag_t *tag)
{
    if (addr < 32 ||
        (addr >= NE2000_PMEM_START && addr < NE2000_MEM_SIZE)) {
    argos_ne2000_readb(s, addr, tag);
        return s->mem[addr];
    } else {
        return 0xff;
    }
}

这里调用了argos_ne2000_readb() ,也就是将网络数据从mem[] 转移到寄存器中去时,如果def net_tracker 寄存器的标签都是

tag->origin=NE2000_DIRTY_TAG= -1 tag->netidx= s->tag[addr];否则是

addr=NE2000_DIRTY_TAG= -1

static inline void
argos_ne2000_readb(NE2000State *s, uint32_t addr, argos_rtag_t *tag)
{
    //if (s->tag[addr])
    if (ARGOS_GET_NETIDX(s->tag[addr]))
    {
        argos_tag_set(tag, NE2000_DIRTY_TAG, s->tag[addr]);
    }
}
 
#define ARGOS_NE2000_READ_OP(sfx, type)                   
static inline void                           
glue(argos_ne2000_read, sfx)(NE2000State *s, uint32_t addr,        
        argos_rtag_t *tag)                   
{                                   
    if (*(type *)(s->tag + addr))                   
        argos_tag_set(tag, NE2000_DIRTY_TAG);       
}
ARGOS_NE2000_READ_OP(b, uint8_t)
ARGOS_NE2000_READ_OP(w, uint16_t)
ARGOS_NE2000_READ_OP(l, uint32_t)

从这里可以看出寄存器中如果标签里存的地址是 -1 那么可以说明此数据刚从网卡中加载到寄存器。


跟踪部分

argos因为是在qemu的基础上扩展了污点分析,即在qemu上进行插装。这里我们讨论的qemu版本是0.10以前,使用的是“dynamic translation” 而这之后是采用 “TCG” ,不用对GCC的依赖,并且不需要编译中间工具(所以在这里之前编译这个版本的argos时,需要gcc 3.x 应该是这个qemu的原因)。

动态翻译的基本思想就是把每条指令分解成几条微指令,然后将这些微指令用C 来实现(放在target-i386/op.c),然后通过中间工具dygen,提取相应的目标文件(op.o)来生成一个动态代码生成器,最后把这些微指令组合成一个函数(target-i386/op.h:dygen_code())

在一个真实的CPU里,执行流程由取指,译指,执行三个部分组成,argos插装也就在译指这个部分。指令的反汇编代码由反汇编器进行解析,具体过程在translate.c:disas_insn()。因为qemu仿真运行过程中在不断地执行指令,而在这个过程中就会用到之前标记的污点数据,因此就涉及到污点的传播。这里在指令的翻译成微指令的同时根据微指令的不同功能进行污点的传播。

argos的开发者只是只是针对地检测这些攻击:

  1. (通过加载攻击者的注入的代码、数据来改变原来的EIP的值来达到劫持控制流的目的,因为这些注入被标记为污点才能被检测)。而涉及到EIP的就三个操作,call,ret,jmp ,因此对这三个操作进行check就可以防止污染数据被加载或者说就能检测到此类攻击。

    开发者在翻译中调用了ARGOS_CHECK() 这个函数共有四个操作:

    void OPPROTO op_argos_jmp_T0(void)
    {
        target_ulong old_pc;
    
        old_pc = EIP + env->segs[R_CS].base;
        EIP = T0;
        ARGOS_CHECK(T0TAG, old_pc, ARGOS_ALERT_JMP);
    #ifdef ARGOS_TRACKSC
        argos_tracksc_on_jmp(env);
    #endif
    }
    

    通过JMP来改变EIP的值 jmp 寄存器/立即数(一个jmp指令,意思是将T0寄存器中的地址赋给EIP,即下条指令的地址),因为涉及到EIP中的数据,所以调用argos_check。即检查T0TAG和当前env->eip是否被污染。分别调用argos_tag_isdirty(tag) 检测和argos_dest_pc_isdirty(env,env->eip)这里有个疑问为什么要同时判断T0寄存器标签和当前的EIPsolution:两个检测因为T0存储的是下一个指令EIP,得判断即将跳转的地址是否被污染,以及现在的EIP也就是调用这个跳转指令的EIP是否已经被污染了,两者其一符合条件都触发警报

    void OPPROTO op_argos_call_jmp_T0(void)
    {
        target_ulong old_pc;
    
        old_pc = EIP + env->segs[R_CS].base;
        EIP = T0;
        ARGOS_CI_CHECK(T0TAG, old_pc, ARGOS_ALERT_CALL);
    

    这个是通过call 来改变EIP的值 call 地址/寄存器 。那么就调用ARGOS_CI_CHECK()。CI_CHECK( code injection ? )这个相比于ARGOS_CHECK(),只是调用 argos_dest_pc_isdirty() 即就是检测这个调用call 当前EIP是否被污染了,这个为什么不检测 argos_tag_isdirty()

    void OPPROTO op_argos_ret_jmp_T0(void)
    {
        target_ulong old_pc;
    
        old_pc = EIP + env->segs[R_CS].base;
        EIP = T0;
        ARGOS_CHECK(T0TAG, old_pc, ARGOS_ALERT_RET);
    

    这个是通过 ret 来改变EIP的值 相当于 pop eip (将栈中的返回地址弹到EIP执行EIP 所指的指令) 。也是调用ARGOS_CHECK(),因为既要对当前EIP也就是ret指令check是否是污染的EIP,又要对ret 的EIP check是否下条即将执行的是否被污染。

  2. 仅仅改变关键函数的参数(如系统调用)的攻击,通过check execve() 这样的函数参数何时被污染了。(每种系统的系统调用不尽相同,这个检测也是个麻烦事)

  3. 对于格式化字符串。

    (这是作者在06年时)在其发表的论文称还可以对MSR和段寄存器进行check,但那个时候没有发现此类攻击,便没有扩展。所以argos只能检测代码注入来劫持控制流的攻击,现在9102了,还有哪些其它的经典攻击手段是否能进行扩展?

报警部分

在上述跟踪部分每个微操作在执行过程中同时也执行了argos的污点传播,如果在设定的check里检测到了污点,那么就触发警报。

报警后生成log ,这里的log 是调用argos_csi

argos_csi(CPUX86State *env, target_ulong new_pc, argos_rtag_t *eiptag,
        target_ulong old_pc, int code)
{
#ifdef CONFIG_USER_ONLY
    qemu_fprintf(stderr, "forensics currently not supported\n");
    return 0;
#else
    FILE *fp;
    int rid;
    char fn[128];
    //rid = rand();
    rid = argos_instance_id;
    snprintf(fn, 128, LOG_FL_TEMPLATE, rid);
    if ((fp = fopen(fn, "wb")) == NULL) {
        perror("Could not create argos log - fopen()");
        return -1;
    }
    if (argos_header_write(fp, env, new_pc, old_pc, code, eiptag) != 0)
        goto cleanup;
    if (argos_process_proc(fp, env, new_pc, 1, 0) != 0)
        goto cleanup;
    if (argos_log_finalize(fp) != 0)
        goto cleanup;
    fclose(fp);
    argos_logf(LOG_MSG_TEMPLATE, fn);
    return rid;
cleanup:
    fclose(fp);
    return -1;
#endif
}
 
 类似资料: