分享

Setjmp | Ret2Forever

 astrotycoon 2019-01-18


简介

setjmp是C语言解决exception的标准方案

函数间跳转我们常用的函数是goto
(goto是很常用的语句,由于国内众多教科书抵制,造成goto不能用的假象,去看看linux内核代码中有多少goto)
但在函数间的跳转就不能用goto了

1
2
3
4
5
6
7
8
9
10
11
12
13
void f()
{
    //...
    Label:
    //...
}
void g()
{
    //...
    GOTO Label;
    //...
}

我们要知道,要实现这种类型的跳转,和操作系统中任务切换的上下文(context)切换有点类似,我们只需要恢复Label标签处函数上下文即可。函数的上下文包括以下内容:

函数栈帧,主要是栈帧指针BP和栈顶指针SP
程序指针PC,此处为指向 Label 语句的地址
其它寄存器,这是和体系相关的

这样,在执行GOTO Label这条语句,我们恢复Label处的上下文,即完成跳转到Label处的功能。

Linux 会把进程的上下文保存在 task_struct 结构体中,切换时直接恢复。
这就是函数间进行跳转的基本原理,C语言中 setjmp和longjmp就为我们完成了这样的保存上下文和切换上下文的工作。

源代码分析

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <setjmp.h>
int setjmp(jmp_buf env);
setjmp 函数的功能是将函数在此处的上下文保存在 jmp_buf 结构体中,以供 longjmp 从此结构体中恢复。
参数 env 即为保存上下文的 jmp_buf 结构体变量;
如果直接调用该函数,返回值为 0;若该函数从 longjmp 调用返回,返回值为非零,由longjmp函数提供。
根据函数的返回值,我们就可以知道 setjmp 函数调用是第一次直接调用,还是由其它地方跳转过来的。
void longjmp(jmp_buf env, int val);
longjmp 函数的功能是从jmp_buf结构体中恢复由setjmp函数保存的上下文,该函数不返回,而是从setjmp函数中返回。
参数 env 是由 setjmp 函数保存过的上下文。
参数 val 表示从 longjmp 函数传递给 setjmp 函数的返回值,如果 val 值为0, setjmp 将会返回1,否则返回 val。
longjmp 不直接返回,而是从 setjmp 函数中返回,longjmp 执行完之后,程序就像刚从 setjmp 函数返回一样。

具体实现

我们以下面的例子分析在linux x86_64下的setjmp 与longjmp的具体实现:
示例如下:

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
/* setjmp_longjmp.c -- program handles error through ''setjmp()'' */
/* and longjmp() */
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
/* declare variable of type jmp_buf */
jmp_buf env;
void hello(void);
int main(void){
    /* Initialize ''resume_here'' by calling setjmp() */
    if (setjmp(env)) {
        printf("After \''longjump()\'', back in \''main()\''\n");
        printf("\''jump buffer variable \''resume_here\''\'' becomes "
                  "INVALID!\n");
    }
    else {
        printf("\''setjmp()\'' returns first time\n");
        hello();
    }
    return 0;
}
void hello(void){
    printf("Hey, I''m in \''hello()\''\n");
    longjmp(env, 1);
    /* other code */
    printf("can''t be reached here because I did longjmp!\n");
}

运行结果:

setjmp

首先,jmp_buf 的结构在bss段中
我们首先看jmp_buf的数据结构:调试进入到setjmp函数中,其源文件glibc/sysdeps/x86_64/setjmp.S是汇编写的,但是比较晦涩,我们从调试的角度看这段汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gdb-peda$ x/50i 0x7ffff7a421b0
=> 0x7ffff7a421b0 <__sigsetjmp>:    mov    QWORD PTR [rdi],rbx
   0x7ffff7a421b3 <__sigsetjmp+3>:  mov    rax,rbp
   0x7ffff7a421b6 <__sigsetjmp+6>:  xor    rax,QWORD PTR fs:0x30
   0x7ffff7a421bf <__sigsetjmp+15>: rol    rax,0x11
   0x7ffff7a421c3 <__sigsetjmp+19>: mov    QWORD PTR [rdi+0x8],rax
   0x7ffff7a421c7 <__sigsetjmp+23>: mov    QWORD PTR [rdi+0x10],r12
   0x7ffff7a421cb <__sigsetjmp+27>: mov    QWORD PTR [rdi+0x18],r13
   0x7ffff7a421cf <__sigsetjmp+31>: mov    QWORD PTR [rdi+0x20],r14
   0x7ffff7a421d3 <__sigsetjmp+35>: mov    QWORD PTR [rdi+0x28],r15
   0x7ffff7a421d7 <__sigsetjmp+39>: lea    rdx,[rsp+0x8]
   0x7ffff7a421dc <__sigsetjmp+44>: xor    rdx,QWORD PTR fs:0x30
   0x7ffff7a421e5 <__sigsetjmp+53>: rol    rdx,0x11
   0x7ffff7a421e9 <__sigsetjmp+57>: mov    QWORD PTR [rdi+0x30],rdx
   0x7ffff7a421ed <__sigsetjmp+61>: mov    rax,QWORD PTR [rsp]
   0x7ffff7a421f1 <__sigsetjmp+65>: nop
   0x7ffff7a421f2 <__sigsetjmp+66>: xor    rax,QWORD PTR fs:0x30
   0x7ffff7a421fb <__sigsetjmp+75>: rol    rax,0x11
   0x7ffff7a421ff <__sigsetjmp+79>: mov    QWORD PTR [rdi+0x38],rax
   0x7ffff7a42203 <__sigsetjmp+83>: jmp    0x7ffff7a42210 <__sigjmp_save>

这段代码就就是在x86-64体系下存在jmp_buf的内容,其中,我们可见有一个简单的亦或与循环移位的加密:

1
2
3
xor    rax,QWORD PTR fs:0x30
rol    rax,0x11

其中fs:0x30是每次会变化,但在一次运行的过程中是不变的,我们不妨称之为cookie,称整个加密过程为encrypt,用python描述如下:

1
2
3
4
5
6
7
8
def rol(Num):
    res = ((Num>>(64-0x11))+(Num<<0x11))&0xffffffffffffffff
    return res
def encrypt(Num,Cookie):
    Num = Num^Cookie
    result = rol(Num)
    return result

setjmp的作用就是将当前的程序状态存储到这个jmp_buf结构中,部分关键内容进行加密

jmp_buf

根据汇编中的内容,我们可以知道jmp_buf中保存的内容,整个jmp_buf的结构描述如下,虽然jmp_buf开辟空间不小,但在x64下只用到了一下空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x601080 <env>:     0x0000000000000000  0xd71da453f1bec959
0x601090 <env+16>:  0x00000000004004c0  0x00007fffffffd980
0x6010a0 <env+32>:  0x0000000000000000  0x0000000000000000
0x6010b0 <env+48>:  0xd71da453f1bec959  0x28e25b2c4b76c959
0x6010c0 <env+64>:  0x0000000000000000  0x0000000000000000
0x6010d0 <env+80>:  0x0000000000000000  0x0000000000000000
0x6010e0 <env+96>:  0x0000000000000000  0x0000000000000000
0x6010f0 <env+112>: 0x0000000000000000  0x0000000000000000
0x601100 <env+128>: 0x0000000000000000  0x0000000000000000
0x601110 <env+144>: 0x0000000000000000  0x0000000000000000
0x601120 <env+160>: 0x0000000000000000  0x0000000000000000
0x601130 <env+176>: 0x0000000000000000  0x0000000000000000
0x601140 <env+192>: 0x0000000000000000  0x0000000000000000

longjmp

longjmp的源代码glibc/sysdeps/x86_64/__longjmp.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
=> 0x7ffff7a422a0 <__longjmp>:      mov    r8,QWORD PTR [rdi+0x30]
   0x7ffff7a422a4 <__longjmp+4>:    mov    r9,QWORD PTR [rdi+0x8]
   0x7ffff7a422a8 <__longjmp+8>:    mov    rdx,QWORD PTR [rdi+0x38]
   0x7ffff7a422ac <__longjmp+12>:   ror    r8,0x11
   0x7ffff7a422b0 <__longjmp+16>:   xor    r8,QWORD PTR fs:0x30
   0x7ffff7a422b9 <__longjmp+25>:   ror    r9,0x11
   0x7ffff7a422bd <__longjmp+29>:   xor    r9,QWORD PTR fs:0x30
   0x7ffff7a422c6 <__longjmp+38>:   ror    rdx,0x11
   0x7ffff7a422ca <__longjmp+42>:   xor    rdx,QWORD PTR fs:0x30
   0x7ffff7a422d3 <__longjmp+51>:   nop
   0x7ffff7a422d4 <__longjmp+52>:   mov    rbx,QWORD PTR [rdi]
   0x7ffff7a422d7 <__longjmp+55>:   mov    r12,QWORD PTR [rdi+0x10]
   0x7ffff7a422db <__longjmp+59>:   mov    r13,QWORD PTR [rdi+0x18]
   0x7ffff7a422df <__longjmp+63>:   mov    r14,QWORD PTR [rdi+0x20]
   0x7ffff7a422e3 <__longjmp+67>:   mov    r15,QWORD PTR [rdi+0x28]
   0x7ffff7a422e7 <__longjmp+71>:   mov    eax,esi
   0x7ffff7a422e9 <__longjmp+73>:   mov    rsp,r8
   0x7ffff7a422ec <__longjmp+76>:   mov    rbp,r9
   0x7ffff7a422ef <__longjmp+79>:   nop
   0x7ffff7a422f0 <__longjmp+80>:   jmp    rdx

可见在longjmp中主要是有一个简单的解密过程,用python描述如下:

1
2
3
4
5
6
7
8
def rerol(Num):
    res = ((Num<<(64-0x11))+(Num>>0x11))&0xffffffffffffffff
    return res
def decrypt(Num,Cookie):
    Num = rerol(Num)
    result = Num^Cookie
    return result

之后从rdi,也就是jmp_buf中把相应的内容恢复,这样就实现了类似context的切换过程

整个过程中,理解setjmp最关键的是对整个jmp_buf结构的把握

使用setjmp和longjmp要注意以下几点:

1、setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,也即先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的context
2. longjmp必须在setjmp调用之后,而且longjmp必须在setjmp的作用域之内。具体来说,在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到setjmp的下一条语句执行。实际上setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放(函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。

setjmp异常处理

setjmp有一个重要的功能:exception 的抛出和捕获。
setjmp被用于包住一个例外处理,类似try。longjmp调用类似于throw语句,允许一个异常返回给setjmp一个异常值
读者可自行分析,不在赘述

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
void first(void);
void second(void);
/* This program''s output is:
calling first
calling second
entering second
second failed with type 3 exception; remapping to type 1.
first failed, exception type 1
*/
/* Use a file scoped static variable for the exception stack so we can access
* it anywhere within this translation unit. */
static jmp_buf exception_env;
static int exception_type;
int main() {
    void *volatile mem_buffer;
    mem_buffer = NULL;
    if (setjmp(exception_env)) {
        /* if we get here there was an exception */
        printf("first failed, exception type %d\n", exception_type);
    } else {
        /* Run code that may signal failure via longjmp. */
        printf("calling first\n");
        first();
        mem_buffer = malloc(300); /* allocate a resource */
        printf("%s",strcpy((char*) mem_buffer, "first succeeded!")); /* ... this will not happen */
    }
    if (mem_buffer)
        free((void*) mem_buffer); /* carefully deallocate resource */
    return 0;
}
void first(void) {
    jmp_buf my_env;
    printf("calling second\n");
    memcpy(my_env, exception_env, sizeof(jmp_buf));
    switch (setjmp(exception_env)) {
        case 3:
            /* if we get here there was an exception. */
            printf("second failed with type 3 exception; remapping to type 1.\n");
            exception_type = 1;
        default: /* fall through */
            memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
            longjmp(exception_env, exception_type); /* continue handling the exception */
        case 0:
            /* normal, desired operation */
            second();
            printf("second succeeded\n");  /* not reached */
    }
    memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
}
void second(void) {
    printf("entering second\n" ); /* reached */
    exception_type = 3;
    longjmp(exception_env, exception_type); /* declare that the program has failed */
    printf("leaving second\n"); /* not reached */
}

结果:

参考资料

http://en./wiki/Longjmp
http://www.cnblogs.com/hazir/p/c_setjmp_longjmp.html
http://www./c-questions-setjmp-longjmp-functions-standard-library-uses/
http://blog.csdn.net/chenyiming_1990/article/details/8683413

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多